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,240 @@
1
+ /**
2
+ * @file slack-notify.ts
3
+ * @description The operator's one free action per application (#4999): post
4
+ * a new `ProjectListing` to the site's Slack channel with Approve/Reject
5
+ * buttons, then update that SAME message in place once a decision lands.
6
+ *
7
+ * A thin `fetch` client over the Slack Web API — no Slack SDK (this
8
+ * environment's contract). `AILK_SLACK_BOT_TOKEN`/`AILK_SLACK_CHANNEL_ID`
9
+ * absent means Slack is off: both exports become no-ops that log
10
+ * `slack-skipped` (id only) and never throw — a create must never fail
11
+ * because Slack is unconfigured (or down), and a decision must always be
12
+ * appliable even if the outcome can't be posted back.
13
+ *
14
+ * PII boundary: only `listingId` (plus a Slack API `error` code, never a
15
+ * response body) ever reaches a log line here — never `founderEmail`,
16
+ * `founderName`, `oneLiner`, `problem`, or any other applicant-authored text.
17
+ * The Block Kit body sent TO Slack necessarily carries that text (that's the
18
+ * point of the notification) — Slack, not this process's own logs, is the
19
+ * destination for it.
20
+ *
21
+ * Security note (PR #4999 review): every applicant-authored string
22
+ * (`oneLiner`/`problem`/`founderName`/`founderCompany`/`url`) is
23
+ * submitted by an ANONYMOUS, unauthenticated caller (`POST
24
+ * /v1/project-listings`) and lands in `mrkdwn` fields, which Slack parses for
25
+ * `<!channel>`/`<!here>`/`<@U…>` mentions and `<url|label>` masked links — an
26
+ * applicant could otherwise mass-ping the channel or mask a phishing link
27
+ * behind innocuous-looking text. `escapeMrkdwn` (Slack's own documented
28
+ * escaping: `&` -> `&amp;`, `<` -> `&lt;`, `>` -> `&gt;`, in that order) runs
29
+ * on every such string, and on the Slack-supplied `decidedByName` too (lower
30
+ * risk — only a member of the notified channel controls it — but the same
31
+ * fix applies for defense in depth).
32
+ */
33
+ import { logger } from "@working-theory/observability";
34
+
35
+ import { config } from "../config/index.js";
36
+ import { prisma } from "../lib/prisma.js";
37
+
38
+ const SLACK_API_BASE = "https://slack.com/api";
39
+
40
+ /** The subset of a ProjectListing this module ever renders into a Slack message. */
41
+ export type ProjectListingSlackInfo = {
42
+ id: string;
43
+ /**
44
+ * Nullable since #5036 D1: every application field is nullable at rest so a
45
+ * half-filled draft is storable. A listing only reaches Slack once it has
46
+ * been SUBMITTED, so in practice these are set — the nulls are rendered as
47
+ * an em dash rather than asserted away, because a `!` here would turn a data
48
+ * surprise into a crash inside a notifier that is not allowed to throw.
49
+ */
50
+ name: string | null;
51
+ oneLiner: string | null;
52
+ problem: string | null;
53
+ url: string | null;
54
+ /** The six-rung product ladder that replaced `stage` (#5036 D2). */
55
+ product: string | null;
56
+ founderName: string;
57
+ founderCompany: string | null;
58
+ };
59
+
60
+ /** What an absent field renders as in a Slack field. */
61
+ const ABSENT = "—";
62
+
63
+ type SlackApiResult = {
64
+ ok: boolean;
65
+ error?: string;
66
+ ts?: string;
67
+ channel?: string;
68
+ };
69
+
70
+ /**
71
+ * Slack's own documented `mrkdwn` escaping — `&` first, then `<`/`>` — so an
72
+ * applicant-authored string can never open a `<!channel>`/`<@U…>` mention or
73
+ * a `<url|label>` masked link inside a `mrkdwn` field.
74
+ */
75
+ function escapeMrkdwn(value: string): string {
76
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
77
+ }
78
+
79
+ async function callSlackApi(
80
+ token: string,
81
+ method: "chat.postMessage" | "chat.update",
82
+ body: Record<string, unknown>,
83
+ ): Promise<SlackApiResult> {
84
+ const res = await fetch(`${SLACK_API_BASE}/${method}`, {
85
+ method: "POST",
86
+ headers: {
87
+ "content-type": "application/json; charset=utf-8",
88
+ authorization: `Bearer ${token}`,
89
+ },
90
+ body: JSON.stringify(body),
91
+ });
92
+ return (await res.json()) as SlackApiResult;
93
+ }
94
+
95
+ /**
96
+ * The header + info-fields blocks shared by the initial post and the decided
97
+ * update. Every applicant-authored string is `escapeMrkdwn`-ed before it
98
+ * enters a `mrkdwn` field (see the module doc's security note) — an
99
+ * anonymous applicant's `oneLiner`/`problem`/`founderName`/
100
+ * `founderCompany`/`url` can otherwise inject a Slack mention or a masked
101
+ * phishing link. `listing.name` is NOT escaped here — it renders in the
102
+ * `header` block as `plain_text`, which Slack never interprets as markup.
103
+ */
104
+ function infoBlocks(listing: ProjectListingSlackInfo): unknown[] {
105
+ const fields = [
106
+ { type: "mrkdwn", text: `*One-liner:*\n${escapeMrkdwn(listing.oneLiner ?? ABSENT)}` },
107
+ { type: "mrkdwn", text: `*Problem:*\n${escapeMrkdwn(listing.problem ?? ABSENT)}` },
108
+ { type: "mrkdwn", text: `*Product:*\n${escapeMrkdwn(listing.product ?? ABSENT)}` },
109
+ ...(listing.url ? [{ type: "mrkdwn", text: `*URL:*\n${escapeMrkdwn(listing.url)}` }] : []),
110
+ {
111
+ type: "mrkdwn",
112
+ text: `*Founder:*\n${escapeMrkdwn(listing.founderName)}${
113
+ listing.founderCompany ? ` (${escapeMrkdwn(listing.founderCompany)})` : ""
114
+ }`,
115
+ },
116
+ ];
117
+
118
+ return [
119
+ { type: "header", text: { type: "plain_text", text: listing.name ?? ABSENT, emoji: true } },
120
+ { type: "section", fields },
121
+ ];
122
+ }
123
+
124
+ /**
125
+ * Posts the new application to Slack (`chat.postMessage`) with Approve/Reject
126
+ * buttons carrying the listing id as `value`. On success, stamps the
127
+ * returned `channel`/`ts` onto the row so `updateListingMessage` can later
128
+ * target the same message. No-op (logs `slack-skipped`) when Slack env is
129
+ * absent; never throws — a Slack failure (missing config, API error, network
130
+ * error) never fails the create that already succeeded.
131
+ */
132
+ export async function postListingApplication(listing: ProjectListingSlackInfo): Promise<void> {
133
+ const token = config.AILK_SLACK_BOT_TOKEN;
134
+ const channel = config.AILK_SLACK_CHANNEL_ID;
135
+ if (!token || !channel) {
136
+ logger.info({ listingId: listing.id }, "slack-skipped");
137
+ return;
138
+ }
139
+
140
+ try {
141
+ const result = await callSlackApi(token, "chat.postMessage", {
142
+ channel,
143
+ // `text:` is mrkdwn-parsed and is what Slack shows in the notification
144
+ // fallback, so the applicant-authored name is escaped here even though
145
+ // the same value is safe unescaped in the `plain_text` header block
146
+ // (PR #5042 security review, finding 5).
147
+ text: `New project application: ${escapeMrkdwn(listing.name ?? ABSENT)}`,
148
+ blocks: [
149
+ ...infoBlocks(listing),
150
+ {
151
+ type: "actions",
152
+ block_id: "listing_decision",
153
+ elements: [
154
+ {
155
+ type: "button",
156
+ text: { type: "plain_text", text: "Approve", emoji: true },
157
+ style: "primary",
158
+ action_id: "listing_approve",
159
+ value: listing.id,
160
+ },
161
+ {
162
+ type: "button",
163
+ text: { type: "plain_text", text: "Reject", emoji: true },
164
+ style: "danger",
165
+ action_id: "listing_reject",
166
+ value: listing.id,
167
+ },
168
+ ],
169
+ },
170
+ ],
171
+ });
172
+
173
+ if (!result.ok) {
174
+ logger.warn({ listingId: listing.id, slackError: result.error }, "slack post failed");
175
+ return;
176
+ }
177
+
178
+ await prisma.projectListing.update({
179
+ where: { id: listing.id },
180
+ data: { slackChannel: result.channel ?? channel, slackTs: result.ts ?? null },
181
+ });
182
+ } catch (err) {
183
+ logger.warn(
184
+ { listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
185
+ "slack post threw",
186
+ );
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Replaces the posted message's actions block with an outcome line
192
+ * (`chat.update`) — same header + info fields, Approve/Reject buttons gone.
193
+ * No-op (logs `slack-skipped`) when Slack env is absent OR the row never
194
+ * carries `slackChannel`/`slackTs` (the post was skipped or failed); never
195
+ * throws — the decision has already been applied by the time this runs.
196
+ */
197
+ export async function updateListingMessage(
198
+ listing: ProjectListingSlackInfo & { slackChannel: string | null; slackTs: string | null },
199
+ /** The stored decision status (#5033 — `active` is the approved state). */
200
+ outcome: "active" | "rejected",
201
+ decidedByName: string,
202
+ ): Promise<void> {
203
+ const token = config.AILK_SLACK_BOT_TOKEN;
204
+ if (!token || !listing.slackChannel || !listing.slackTs) {
205
+ logger.info({ listingId: listing.id }, "slack-skipped");
206
+ return;
207
+ }
208
+
209
+ // decidedByName is Slack-supplied (payload.user.username/name) — escaped
210
+ // for the same reason applicant text is (see the module doc's security
211
+ // note), even though only a member of the notified channel controls it.
212
+ const safeDecidedByName = escapeMrkdwn(decidedByName);
213
+ // The operator-facing copy stays "Approved" — the vocabulary change is in
214
+ // the stored status, not in what a human reads in the channel.
215
+ const outcomeLine =
216
+ outcome === "active"
217
+ ? `:white_check_mark: Approved by ${safeDecidedByName}`
218
+ : `:x: Rejected by ${safeDecidedByName}`;
219
+
220
+ try {
221
+ const result = await callSlackApi(token, "chat.update", {
222
+ channel: listing.slackChannel,
223
+ ts: listing.slackTs,
224
+ text: `${escapeMrkdwn(listing.name ?? ABSENT)} — ${outcomeLine}`,
225
+ blocks: [
226
+ ...infoBlocks(listing),
227
+ { type: "section", text: { type: "mrkdwn", text: outcomeLine } },
228
+ ],
229
+ });
230
+
231
+ if (!result.ok) {
232
+ logger.warn({ listingId: listing.id, slackError: result.error }, "slack update failed");
233
+ }
234
+ } catch (err) {
235
+ logger.warn(
236
+ { listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
237
+ "slack update threw",
238
+ );
239
+ }
240
+ }
@@ -0,0 +1,451 @@
1
+ // oss: false
2
+ /**
3
+ * Tenant-context service — the multi-tenant module's guard + tenancy assembly
4
+ * (FEAT-042, design decision (a): row-level isolation at ONE data-access
5
+ * boundary + a default-deny scope layer + a guard).
6
+ *
7
+ * This module and `../lib/tenant-db.ts` are the ONLY two files allowed to
8
+ * touch tenancy/domain Prisma models directly (enforced by the
9
+ * `forbidden_pattern` tenant-scope rule in architecture.yaml). Everything
10
+ * here follows the ADR 0009 D3 service-layer convention: cross-namespace
11
+ * assembly (auth-namespace Membership/Workspace/Session ↔ content-namespace
12
+ * AgentSession/Artifact) happens only in `apps/api/src/services/`.
13
+ *
14
+ * The guard model (default-deny — every branch that is not an explicit grant
15
+ * is a denial):
16
+ *
17
+ * 1. `resolveActiveTenant` — validates the caller's OWN session context:
18
+ * cookie session present (bearer tokens carry no workspace binding —
19
+ * #3591), an active workspace set, the workspace row exists, the
20
+ * session's activeOrganizationId matches the workspace's organization,
21
+ * and a Membership(user, org) row exists. Result: a `TenantContext`
22
+ * every downstream domain query is scoped by.
23
+ * 2. Workspace-target routes compare the URL's workspaceId against the
24
+ * resolved context — mismatch is `forbidden` WITHOUT looking the target
25
+ * up (no tenant enumeration).
26
+ * 3. Session/artifact-target routes never load the target unscoped at the
27
+ * route layer; `tenant-db.ts` scopes every lookup by workspaceId, so a
28
+ * foreign target is indistinguishable from a missing one (`not_found`).
29
+ *
30
+ * Service results use the discriminated `{ ok } | { ok: false, error }`
31
+ * convention from `lead-routing.ts`; routes translate kinds to HTTP statuses.
32
+ */
33
+ import {
34
+ DEFAULT_ORG_ID,
35
+ DEFAULT_WORKSPACE_ID,
36
+ OPERATOR_MEMBERSHIP_ID,
37
+ } from "@working-theory/database/tenancy";
38
+ import { isSentinel, type Id } from "@working-theory/ids";
39
+
40
+ import { prisma } from "../lib/prisma.js";
41
+ import type { SessionTenant } from "../middleware/auth.js";
42
+
43
+ export type MembershipRole = "owner" | "admin" | "member";
44
+
45
+ /**
46
+ * The tenant scope every domain query is bound to. Ids carry the ADR-0031
47
+ * brand; values originating from Prisma reads are brand-asserted at the read
48
+ * edge (the DB CHECK constraints are the runtime guarantee).
49
+ */
50
+ export interface TenantContext {
51
+ userId: Id<"user">;
52
+ organizationId: Id<"organization">;
53
+ workspaceId: Id<"workspace">;
54
+ role: MembershipRole;
55
+ }
56
+
57
+ export type TenantErrorKind = "forbidden" | "not_found" | "conflict";
58
+
59
+ export type TenantResult<T> =
60
+ | { ok: true; value: T }
61
+ | { ok: false; error: { kind: TenantErrorKind; message: string } };
62
+
63
+ function deny<T>(kind: TenantErrorKind, message: string): TenantResult<T> {
64
+ return { ok: false, error: { kind, message } };
65
+ }
66
+
67
+ /**
68
+ * TOFU-binds the operator Membership for a single-tenant deployment (spec
69
+ * D1–D4) — the replacement for the deleted user-agnostic `owner` grant. The
70
+ * Membership is created with a fixed, well-known id
71
+ * (`OPERATOR_MEMBERSHIP_ID`); the `Membership.id` primary-key uniqueness —
72
+ * not a read-then-write "is this the first user?" guard — enforces exactly
73
+ * one owner under a concurrent first-login race. The losing caller's
74
+ * `create` raises `P2002`, swallowed here: that caller is then unbound and
75
+ * denied by the `membership.findUnique` the caller of this function runs
76
+ * next.
77
+ *
78
+ * Idempotent + at-most-once: a caller who already holds the Membership (a
79
+ * repeat call, or the deployment's steady state after the first bind) hits
80
+ * the same `P2002` path and is a no-op. Reachable ONLY from
81
+ * `resolveSingleTenantDefault`, itself gated by the
82
+ * `count === 1 && default-workspace-exists` check — unreachable from any
83
+ * multi-tenant state (`count > 1`).
84
+ *
85
+ * When `AILK_OPERATOR_EMAIL` is set (spec D4 — trimmed + lowercased before
86
+ * comparison), the bind is attempted ONLY for the caller whose `User.email`
87
+ * matches it; every other caller returns without writing and is denied
88
+ * downstream. An empty/whitespace-only `AILK_OPERATOR_EMAIL` normalizes to
89
+ * `""`, which is falsy — treated as unset (pure TOFU), so a blank pin can
90
+ * never "match" a blank `User.email`. When unset, the bind is pure TOFU —
91
+ * first caller wins.
92
+ */
93
+ async function bindOperatorMembership(userId: Id<"user">): Promise<void> {
94
+ const pinnedEmail = process.env.AILK_OPERATOR_EMAIL?.trim().toLowerCase();
95
+ if (pinnedEmail) {
96
+ const user = await prisma.user.findUnique({
97
+ where: { id: userId },
98
+ select: { email: true },
99
+ });
100
+ if (!user || user.email.trim().toLowerCase() !== pinnedEmail) {
101
+ return;
102
+ }
103
+ }
104
+ try {
105
+ await prisma.membership.create({
106
+ data: {
107
+ id: OPERATOR_MEMBERSHIP_ID,
108
+ organizationId: DEFAULT_ORG_ID,
109
+ userId,
110
+ role: "owner",
111
+ },
112
+ });
113
+ } catch (error) {
114
+ if (!isUniqueViolation(error)) {
115
+ throw error;
116
+ }
117
+ // Another caller already holds OPERATOR_MEMBERSHIP_ID (the TOFU race
118
+ // winner, or a prior deployment run) — swallow and fall through.
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Resolves the default single-tenant scope when the deployment is
124
+ * unambiguously single-tenant — the default Workspace (`DEFAULT_WORKSPACE_ID`)
125
+ * exists AND it is the ONLY Workspace (`workspace.count() === 1`), the exact
126
+ * mirror of the Phase-1 provision guard's `count === 0` check (spec D2).
127
+ * Returns `null` (never grants) the instant a second Workspace exists, or the
128
+ * sole Workspace is not the well-known default — the caller falls through to
129
+ * the existing multi-tenant default-deny in that case.
130
+ *
131
+ * Converges on the SAME Membership-based authz the multi-tenant path uses
132
+ * (spec D1 — the fix): attempts to TOFU-bind the caller as operator
133
+ * (`bindOperatorMembership`, a no-op once an operator is already bound or
134
+ * the caller doesn't match the optional env pin), then runs the real
135
+ * `membership.findUnique` check. A caller holding no Membership on the
136
+ * default Org — including every caller after the first once TOFU has bound
137
+ * someone else — resolves `null` here, and `resolveActiveTenant` denies.
138
+ * There is no more user-agnostic `owner` grant.
139
+ */
140
+ async function resolveSingleTenantDefault(
141
+ userId: Id<"user">,
142
+ ): Promise<TenantContext | null> {
143
+ const workspaceCount = await prisma.workspace.count();
144
+ if (workspaceCount !== 1) {
145
+ return null;
146
+ }
147
+ const workspace = await prisma.workspace.findUnique({
148
+ where: { id: DEFAULT_WORKSPACE_ID },
149
+ select: { id: true, organizationId: true },
150
+ });
151
+ // The default workspace must hang off the sentinel default Organization
152
+ // (ADR-0031 rule 7): keyed on the reserved-range predicate, not a literal.
153
+ if (!workspace || !isSentinel(workspace.organizationId)) {
154
+ return null;
155
+ }
156
+ await bindOperatorMembership(userId);
157
+ const membership = await prisma.membership.findUnique({
158
+ where: {
159
+ organizationId_userId: {
160
+ organizationId: workspace.organizationId,
161
+ userId,
162
+ },
163
+ },
164
+ select: { role: true },
165
+ });
166
+ if (!membership) {
167
+ return null;
168
+ }
169
+ return {
170
+ userId,
171
+ organizationId: workspace.organizationId as Id<"organization">,
172
+ workspaceId: workspace.id as Id<"workspace">,
173
+ role: membership.role as MembershipRole,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Validate the caller's active tenant context. See the guard model above.
179
+ * `sessionTenant` is undefined for bearer-authenticated requests — denied
180
+ * here (default-deny) until agent tokens carry a workspace binding (#3591).
181
+ */
182
+ export async function resolveActiveTenant(params: {
183
+ userId: Id<"user">;
184
+ sessionTenant: SessionTenant | undefined;
185
+ }): Promise<TenantResult<TenantContext>> {
186
+ const { userId, sessionTenant } = params;
187
+ if (!sessionTenant) {
188
+ return deny("forbidden", "Tenant context requires a cookie session");
189
+ }
190
+ const { activeOrganizationId, activeWorkspaceId } = sessionTenant;
191
+ if (!activeWorkspaceId || !activeOrganizationId) {
192
+ // Single-tenant default resolution (spec D1/D2) — only on the previously
193
+ // unconditional-deny branch, so multi-tenant explicit-binding paths below
194
+ // add zero extra queries. Falls through to the deny below in any
195
+ // multi-tenant state (count > 1, or count === 1 but not the default).
196
+ const singleTenantDefault = await resolveSingleTenantDefault(userId);
197
+ if (singleTenantDefault) {
198
+ return { ok: true, value: singleTenantDefault };
199
+ }
200
+ return deny("forbidden", "No active workspace");
201
+ }
202
+
203
+ const workspace = await prisma.workspace.findUnique({
204
+ where: { id: activeWorkspaceId },
205
+ select: { id: true, organizationId: true },
206
+ });
207
+ if (!workspace) {
208
+ return deny("forbidden", "Active workspace no longer exists");
209
+ }
210
+ if (workspace.organizationId !== activeOrganizationId) {
211
+ return deny("forbidden", "Stale session tenant context");
212
+ }
213
+
214
+ const membership = await prisma.membership.findUnique({
215
+ where: {
216
+ organizationId_userId: {
217
+ organizationId: workspace.organizationId,
218
+ userId,
219
+ },
220
+ },
221
+ select: { role: true },
222
+ });
223
+ if (!membership) {
224
+ return deny("forbidden", "Not a member of the active organization");
225
+ }
226
+
227
+ return {
228
+ ok: true,
229
+ value: {
230
+ userId,
231
+ organizationId: workspace.organizationId as Id<"organization">,
232
+ workspaceId: workspace.id as Id<"workspace">,
233
+ role: membership.role as MembershipRole,
234
+ },
235
+ };
236
+ }
237
+
238
+ /** The switcher payload: every org the user belongs to, with its workspaces. */
239
+ export interface TenancyListing {
240
+ activeOrganizationId: Id<"organization"> | null;
241
+ activeWorkspaceId: Id<"workspace"> | null;
242
+ organizations: Array<{
243
+ id: Id<"organization">;
244
+ name: string;
245
+ slug: string;
246
+ role: MembershipRole;
247
+ workspaces: Array<{ id: Id<"workspace">; name: string; slug: string }>;
248
+ }>;
249
+ }
250
+
251
+ /**
252
+ * List the caller's organizations + workspaces (WorkspaceSwitcher hydration).
253
+ * Membership-scoped by construction: starts from Membership rows, so no org
254
+ * outside the caller's memberships can appear.
255
+ */
256
+ export async function listTenancy(params: {
257
+ userId: Id<"user">;
258
+ sessionTenant: SessionTenant | undefined;
259
+ }): Promise<TenantResult<TenancyListing>> {
260
+ const memberships = await prisma.membership.findMany({
261
+ where: { userId: params.userId },
262
+ select: {
263
+ role: true,
264
+ organization: {
265
+ select: {
266
+ id: true,
267
+ name: true,
268
+ slug: true,
269
+ workspaces: {
270
+ select: { id: true, name: true, slug: true },
271
+ orderBy: { createdAt: "asc" },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ orderBy: { createdAt: "asc" },
277
+ });
278
+
279
+ return {
280
+ ok: true,
281
+ value: {
282
+ activeOrganizationId: params.sessionTenant?.activeOrganizationId ?? null,
283
+ activeWorkspaceId: params.sessionTenant?.activeWorkspaceId ?? null,
284
+ organizations: memberships.map((m) => ({
285
+ id: m.organization.id as Id<"organization">,
286
+ name: m.organization.name,
287
+ slug: m.organization.slug,
288
+ role: m.role as MembershipRole,
289
+ workspaces: m.organization.workspaces as Array<{
290
+ id: Id<"workspace">;
291
+ name: string;
292
+ slug: string;
293
+ }>,
294
+ })),
295
+ },
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Switch the caller's active workspace (and, implicitly, organization).
301
+ * Grant requires the target workspace to exist AND the caller to hold a
302
+ * Membership in its organization — both failures return `not_found` so a
303
+ * non-member cannot distinguish "exists" from "doesn't" (no enumeration).
304
+ * The update is server-side on the Session row (the thin activeWorkspaceId
305
+ * session extension the apps/workspace auth config declares).
306
+ */
307
+ export async function activateWorkspace(params: {
308
+ userId: Id<"user">;
309
+ sessionTenant: SessionTenant | undefined;
310
+ workspaceId: Id<"workspace">;
311
+ }): Promise<TenantResult<TenantContext>> {
312
+ const { userId, sessionTenant, workspaceId } = params;
313
+ if (!sessionTenant) {
314
+ return deny("forbidden", "Tenant context requires a cookie session");
315
+ }
316
+
317
+ const workspace = await prisma.workspace.findUnique({
318
+ where: { id: workspaceId },
319
+ select: { id: true, organizationId: true },
320
+ });
321
+ if (!workspace) {
322
+ return deny("not_found", "Workspace not found");
323
+ }
324
+
325
+ const membership = await prisma.membership.findUnique({
326
+ where: {
327
+ organizationId_userId: {
328
+ organizationId: workspace.organizationId,
329
+ userId,
330
+ },
331
+ },
332
+ select: { role: true },
333
+ });
334
+ if (!membership) {
335
+ return deny("not_found", "Workspace not found");
336
+ }
337
+
338
+ await prisma.session.update({
339
+ where: { id: sessionTenant.sessionRowId },
340
+ data: {
341
+ activeWorkspaceId: workspace.id,
342
+ activeOrganizationId: workspace.organizationId,
343
+ },
344
+ });
345
+
346
+ return {
347
+ ok: true,
348
+ value: {
349
+ userId,
350
+ organizationId: workspace.organizationId as Id<"organization">,
351
+ workspaceId: workspace.id as Id<"workspace">,
352
+ role: membership.role as MembershipRole,
353
+ },
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Create a workspace in an organization. Role floor: admin (owner > admin).
359
+ * Non-members get `not_found` (no org enumeration); members below the floor
360
+ * get `forbidden`.
361
+ */
362
+ export async function createWorkspace(params: {
363
+ userId: Id<"user">;
364
+ organizationId: Id<"organization">;
365
+ name: string;
366
+ }): Promise<
367
+ TenantResult<{
368
+ id: Id<"workspace">;
369
+ name: string;
370
+ slug: string;
371
+ organizationId: Id<"organization">;
372
+ }>
373
+ > {
374
+ const { userId, organizationId, name } = params;
375
+
376
+ const membership = await prisma.membership.findUnique({
377
+ where: { organizationId_userId: { organizationId, userId } },
378
+ select: { role: true },
379
+ });
380
+ if (!membership) {
381
+ return deny("not_found", "Organization not found");
382
+ }
383
+ if (membership.role !== "owner" && membership.role !== "admin") {
384
+ return deny("forbidden", "Creating a workspace requires the admin role");
385
+ }
386
+
387
+ const slug = slugify(name);
388
+ try {
389
+ const workspace = await prisma.workspace.create({
390
+ data: { organizationId, name, slug },
391
+ select: { id: true, name: true, slug: true, organizationId: true },
392
+ });
393
+ return {
394
+ ok: true,
395
+ value: {
396
+ ...workspace,
397
+ id: workspace.id as Id<"workspace">,
398
+ organizationId: workspace.organizationId as Id<"organization">,
399
+ },
400
+ };
401
+ } catch (error) {
402
+ if (isUniqueViolation(error)) {
403
+ return deny(
404
+ "conflict",
405
+ "A workspace with this name already exists in the organization",
406
+ );
407
+ }
408
+ throw error;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Delete-cascade for a workspace's site config — the service-layer replacement
414
+ * for the dropped cross-namespace FK (ADR-0009 D3 / #3634 D3). Removes the
415
+ * workspace's `WorkspaceTheme` / `WorkspaceSiteIdentity` / `WorkspaceSEO` rows,
416
+ * each scoped by `workspaceId` (the `@unique` key), so it can never touch
417
+ * another workspace's rows.
418
+ *
419
+ * OPTION 1 (Sam-disposed, #3634): this is a HELPER ONLY — no DELETE route,
420
+ * caller, or invocation exists in this phase. The destructive owner-gated
421
+ * entry-point that will call it is a separate, parked, Sam-reviewed issue. It
422
+ * takes a bare `workspaceId` (not a `TenantContext`) because that future caller
423
+ * has already authorized the delete; scope-safety is the caller's contract.
424
+ *
425
+ * Lives here (a `tenant-scope`-excluded file) so the raw `prisma.workspace*`
426
+ * calls pass the architecture lint.
427
+ */
428
+ export async function deleteWorkspaceConfig(workspaceId: string): Promise<void> {
429
+ await prisma.workspaceTheme.deleteMany({ where: { workspaceId } });
430
+ await prisma.workspaceSiteIdentity.deleteMany({ where: { workspaceId } });
431
+ await prisma.workspaceSEO.deleteMany({ where: { workspaceId } });
432
+ }
433
+
434
+ function slugify(name: string): string {
435
+ return (
436
+ name
437
+ .toLowerCase()
438
+ .replace(/[^a-z0-9]+/g, "-")
439
+ .replace(/^-+|-+$/g, "")
440
+ .slice(0, 48) || "workspace"
441
+ );
442
+ }
443
+
444
+ function isUniqueViolation(error: unknown): boolean {
445
+ return (
446
+ typeof error === "object" &&
447
+ error !== null &&
448
+ "code" in error &&
449
+ (error as { code?: unknown }).code === "P2002"
450
+ );
451
+ }