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,225 +0,0 @@
1
- // oss: false
2
- /**
3
- * Tenant-scoped data-access boundary (the multi-tenant module's scope layer —
4
- * FEAT-042, design decision (a)).
5
- *
6
- * Every domain-content query (agent_sessions, conversation_events,
7
- * session_read_receipts, artifacts, artifact_versions) goes through
8
- * `tenantDb(tenant)`, which injects the resolved `TenantContext.workspaceId`
9
- * into EVERY where/data clause. There is deliberately no method that accepts
10
- * a caller-supplied workspaceId, and lookups by id are `findFirst({ id,
11
- * workspaceId })` — never `findUnique({ id })` — so a foreign-tenant row is
12
- * indistinguishable from a missing one.
13
- *
14
- * Raw `prisma.<domainModel>` access outside this file and
15
- * `../services/tenant-context.ts` is forbidden by the tenant-scope
16
- * `forbidden_pattern` in architecture.yaml (CI: `pnpm architecture`).
17
- */
18
- import type {
19
- AgentSession,
20
- Artifact,
21
- ConversationEvent,
22
- Prisma,
23
- SessionReadReceipt,
24
- WorkspaceSEO,
25
- WorkspaceSiteIdentity,
26
- WorkspaceTheme,
27
- } from "@working-theory/database";
28
- import type { Id } from "@working-theory/ids";
29
-
30
- import type { TenantContext } from "../services/tenant-context.js";
31
-
32
- import { prisma } from "./prisma.js";
33
-
34
- /** Explicit surface (declaration-emit portability + a reviewable contract). */
35
- export interface TenantDb {
36
- agentSession: {
37
- list(limit: number): Promise<AgentSession[]>;
38
- byId(id: Id<"agentSession">): Promise<AgentSession | null>;
39
- create(data: { title?: string | null }): Promise<AgentSession>;
40
- updateDraft(
41
- id: Id<"agentSession">,
42
- draftMessage: string | null,
43
- ): Promise<{ count: number }>;
44
- };
45
- conversationEvent: {
46
- listSince(
47
- sessionId: Id<"agentSession">,
48
- sinceSeq: number,
49
- limit: number,
50
- ): Promise<ConversationEvent[]>;
51
- };
52
- readReceipt: {
53
- listForUser(): Promise<Array<Pick<SessionReadReceipt, "sessionId" | "lastReadSeq">>>;
54
- upsert(
55
- sessionId: Id<"agentSession">,
56
- lastReadSeq: number,
57
- ): Promise<SessionReadReceipt>;
58
- };
59
- artifact: {
60
- listBySession(sessionId: Id<"agentSession">): Promise<Artifact[]>;
61
- byId(id: Id<"artifact">): Promise<Artifact | null>;
62
- };
63
- artifactVersion: {
64
- listByArtifact(
65
- artifactId: Id<"artifact">,
66
- ): Promise<
67
- Array<{ id: Id<"artifactVersion">; savedAt: Date; isCurrent: boolean }>
68
- >;
69
- };
70
- // Per-workspace site config (#3634). Each model is `workspaceId @unique`, so
71
- // the unique key IS the tenant scope — `findUnique`/`upsert` on
72
- // `{ workspaceId }` never accepts a caller-supplied scope.
73
- theme: {
74
- get(): Promise<WorkspaceTheme | null>;
75
- upsert(data: unknown): Promise<WorkspaceTheme>;
76
- };
77
- siteIdentity: {
78
- get(): Promise<WorkspaceSiteIdentity | null>;
79
- upsert(data: unknown): Promise<WorkspaceSiteIdentity>;
80
- };
81
- seo: {
82
- get(): Promise<WorkspaceSEO | null>;
83
- upsert(data: unknown): Promise<WorkspaceSEO>;
84
- };
85
- }
86
-
87
- export function tenantDb(tenant: TenantContext): TenantDb {
88
- const { workspaceId, userId } = tenant;
89
-
90
- return {
91
- agentSession: {
92
- list(limit: number) {
93
- return prisma.agentSession.findMany({
94
- where: { workspaceId },
95
- orderBy: [
96
- { lastMessageAt: { sort: "desc", nulls: "last" } },
97
- { createdAt: "desc" },
98
- ],
99
- take: limit,
100
- });
101
- },
102
- byId(id: string) {
103
- return prisma.agentSession.findFirst({
104
- where: { id, workspaceId },
105
- });
106
- },
107
- create(data: { title?: string | null; agentId?: string }) {
108
- return prisma.agentSession.create({
109
- data: { title: data.title ?? null, workspaceId },
110
- });
111
- },
112
- updateDraft(id: string, draftMessage: string | null) {
113
- return prisma.agentSession.updateMany({
114
- where: { id, workspaceId },
115
- data: { draftMessage },
116
- });
117
- },
118
- },
119
-
120
- conversationEvent: {
121
- listSince(sessionId: string, sinceSeq: number, limit: number) {
122
- return prisma.conversationEvent.findMany({
123
- where: { sessionId, workspaceId, seq: { gt: sinceSeq } },
124
- orderBy: { seq: "asc" },
125
- take: limit,
126
- });
127
- },
128
- },
129
-
130
- readReceipt: {
131
- /** Read positions for the CALLING user across this workspace's sessions. */
132
- listForUser() {
133
- return prisma.sessionReadReceipt.findMany({
134
- where: { workspaceId, userId },
135
- select: { sessionId: true, lastReadSeq: true },
136
- });
137
- },
138
- upsert(sessionId: string, lastReadSeq: number) {
139
- return prisma.sessionReadReceipt.upsert({
140
- where: { sessionId_userId: { sessionId, userId } },
141
- create: { sessionId, workspaceId, userId, lastReadSeq },
142
- update: { lastReadSeq },
143
- });
144
- },
145
- },
146
-
147
- artifact: {
148
- listBySession(sessionId: string) {
149
- return prisma.artifact.findMany({
150
- where: { sessionId, workspaceId },
151
- orderBy: { createdAt: "asc" },
152
- });
153
- },
154
- byId(id: string) {
155
- return prisma.artifact.findFirst({ where: { id, workspaceId } });
156
- },
157
- },
158
-
159
- artifactVersion: {
160
- listByArtifact(artifactId: string) {
161
- // Brand assertion at the Prisma read edge (the DB CHECK constraints
162
- // guarantee the id shape).
163
- return prisma.artifactVersion.findMany({
164
- where: { artifactId, workspaceId },
165
- orderBy: { savedAt: "desc" },
166
- select: {
167
- id: true,
168
- savedAt: true,
169
- isCurrent: true,
170
- },
171
- }) as Promise<
172
- Array<{ id: Id<"artifactVersion">; savedAt: Date; isCurrent: boolean }>
173
- >;
174
- },
175
- },
176
-
177
- // Site config — scoped by the `@unique` workspaceId (the unique key IS the
178
- // scope). The validated blob is stored in the `data` Json column; validation
179
- // happens at the route boundary against the shared @working-theory/site-config
180
- // schema, so the DB layer stores an already-validated object.
181
- theme: {
182
- get() {
183
- return prisma.workspaceTheme.findUnique({ where: { workspaceId } });
184
- },
185
- upsert(data: unknown) {
186
- const json = data as Prisma.InputJsonValue;
187
- return prisma.workspaceTheme.upsert({
188
- where: { workspaceId },
189
- create: { workspaceId, data: json },
190
- update: { data: json },
191
- });
192
- },
193
- },
194
-
195
- siteIdentity: {
196
- get() {
197
- return prisma.workspaceSiteIdentity.findUnique({
198
- where: { workspaceId },
199
- });
200
- },
201
- upsert(data: unknown) {
202
- const json = data as Prisma.InputJsonValue;
203
- return prisma.workspaceSiteIdentity.upsert({
204
- where: { workspaceId },
205
- create: { workspaceId, data: json },
206
- update: { data: json },
207
- });
208
- },
209
- },
210
-
211
- seo: {
212
- get() {
213
- return prisma.workspaceSEO.findUnique({ where: { workspaceId } });
214
- },
215
- upsert(data: unknown) {
216
- const json = data as Prisma.InputJsonValue;
217
- return prisma.workspaceSEO.upsert({
218
- where: { workspaceId },
219
- create: { workspaceId, data: json },
220
- update: { data: json },
221
- });
222
- },
223
- },
224
- };
225
- }
@@ -1,91 +0,0 @@
1
- // oss: false
2
- /**
3
- * Short-lived stream tokens (BACKEND-REQUIREMENTS R-T3): the push channel is
4
- * authenticated by a per-session token minted over REST, not the ambient
5
- * cookie. Compact HMAC-SHA256 JWT via node:crypto — no new dependency; the
6
- * Phase-6 realtime server verifies with `verifyWsToken`.
7
- *
8
- * Secret resolution mirrors apps/web's Better Auth pattern: AILK_WS_TOKEN_SECRET
9
- * wins, BETTER_AUTH_SECRET is the shared fallback, and a dev-only default
10
- * keeps the OSS zero-config boot working. Production deployments MUST set one
11
- * of the two env vars.
12
- */
13
- import { createHmac, timingSafeEqual } from "node:crypto";
14
-
15
- import { getClock } from "@working-theory/common";
16
- import type { Id } from "@working-theory/ids";
17
-
18
- export const WS_TOKEN_TTL_SECONDS = 60;
19
-
20
- export interface WsTokenClaims {
21
- /** Caller user id. */
22
- sub: Id<"user">;
23
- /** Agent-session id the stream is scoped to. */
24
- sid: Id<"agentSession">;
25
- /** Workspace id the session belongs to (tenant scope). */
26
- wid: Id<"workspace">;
27
- iat: number;
28
- exp: number;
29
- }
30
-
31
- function resolveSecret(): string {
32
- const secret =
33
- process.env.AILK_WS_TOKEN_SECRET ?? process.env.BETTER_AUTH_SECRET;
34
- if (secret) return secret;
35
- // Fail CLOSED in production (security review M2): a token-minting path
36
- // must never run on a guessable default secret.
37
- if (process.env.NODE_ENV === "production") {
38
- throw new Error(
39
- "ws-token: set AILK_WS_TOKEN_SECRET or BETTER_AUTH_SECRET in production",
40
- );
41
- }
42
- return "ailk-dev-secret-do-not-use-in-production";
43
- }
44
-
45
- function b64url(input: Buffer | string): string {
46
- return Buffer.from(input).toString("base64url");
47
- }
48
-
49
- function sign(payload: string): string {
50
- return createHmac("sha256", resolveSecret()).update(payload).digest("base64url");
51
- }
52
-
53
- /** Mint a short-lived stream token for (user, session, workspace). */
54
- export function mintWsToken(params: {
55
- userId: Id<"user">;
56
- sessionId: Id<"agentSession">;
57
- workspaceId: Id<"workspace">;
58
- }): { token: string; expiresAt: string } {
59
- const nowSec = Math.floor(getClock().now() / 1000);
60
- const claims: WsTokenClaims = {
61
- sub: params.userId,
62
- sid: params.sessionId,
63
- wid: params.workspaceId,
64
- iat: nowSec,
65
- exp: nowSec + WS_TOKEN_TTL_SECONDS,
66
- };
67
- const head = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
68
- const body = b64url(JSON.stringify(claims));
69
- const token = `${head}.${body}.${sign(`${head}.${body}`)}`;
70
- return { token, expiresAt: new Date(claims.exp * 1000).toISOString() };
71
- }
72
-
73
- /** Verify a stream token; returns claims or null (invalid / expired). */
74
- export function verifyWsToken(token: string): WsTokenClaims | null {
75
- const [head, body, sig] = token.split(".");
76
- if (!head || !body || !sig) return null;
77
- const expected = sign(`${head}.${body}`);
78
- const a = Buffer.from(sig);
79
- const b = Buffer.from(expected);
80
- if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
81
- try {
82
- const claims = JSON.parse(
83
- Buffer.from(body, "base64url").toString("utf-8"),
84
- ) as WsTokenClaims;
85
- if (typeof claims.exp !== "number") return null;
86
- if (claims.exp * 1000 <= getClock().now()) return null;
87
- return claims;
88
- } catch {
89
- return null;
90
- }
91
- }
@@ -1,99 +0,0 @@
1
- // oss: false
2
- /**
3
- * Tenant guard preHandler (multi-tenant module — FEAT-042).
4
- *
5
- * Runs AFTER the dual-mode auth preHandler inside the workspace route scope.
6
- * Default-deny: resolves the caller's active tenant via
7
- * `services/tenant-context.ts#resolveActiveTenant` and rejects everything
8
- * that is not an explicit grant:
9
- *
10
- * - bearer-authenticated requests (no workspace binding yet — #3591) → 403
11
- * - cookie sessions with no / stale active workspace → 403
12
- * - revoked membership → 403
13
- * - URL workspaceId ≠ the session's active workspace → 403
14
- * (compared BEFORE any lookup of the target — no tenant enumeration)
15
- *
16
- * On grant it decorates `request.tenant` (a `TenantContext`); route handlers
17
- * pass that to `tenantDb(tenant)` so every domain query is workspace-scoped.
18
- * Routes whose target is a session/artifact id (no workspaceId param) rely on
19
- * the scope layer's `findFirst({ id, workspaceId })` — a foreign target is a
20
- * 404, indistinguishable from a missing one.
21
- */
22
- import type {
23
- FastifyPluginCallback,
24
- FastifyReply,
25
- FastifyRequest,
26
- } from "fastify";
27
-
28
- import {
29
- resolveActiveTenant,
30
- type TenantContext,
31
- } from "../services/tenant-context.js";
32
-
33
- declare module "fastify" {
34
- interface FastifyRequest {
35
- tenant?: TenantContext;
36
- }
37
- }
38
-
39
- const FORBIDDEN = {
40
- ok: false,
41
- error: { code: "forbidden", message: "Forbidden" },
42
- } as const;
43
-
44
- /**
45
- * Resolve + attach the active tenant, or reply 403. Registered as a hook in
46
- * the workspace route scope (after auth), so no tenant-scoped handler runs
47
- * without a granted TenantContext.
48
- */
49
- export async function tenantPreHandler(
50
- request: FastifyRequest,
51
- reply: FastifyReply,
52
- ): Promise<void> {
53
- const userId = request.userId;
54
- if (!userId) {
55
- reply.code(401).send({
56
- ok: false,
57
- error: { code: "unauthorized", message: "Authentication required" },
58
- });
59
- return;
60
- }
61
-
62
- const resolved = await resolveActiveTenant({
63
- userId,
64
- sessionTenant: request.sessionTenant,
65
- });
66
- if (!resolved.ok) {
67
- reply.code(403).send(FORBIDDEN);
68
- return;
69
- }
70
-
71
- // URL-scoped routes: the target workspace MUST be the session's active
72
- // workspace. Compared before any target lookup — a foreign (or nonexistent)
73
- // workspaceId gets the same 403.
74
- const params = request.params as { workspaceId?: string } | undefined;
75
- if (params?.workspaceId && params.workspaceId !== resolved.value.workspaceId) {
76
- reply.code(403).send(FORBIDDEN);
77
- return;
78
- }
79
-
80
- request.tenant = resolved.value;
81
- }
82
-
83
- /**
84
- * Plugin form, mirroring `authPreHandlerPlugin`: skip-override so the hook
85
- * propagates to sibling route plugins registered in the same scope.
86
- */
87
- export const tenantPreHandlerPlugin: FastifyPluginCallback = (
88
- fastify,
89
- _opts,
90
- done,
91
- ) => {
92
- fastify.decorateRequest("tenant", undefined);
93
- fastify.addHook("preHandler", tenantPreHandler);
94
- done();
95
- };
96
-
97
- (tenantPreHandlerPlugin as FastifyPluginCallback & { [key: symbol]: boolean })[
98
- Symbol.for("skip-override")
99
- ] = true;