create-ailk 0.2.0 → 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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -1,125 +0,0 @@
1
- // oss: false
2
- /**
3
- * Workspace site-config routes (multi-tenant module — FEAT-042; STC-P4 #3634).
4
- *
5
- * The write surface an end-tenant uses to theme THEIR OWN workspace at runtime:
6
- *
7
- * GET /v1/workspaces/:workspaceId/theme · PUT /v1/workspaces/:workspaceId/theme
8
- * GET /v1/workspaces/:workspaceId/identity · PUT /v1/workspaces/:workspaceId/identity
9
- * GET /v1/workspaces/:workspaceId/seo · PUT /v1/workspaces/:workspaceId/seo
10
- *
11
- * All tenant-guarded (tenantPreHandlerPlugin runs first): the cross-workspace
12
- * 403 is enforced by the guard's `params.workspaceId ≠ resolved` check BEFORE
13
- * any handler runs — no config query executes for a foreign workspace. The
14
- * scope is ALWAYS `request.tenant.workspaceId` (server-resolved from the
15
- * validated session); no handler reads a scope from the body/query. Each config
16
- * model is `workspaceId @unique`, so the `tenantDb` accessor's
17
- * `findUnique`/`upsert` on `{ workspaceId }` is inherently tenant-scoped.
18
- *
19
- * PUT validates the body against the shared @working-theory/site-config schema
20
- * (400 on failure) and stores the validated blob in the `data` Json column. GET
21
- * returns the stored blob, or `{ ok: true, data: null }` for a not-yet-configured
22
- * workspace (a valid runtime state — the render/seed default is Phase 5).
23
- *
24
- * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
25
- * `{ ok: false, error: { code, message } }`, real HTTP status codes.
26
- *
27
- * MCP parity note: identity-bound tenant routes; the OSS MCP surface is
28
- * identity-blind (apps/mcp/CLAUDE.md), so — like the sibling /v1/workspaces
29
- * routes — they have no anonymous MCP twin. The tenant-scoped authed MCP
30
- * surface is tracked in #3591.
31
- */
32
- import { zWorkspaceId } from "@working-theory/ids";
33
- import {
34
- SEOSchema,
35
- SiteIdentitySchema,
36
- ThemeSchema,
37
- } from "@working-theory/site-config/schema";
38
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
39
- import type { ZodType } from "zod";
40
- import { z } from "zod";
41
-
42
- import { tenantDb } from "../../lib/tenant-db.js";
43
- import type { TenantDb } from "../../lib/tenant-db.js";
44
-
45
- function unauthorized(reply: FastifyReply) {
46
- return reply.status(401).send({
47
- ok: false,
48
- error: { code: "unauthorized", message: "Authentication required" },
49
- });
50
- }
51
-
52
- function invalidId(reply: FastifyReply) {
53
- return reply.status(400).send({
54
- ok: false,
55
- error: { code: "validation_failed", message: "Invalid workspace id" },
56
- });
57
- }
58
-
59
- // Boundary id validation (ADR-0031 rule 5): the :workspaceId param must be a
60
- // well-formed workspace id before any handler logic runs.
61
- const workspaceParamsSchema = z.object({ workspaceId: zWorkspaceId });
62
-
63
- /**
64
- * The three config types share one GET/PUT shape — the only per-type differences
65
- * are the URL segment, the validation schema, and which `tenantDb` accessor to
66
- * reach. `accessor` names a config accessor on `TenantDb` (`get` + `upsert`).
67
- */
68
- type ConfigAccessorKey = "theme" | "siteIdentity" | "seo";
69
-
70
- interface ConfigType {
71
- segment: string;
72
- schema: ZodType;
73
- accessor: ConfigAccessorKey;
74
- }
75
-
76
- const CONFIG_TYPES: ConfigType[] = [
77
- { segment: "theme", schema: ThemeSchema, accessor: "theme" },
78
- { segment: "identity", schema: SiteIdentitySchema, accessor: "siteIdentity" },
79
- { segment: "seo", schema: SEOSchema, accessor: "seo" },
80
- ];
81
-
82
- export const workspaceConfigRoutes: FastifyPluginAsync = async (fastify) => {
83
- for (const { segment, schema, accessor } of CONFIG_TYPES) {
84
- fastify.get<{ Params: { workspaceId: string } }>(
85
- `/v1/workspaces/:workspaceId/${segment}`,
86
- async (request, reply) => {
87
- const tenant = request.tenant;
88
- if (!tenant) return unauthorized(reply);
89
-
90
- const params = workspaceParamsSchema.safeParse(request.params);
91
- if (!params.success) return invalidId(reply);
92
-
93
- const db: TenantDb = tenantDb(tenant);
94
- const row = await db[accessor].get();
95
- // A not-yet-configured workspace is a valid state, not an error (D6).
96
- return reply.send({ ok: true, data: row?.data ?? null });
97
- },
98
- );
99
-
100
- fastify.put<{ Params: { workspaceId: string } }>(
101
- `/v1/workspaces/:workspaceId/${segment}`,
102
- async (request, reply) => {
103
- const tenant = request.tenant;
104
- if (!tenant) return unauthorized(reply);
105
-
106
- const params = workspaceParamsSchema.safeParse(request.params);
107
- if (!params.success) return invalidId(reply);
108
-
109
- // The ONLY untrusted input: validate against the shared schema. The
110
- // scope is NEVER read from the body — it is tenant.workspaceId.
111
- const parsed = schema.safeParse(request.body);
112
- if (!parsed.success) {
113
- return reply.status(400).send({
114
- ok: false,
115
- error: { code: "validation_failed", message: "Invalid config" },
116
- });
117
- }
118
-
119
- const db: TenantDb = tenantDb(tenant);
120
- const row = await db[accessor].upsert(parsed.data);
121
- return reply.send({ ok: true, data: row.data });
122
- },
123
- );
124
- }
125
- };
@@ -1,194 +0,0 @@
1
- // oss: false
2
- /**
3
- * Workspace routes (multi-tenant module — FEAT-042).
4
- *
5
- * Two registration scopes (see server.ts):
6
- *
7
- * AUTH-ONLY (no tenant guard — they establish/inspect tenant context):
8
- * GET /v1/workspaces — switcher listing (orgs + workspaces)
9
- * POST /v1/workspaces — create workspace (role admin+)
10
- * POST /v1/workspaces/:workspaceId/activate — switch active workspace
11
- *
12
- * TENANT-GUARDED (tenantPreHandlerPlugin runs first; request.tenant set):
13
- * GET /v1/workspaces/:workspaceId/sessions — list agent sessions (+unread)
14
- * POST /v1/workspaces/:workspaceId/sessions — create agent session
15
- *
16
- * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
17
- * `{ ok: false, error: { code, message } }`, real HTTP status codes.
18
- *
19
- * MCP parity note: these are identity-bound tenant routes; the OSS MCP
20
- * surface is identity-blind (apps/mcp/CLAUDE.md), so — like the existing
21
- * /v1/auth/tokens and /v1/user routes — they have no anonymous MCP twin.
22
- * The tenant-scoped authed MCP surface is tracked in #3591.
23
- */
24
- import { zOrganizationId, zWorkspaceId } from "@working-theory/ids";
25
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
26
- import { z } from "zod";
27
-
28
- import { tenantDb } from "../../lib/tenant-db.js";
29
- import {
30
- activateWorkspace,
31
- createWorkspace,
32
- listTenancy,
33
- type TenantErrorKind,
34
- } from "../../services/tenant-context.js";
35
-
36
- const TENANT_ERROR_STATUS: Record<TenantErrorKind, number> = {
37
- forbidden: 403,
38
- not_found: 404,
39
- conflict: 409,
40
- };
41
-
42
- function sendTenantError(
43
- reply: FastifyReply,
44
- error: { kind: TenantErrorKind; message: string },
45
- ) {
46
- return reply.status(TENANT_ERROR_STATUS[error.kind]).send({
47
- ok: false,
48
- error: { code: error.kind, message: error.message },
49
- });
50
- }
51
-
52
- function unauthorized(reply: FastifyReply) {
53
- return reply.status(401).send({
54
- ok: false,
55
- error: { code: "unauthorized", message: "Authentication required" },
56
- });
57
- }
58
-
59
- const createWorkspaceSchema = z.object({
60
- // Boundary id validation (ADR-0031 rule 5): the entity-typed Zod schema,
61
- // not z.string().min(1) — a user_/wksp_ id passed here is a 400, not a query.
62
- organizationId: zOrganizationId,
63
- name: z.string().min(1).max(80),
64
- });
65
-
66
- const workspaceParamsSchema = z.object({ workspaceId: zWorkspaceId });
67
-
68
- const createSessionSchema = z.object({
69
- title: z.string().min(1).max(200).optional(),
70
- });
71
-
72
- const listSessionsQuerySchema = z.object({
73
- limit: z.coerce.number().int().min(1).max(100).default(50),
74
- });
75
-
76
- /** Auth-only workspace routes — establish/inspect tenant context. */
77
- export const workspaceContextRoutes: FastifyPluginAsync = async (fastify) => {
78
- fastify.get("/v1/workspaces", async (request, reply) => {
79
- const userId = request.userId;
80
- if (!userId) return unauthorized(reply);
81
-
82
- const result = await listTenancy({
83
- userId,
84
- sessionTenant: request.sessionTenant,
85
- });
86
- if (!result.ok) return sendTenantError(reply, result.error);
87
- return reply.send({ ok: true, data: result.value });
88
- });
89
-
90
- fastify.post("/v1/workspaces", async (request, reply) => {
91
- const userId = request.userId;
92
- if (!userId) return unauthorized(reply);
93
-
94
- const parsed = createWorkspaceSchema.safeParse(request.body);
95
- if (!parsed.success) {
96
- return reply.status(400).send({
97
- ok: false,
98
- error: { code: "validation_failed", message: "Invalid workspace input" },
99
- });
100
- }
101
-
102
- const result = await createWorkspace({ userId, ...parsed.data });
103
- if (!result.ok) return sendTenantError(reply, result.error);
104
- return reply.status(201).send({ ok: true, data: result.value });
105
- });
106
-
107
- fastify.post<{ Params: { workspaceId: string } }>(
108
- "/v1/workspaces/:workspaceId/activate",
109
- async (request, reply) => {
110
- const userId = request.userId;
111
- if (!userId) return unauthorized(reply);
112
-
113
- const params = workspaceParamsSchema.safeParse(request.params);
114
- if (!params.success) {
115
- return reply.status(400).send({
116
- ok: false,
117
- error: { code: "validation_failed", message: "Invalid workspace id" },
118
- });
119
- }
120
-
121
- const result = await activateWorkspace({
122
- userId,
123
- sessionTenant: request.sessionTenant,
124
- workspaceId: params.data.workspaceId,
125
- });
126
- if (!result.ok) return sendTenantError(reply, result.error);
127
- return reply.send({ ok: true, data: result.value });
128
- },
129
- );
130
- };
131
-
132
- /** Tenant-guarded, workspace-scoped domain routes. */
133
- export const workspaceSessionRoutes: FastifyPluginAsync = async (fastify) => {
134
- fastify.get<{ Params: { workspaceId: string } }>(
135
- "/v1/workspaces/:workspaceId/sessions",
136
- async (request, reply) => {
137
- const tenant = request.tenant;
138
- if (!tenant) return unauthorized(reply);
139
-
140
- const query = listSessionsQuerySchema.safeParse(request.query ?? {});
141
- if (!query.success) {
142
- return reply.status(400).send({
143
- ok: false,
144
- error: { code: "validation_failed", message: "Invalid query" },
145
- });
146
- }
147
-
148
- const db = tenantDb(tenant);
149
- const [sessions, receipts] = await Promise.all([
150
- db.agentSession.list(query.data.limit),
151
- db.readReceipt.listForUser(),
152
- ]);
153
- const readBySession = new Map(
154
- receipts.map((r) => [r.sessionId, r.lastReadSeq]),
155
- );
156
-
157
- return reply.send({
158
- ok: true,
159
- data: {
160
- sessions: sessions.map((s) => ({
161
- id: s.id,
162
- title: s.title,
163
- status: s.status,
164
- lastMessageAt: s.lastMessageAt,
165
- isWorking: s.status === "working",
166
- hasUnread: s.lastSeq > (readBySession.get(s.id) ?? 0),
167
- createdAt: s.createdAt,
168
- })),
169
- },
170
- });
171
- },
172
- );
173
-
174
- fastify.post<{ Params: { workspaceId: string } }>(
175
- "/v1/workspaces/:workspaceId/sessions",
176
- async (request, reply) => {
177
- const tenant = request.tenant;
178
- if (!tenant) return unauthorized(reply);
179
-
180
- const parsed = createSessionSchema.safeParse(request.body ?? {});
181
- if (!parsed.success) {
182
- return reply.status(400).send({
183
- ok: false,
184
- error: { code: "validation_failed", message: "Invalid session input" },
185
- });
186
- }
187
-
188
- const session = await tenantDb(tenant).agentSession.create({
189
- title: parsed.data.title ?? null,
190
- });
191
- return reply.status(201).send({ ok: true, data: session });
192
- },
193
- );
194
- };
@@ -1,218 +0,0 @@
1
- // oss: false
2
- /**
3
- * Session-target routes (multi-tenant module — FEAT-042; transport contract
4
- * from docs/research/ploy-capture/BACKEND-REQUIREMENTS.md §1/§3).
5
- *
6
- * All tenant-guarded (tenantPreHandlerPlugin): request.tenant is the caller's
7
- * validated active workspace. Targets are session/artifact ids; every lookup
8
- * goes through tenantDb's workspace-scoped `findFirst({ id, workspaceId })`,
9
- * so a foreign-tenant target 404s exactly like a missing one — cross-org
10
- * reads AND writes are denied without revealing existence.
11
- *
12
- * GET /v1/sessions/:sessionId/conversation-events?sinceSeq=N&limit=M
13
- * → { events[], lastSeq } (R-T4 replay)
14
- * POST /v1/sessions/:sessionId/mark-read (R-T7)
15
- * GET /v1/sessions/:sessionId/ws-token (R-T3 short-lived token)
16
- * GET /v1/sessions/:sessionId/artifacts (canvas hydration)
17
- * GET /v1/artifacts/:artifactId/versions (VersionHistory hydration)
18
- *
19
- * The realtime push channel itself (WS server consuming these tokens) is
20
- * Phase-6 scope; this run supplies the replayable, workspace-scoped data
21
- * plane the frame reads.
22
- */
23
- import { zAgentSessionId, zArtifactId } from "@working-theory/ids";
24
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
25
- import { z } from "zod";
26
-
27
- import { tenantDb } from "../../lib/tenant-db.js";
28
- import { mintWsToken } from "../../lib/ws-token.js";
29
-
30
- function notFound(reply: FastifyReply) {
31
- return reply.status(404).send({
32
- ok: false,
33
- error: { code: "not_found", message: "Not found" },
34
- });
35
- }
36
-
37
- function invalidId(reply: FastifyReply) {
38
- return reply.status(400).send({
39
- ok: false,
40
- error: { code: "validation_failed", message: "Invalid id" },
41
- });
42
- }
43
-
44
- // Boundary id validation (ADR-0031 rule 5): target params must be well-formed
45
- // entity ids (agsess_/artif_ prefixed uuids) before any lookup runs.
46
- const sessionParamsSchema = z.object({ sessionId: zAgentSessionId });
47
- const artifactParamsSchema = z.object({ artifactId: zArtifactId });
48
-
49
- function unauthorized(reply: FastifyReply) {
50
- return reply.status(401).send({
51
- ok: false,
52
- error: { code: "unauthorized", message: "Authentication required" },
53
- });
54
- }
55
-
56
- const eventsQuerySchema = z.object({
57
- sinceSeq: z.coerce.number().int().min(0).default(0),
58
- limit: z.coerce.number().int().min(1).max(500).default(200),
59
- });
60
-
61
- const markReadSchema = z.object({
62
- seq: z.coerce.number().int().min(0).optional(),
63
- });
64
-
65
- export const sessionRoutes: FastifyPluginAsync = async (fastify) => {
66
- fastify.get<{ Params: { sessionId: string } }>(
67
- "/v1/sessions/:sessionId/conversation-events",
68
- async (request, reply) => {
69
- const tenant = request.tenant;
70
- if (!tenant) return unauthorized(reply);
71
-
72
- const query = eventsQuerySchema.safeParse(request.query ?? {});
73
- if (!query.success) {
74
- return reply.status(400).send({
75
- ok: false,
76
- error: { code: "validation_failed", message: "Invalid query" },
77
- });
78
- }
79
-
80
- const params = sessionParamsSchema.safeParse(request.params);
81
- if (!params.success) return invalidId(reply);
82
-
83
- const db = tenantDb(tenant);
84
- const session = await db.agentSession.byId(params.data.sessionId);
85
- if (!session) return notFound(reply);
86
-
87
- const events = await db.conversationEvent.listSince(
88
- params.data.sessionId,
89
- query.data.sinceSeq,
90
- query.data.limit,
91
- );
92
-
93
- return reply.send({
94
- ok: true,
95
- data: {
96
- events: events.map((e) => ({
97
- agentId: e.agentId,
98
- seq: e.seq,
99
- type: e.type,
100
- turnId: e.turnId,
101
- payload: e.payload,
102
- createdAt: e.createdAt,
103
- })),
104
- lastSeq: session.lastSeq,
105
- },
106
- });
107
- },
108
- );
109
-
110
- fastify.post<{ Params: { sessionId: string } }>(
111
- "/v1/sessions/:sessionId/mark-read",
112
- async (request, reply) => {
113
- const tenant = request.tenant;
114
- if (!tenant) return unauthorized(reply);
115
-
116
- const parsed = markReadSchema.safeParse(request.body ?? {});
117
- if (!parsed.success) {
118
- return reply.status(400).send({
119
- ok: false,
120
- error: { code: "validation_failed", message: "Invalid body" },
121
- });
122
- }
123
-
124
- const params = sessionParamsSchema.safeParse(request.params);
125
- if (!params.success) return invalidId(reply);
126
-
127
- const db = tenantDb(tenant);
128
- // Scoped resolve FIRST — the upsert below must never run for a session
129
- // outside the tenant (a receipt row would attach to a foreign session).
130
- const session = await db.agentSession.byId(params.data.sessionId);
131
- if (!session) return notFound(reply);
132
-
133
- const lastReadSeq = parsed.data.seq ?? session.lastSeq;
134
- await db.readReceipt.upsert(params.data.sessionId, lastReadSeq);
135
- return reply.send({ ok: true, data: { sessionId: session.id, lastReadSeq } });
136
- },
137
- );
138
-
139
- fastify.get<{ Params: { sessionId: string } }>(
140
- "/v1/sessions/:sessionId/ws-token",
141
- async (request, reply) => {
142
- const tenant = request.tenant;
143
- if (!tenant) return unauthorized(reply);
144
-
145
- const params = sessionParamsSchema.safeParse(request.params);
146
- if (!params.success) return invalidId(reply);
147
-
148
- const session = await tenantDb(tenant).agentSession.byId(
149
- params.data.sessionId,
150
- );
151
- if (!session) return notFound(reply);
152
-
153
- const minted = mintWsToken({
154
- userId: tenant.userId,
155
- sessionId: params.data.sessionId,
156
- workspaceId: tenant.workspaceId,
157
- });
158
- return reply.send({ ok: true, data: minted });
159
- },
160
- );
161
-
162
- fastify.get<{ Params: { sessionId: string } }>(
163
- "/v1/sessions/:sessionId/artifacts",
164
- async (request, reply) => {
165
- const tenant = request.tenant;
166
- if (!tenant) return unauthorized(reply);
167
-
168
- const params = sessionParamsSchema.safeParse(request.params);
169
- if (!params.success) return invalidId(reply);
170
-
171
- const db = tenantDb(tenant);
172
- const session = await db.agentSession.byId(params.data.sessionId);
173
- if (!session) return notFound(reply);
174
-
175
- const artifacts = await db.artifact.listBySession(params.data.sessionId);
176
- return reply.send({
177
- ok: true,
178
- data: {
179
- artifacts: artifacts.map((a) => ({
180
- id: a.id,
181
- type: a.type,
182
- title: a.title,
183
- updatedAt: a.updatedAt,
184
- })),
185
- },
186
- });
187
- },
188
- );
189
-
190
- fastify.get<{ Params: { artifactId: string } }>(
191
- "/v1/artifacts/:artifactId/versions",
192
- async (request, reply) => {
193
- const tenant = request.tenant;
194
- if (!tenant) return unauthorized(reply);
195
-
196
- const params = artifactParamsSchema.safeParse(request.params);
197
- if (!params.success) return invalidId(reply);
198
-
199
- const db = tenantDb(tenant);
200
- const artifact = await db.artifact.byId(params.data.artifactId);
201
- if (!artifact) return notFound(reply);
202
-
203
- const versions = await db.artifactVersion.listByArtifact(
204
- params.data.artifactId,
205
- );
206
- return reply.send({
207
- ok: true,
208
- data: {
209
- versions: versions.map((v) => ({
210
- id: v.id,
211
- savedAt: v.savedAt,
212
- isCurrent: v.isCurrent,
213
- })),
214
- },
215
- });
216
- },
217
- );
218
- };