create-ailk 0.23.0 → 0.25.1

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 (604) hide show
  1. package/README.md +68 -33
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +10 -3
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +38 -4
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +75 -22
  10. package/dist/lib/design-checks.d.ts +9 -4
  11. package/dist/lib/design-checks.js +13 -7
  12. package/dist/lib/extract-module-bundle.js +29 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +1 -1
  14. package/dist/lib/fetch-template-snapshot.js +7 -3
  15. package/dist/lib/import-graph.d.ts +1 -1
  16. package/dist/lib/import-graph.js +4 -4
  17. package/dist/lib/module-license-gate.d.ts +12 -1
  18. package/dist/lib/module-license-gate.js +34 -12
  19. package/dist/lib/scaffold-render-sweep.d.ts +33 -0
  20. package/dist/lib/scaffold-render-sweep.js +45 -0
  21. package/dist/module-architecture.d.ts +20 -3
  22. package/dist/module-architecture.js +122 -538
  23. package/dist/paid-sections.d.ts +25 -1
  24. package/dist/paid-sections.js +88 -3
  25. package/dist/surfaces.js +10 -2
  26. package/dist/sync-routes.d.ts +30 -4
  27. package/dist/sync-routes.js +271 -18
  28. package/dist/template-record.d.ts +14 -4
  29. package/dist/template-record.js +23 -5
  30. package/dist/topology.d.ts +16 -3
  31. package/dist/topology.js +31 -19
  32. package/dist/update.d.ts +10 -1
  33. package/dist/update.js +540 -16
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  37. package/templates/.claude/hooks/template-guard.sh +137 -0
  38. package/templates/.claude/rules/extending.md +106 -0
  39. package/templates/.claude/rules/loop-contract.md +2 -2
  40. package/templates/.claude/settings.json +11 -0
  41. package/templates/.env.example +9 -5
  42. package/templates/.github/workflows/ci.yml +7 -4
  43. package/templates/CLAUDE.md +15 -3
  44. package/templates/CONVENTIONS.md +8 -10
  45. package/templates/README.md +14 -13
  46. package/templates/apps/api/.env.example +9 -2
  47. package/templates/apps/api/CLAUDE.md +122 -82
  48. package/templates/apps/api/jest.config.cjs +15 -5
  49. package/templates/apps/api/package.json +16 -20
  50. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  51. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  52. package/templates/apps/api/src/index.ts +1 -1
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
  54. package/templates/apps/api/src/modules/commerce.ts +10 -0
  55. package/templates/apps/api/src/modules/content-write.ts +10 -0
  56. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  57. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  58. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  59. package/templates/apps/api/src/modules/workspace.ts +10 -0
  60. package/templates/apps/api/src/server.ts +117 -1857
  61. package/templates/apps/api/vercel.json +2 -2
  62. package/templates/apps/mcp/CLAUDE.md +77 -24
  63. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  64. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  65. package/templates/apps/mcp/jest.config.cjs +6 -1
  66. package/templates/apps/mcp/package.json +5 -12
  67. package/templates/apps/mcp/src/app.ts +8 -7
  68. package/templates/apps/mcp/src/index.ts +6 -34
  69. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  70. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  71. package/templates/apps/mcp/src/site.ts +12 -0
  72. package/templates/apps/web/.env.example +1 -1
  73. package/templates/apps/web/CLAUDE.md +21 -5
  74. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  75. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  76. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  77. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  78. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  79. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  80. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
  81. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  82. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  83. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  84. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  85. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  86. package/templates/apps/web/auth.ts +2 -2
  87. package/templates/apps/web/jest.config.cjs +1 -0
  88. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  89. package/templates/apps/web/lib/auth-email.ts +1 -1
  90. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  91. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  92. package/templates/apps/web/lib/relay.ts +2 -2
  93. package/templates/apps/web/lib/release-scope.ts +47 -0
  94. package/templates/apps/web/lib/server-api.ts +1 -1
  95. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  96. package/templates/customizations.yaml +37 -0
  97. package/templates/database/prisma.config.ts +10 -14
  98. package/templates/database/seed-test.ts +2 -2
  99. package/templates/database/seed.ts +32 -5
  100. package/templates/package.json +12 -6
  101. package/templates/pnpm-workspace.yaml +0 -1
  102. package/templates/project.yaml +16 -8
  103. package/templates/tools/jest/reporters.cjs +65 -0
  104. package/templates/tools/lint-architecture.ts +757 -7
  105. package/templates/tsconfig.json +1 -2
  106. package/versions.json +48 -35
  107. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  108. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  109. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  110. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  111. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  112. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  113. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  114. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  115. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  116. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  117. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  118. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  119. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  120. package/templates/apps/api/src/bin/spokes.ts +0 -198
  121. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  122. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  123. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  124. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  125. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  126. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  127. package/templates/apps/api/src/config/index.ts +0 -596
  128. package/templates/apps/api/src/config/modules.ts +0 -83
  129. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  130. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  131. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  132. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  133. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  134. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  135. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  136. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  137. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  138. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  139. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  140. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  141. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  142. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  143. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  144. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  145. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  146. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  147. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  148. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
  149. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  150. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  151. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  152. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  153. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  154. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  155. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  156. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  157. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  158. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  159. package/templates/apps/api/src/lib/errors.ts +0 -95
  160. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  161. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  162. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  163. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  164. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  165. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  166. package/templates/apps/api/src/lib/locales.ts +0 -201
  167. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  168. package/templates/apps/api/src/lib/prisma.ts +0 -21
  169. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  170. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  171. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  172. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  173. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  174. package/templates/apps/api/src/lib/site.ts +0 -254
  175. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  176. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  177. package/templates/apps/api/src/lib/stripe.ts +0 -318
  178. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  179. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  180. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  181. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  182. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  183. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  184. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  185. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  186. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  187. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  188. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  189. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  190. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  191. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  192. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  193. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  194. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  195. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  196. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  197. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  198. package/templates/apps/api/src/middleware/auth.ts +0 -864
  199. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  200. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  201. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
  202. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  203. package/templates/apps/api/src/openapi/index.ts +0 -18
  204. package/templates/apps/api/src/openapi/spec.ts +0 -1815
  205. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  206. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  207. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  208. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  209. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  210. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  211. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  212. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  213. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  214. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  215. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  216. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  217. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  218. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
  219. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  220. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
  221. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  222. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  223. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  224. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  225. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  226. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  227. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  228. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  229. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  230. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  231. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  232. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  233. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  234. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  235. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  236. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  237. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  238. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  239. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  240. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  241. package/templates/apps/api/src/routes/content/get-page.ts +0 -268
  242. package/templates/apps/api/src/routes/content/index.ts +0 -49
  243. package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
  244. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  245. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  246. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  247. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  248. package/templates/apps/api/src/routes/content/search.ts +0 -129
  249. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  250. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  251. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  252. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  253. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  254. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  255. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  256. package/templates/apps/api/src/routes/leads/index.ts +0 -573
  257. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  258. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  259. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  260. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  261. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
  262. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
  263. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  264. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  265. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  266. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  267. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  268. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  269. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  270. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  271. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  272. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  273. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  274. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  275. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  276. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  277. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  278. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  279. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  280. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  281. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  282. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  285. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
  286. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  287. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  288. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  289. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  290. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  291. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  292. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  293. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  294. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
  295. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  296. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  297. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  298. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
  299. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
  300. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  301. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  302. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  303. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  304. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  305. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  306. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  307. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
  308. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  309. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  310. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  311. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  312. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  313. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  314. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  315. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  316. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  317. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
  318. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
  319. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
  320. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  321. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  322. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  323. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  324. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  325. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  326. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  327. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  328. package/templates/apps/api/src/services/abandon-email.ts +0 -313
  329. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  330. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  331. package/templates/apps/api/src/services/audit-log.ts +0 -961
  332. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  333. package/templates/apps/api/src/services/consent.ts +0 -834
  334. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  335. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
  336. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  337. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  338. package/templates/apps/api/src/services/entitlements.ts +0 -338
  339. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  340. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  341. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  342. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  343. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  344. package/templates/apps/api/src/services/listing-config.ts +0 -178
  345. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  346. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
  347. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  348. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
  349. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  350. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
  351. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
  352. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  353. package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
  354. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
  355. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  356. package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
  357. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  358. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  359. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  360. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  361. package/templates/apps/api/src/services/person.ts +0 -1188
  362. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  363. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  364. package/templates/apps/api/src/services/project-listings.ts +0 -2073
  365. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  366. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  367. package/templates/apps/api/src/services/revalidation.ts +0 -235
  368. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  369. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  370. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  371. package/templates/apps/api/src/services/verified-email.ts +0 -71
  372. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  373. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  374. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  375. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  376. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  377. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  378. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  379. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  380. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  381. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  382. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  383. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  384. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  385. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  386. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  387. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  388. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
  389. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  390. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  391. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  392. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
  393. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
  394. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  395. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  396. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
  397. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  398. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
  399. package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
  400. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  401. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  402. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
  403. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
  404. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
  405. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  406. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  407. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
  408. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
  409. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  410. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  411. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  412. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  413. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  414. package/templates/apps/mcp/src/abuse/index.ts +0 -342
  415. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  416. package/templates/apps/mcp/src/boot.ts +0 -167
  417. package/templates/apps/mcp/src/config/modules.ts +0 -43
  418. package/templates/apps/mcp/src/http-transport.ts +0 -526
  419. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  420. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  421. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
  422. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
  423. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  424. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  425. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  426. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  427. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  428. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  429. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  430. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  431. package/templates/apps/mcp/src/request-context.ts +0 -52
  432. package/templates/apps/mcp/src/server.ts +0 -343
  433. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  434. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  435. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
  436. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  437. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  438. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  439. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  440. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  441. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  442. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  443. package/templates/apps/mcp/src/tools/index.ts +0 -137
  444. package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
  445. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  446. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  447. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  448. package/templates/apps/mcp/src/tools/read_content.ts +0 -75
  449. package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
  450. package/templates/apps/mcp/src/tools/search_content.ts +0 -67
  451. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
  452. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  453. package/templates/database/CHANGELOG.md +0 -961
  454. package/templates/database/LICENSE +0 -202
  455. package/templates/database/NOTICE +0 -2
  456. package/templates/database/README.md +0 -243
  457. package/templates/database/__tests__/agents.db.test.ts +0 -320
  458. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  459. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  460. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  461. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  462. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  463. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  464. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  465. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  466. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  467. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  468. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  469. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  470. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  471. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  472. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  473. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  474. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  475. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  476. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  477. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  478. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  479. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  480. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  481. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  482. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  483. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  484. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  485. package/templates/database/__tests__/person.db.test.ts +0 -586
  486. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  487. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  488. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  489. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  490. package/templates/database/__tests__/seed.test.ts +0 -240
  491. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  492. package/templates/database/__tests__/tenancy.test.ts +0 -141
  493. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  494. package/templates/database/_gitignore +0 -8
  495. package/templates/database/auth/.gitkeep +0 -0
  496. package/templates/database/auth/schema.prisma +0 -641
  497. package/templates/database/config/.gitkeep +0 -0
  498. package/templates/database/config/schema.prisma +0 -441
  499. package/templates/database/content/.gitkeep +0 -0
  500. package/templates/database/content/schema.prisma +0 -339
  501. package/templates/database/eslint.config.mjs +0 -69
  502. package/templates/database/flows/listing-diagnostic.ts +0 -182
  503. package/templates/database/flows/presets.ts +0 -315
  504. package/templates/database/inbox/.gitkeep +0 -0
  505. package/templates/database/inbox/README.md +0 -3
  506. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  507. package/templates/database/inbox/schema.prisma +0 -1447
  508. package/templates/database/jest.config.cjs +0 -36
  509. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  510. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  511. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  512. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  513. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  514. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  515. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  516. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  517. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  518. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  519. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  520. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  521. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  522. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  523. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  524. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  525. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  526. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  527. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  528. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  529. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  530. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  531. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  532. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  533. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  534. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  535. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  536. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  537. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  538. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  539. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  540. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  541. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  542. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  543. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  544. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  545. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  546. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  547. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  548. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  549. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  550. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  551. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  552. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  553. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  554. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  555. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  556. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  557. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  558. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  559. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  560. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  561. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  562. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  563. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  564. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  565. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  566. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  567. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  568. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  569. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  570. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  571. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  572. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  573. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  574. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  575. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  576. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  577. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  578. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  579. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  580. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  581. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  582. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  583. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  584. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  585. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  586. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  587. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  588. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  589. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  590. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  591. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  592. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  593. package/templates/database/migrations/migration_lock.toml +0 -3
  594. package/templates/database/ops/.gitkeep +0 -0
  595. package/templates/database/ops/schema.prisma +0 -43
  596. package/templates/database/package.json +0 -89
  597. package/templates/database/schema.prisma +0 -71
  598. package/templates/database/scripts/db-generate-locked.sh +0 -80
  599. package/templates/database/scripts/person-backfill.sql +0 -132
  600. package/templates/database/tenancy.ts +0 -183
  601. package/templates/database/tsconfig.build.json +0 -16
  602. package/templates/database/tsconfig.flows.json +0 -15
  603. package/templates/database/tsconfig.json +0 -16
  604. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,864 +0,0 @@
1
- /**
2
- * Dual-mode authentication preHandler for apps/api (#587).
3
- *
4
- * Decorates `request.userId` on authenticated requests, or returns 401.
5
- * Two modes, evaluated in order:
6
- *
7
- * 1. Cookie path — Better Auth session cookie present and valid.
8
- * Looked up directly against the `Session` table (the row Better Auth's
9
- * database adapter creates on sign-in). Cookie wins when both modes are
10
- * present (locked by brief decision OQ-D / spec §3 "Both present").
11
- * Non-GET requests with a cookie are required to carry an Origin header
12
- * that is exactly one of the configured trusted origins (#7395) — the
13
- * lightweight CSRF mechanism agreed in OQ-D. Bearer-authenticated callers
14
- * skip the origin check (non-browser flow).
15
- *
16
- * 2. Bearer path — Authorization: Bearer <token>. The token is sha256-hashed
17
- * and matched against `ApiToken.token` (stored as hash). Revoked / expired
18
- * tokens fail closed; lastUsedAt is bumped on a successful hit.
19
- *
20
- * 3. Agent path (#3591) — a bearer starting `ailk_agt_` is an agent's key
21
- * (`lib/agent-store.ts`). Its digest must match an enabled `Agent` whose
22
- * key-holder still holds a membership; anything else is the same 401.
23
- * The request acts as the agent's person (`request.userId`) and carries
24
- * `request.agent` — never `request.sessionTenant`, which stays the
25
- * cookie-only signal. An agent reaches ONLY routes a tenant guard tagged
26
- * (`config.tenantGuarded`, set by both guards' `onRoute` hook) and is 403
27
- * everywhere else; inside them the guard clamps its role. Each agent is
28
- * limited to 60 requests a minute, at most 10 of them writes (any
29
- * method but GET and HEAD).
30
- *
31
- * 4. Hub-credential path (#7894) — a bearer starting `ailk_hst_` is a
32
- * credential a spoke holds for a person the hub signed in on its behalf
33
- * (`lib/hub-sign-in-store.ts`). It is admitted only beside the SAME
34
- * spoke's key (`x-ailk-spoke-key`), only while its grant is unrevoked and
35
- * unexpired, its client enabled and its person's email verified, and only
36
- * on `HUB_SESSION_ROUTES` — an exact list of METHOD + registered URL
37
- * pairs — the credential's own `me`, the member routes and the
38
- * account-agreements pair, 26 in all (#8220, #8515, #8765); any other
39
- * route is 404 (never 403, so the account area's `read()` loads
40
- * "nothing for this caller"). It acts as its person
41
- * (`request.userId`) and carries `request.hubSession` — never
42
- * `request.sessionTenant` — and its request logger names the spoke
43
- * (`spokeId`). Each person is limited, at each spoke, to 60 requests and
44
- * 10 writes a minute and 30 writes an hour — a fixed hour window from
45
- * the bucket's first admitted write (#8220 D2, SEC-8169-4): the bucket is
46
- * keyed on the VERIFIED spoke and person, never a header, and charged
47
- * only after every check above passes, so a refused credential or an
48
- * off-list route never gets one. The write limits refuse only writes,
49
- * and a request the budget refuses is not counted.
50
- *
51
- * Every authenticated request carries `request.actor` — the person, or the
52
- * agent and the person it acts for — and its request/reply loggers are
53
- * rebound to name it, so Fastify's "request completed" line says who acted.
54
- * No log line carries a credential.
55
- *
56
- * Decision matrix (per spec §3):
57
- * - Cookie valid → cookie path wins. Bearer ignored.
58
- * - Cookie absent, bearer valid → bearer path; CSRF skipped.
59
- * - Both modes invalid → 401 unauthorized.
60
- * - Cookie expired + bearer valid → 401 (no silent fallback).
61
- * - Cookie absent, bearer malformed → 401 (no silent fallback).
62
- * - DATABASE_URL unset → 503 database_not_configured (#6482).
63
- *
64
- * Public routes (health, content, leads, webhooks, openapi) are NOT registered
65
- * with this preHandler — server.ts narrows scope via a sub-plugin.
66
- */
67
- import { createHash } from "node:crypto";
68
-
69
- import { getClock } from "@working-theory/common";
70
- import type { Id } from "@working-theory/ids";
71
- import type {
72
- FastifyPluginCallback,
73
- FastifyReply,
74
- FastifyRequest,
75
- } from "fastify";
76
-
77
- import {
78
- AGENT_KEY_PREFIX,
79
- findAgentByKeyHash,
80
- hashAgentKey,
81
- touchAgentLastActive,
82
- type AgentGrant,
83
- } from "../lib/agent-store.js";
84
- import {
85
- findAuthorizationByCredentialHash,
86
- HUB_SESSION_PREFIX,
87
- } from "../lib/hub-sign-in-store.js";
88
- import { prisma } from "../lib/prisma.js";
89
- import { findSpokeByKeyHash, hashSpokeKey } from "../lib/spoke-store.js";
90
-
91
- /**
92
- * Better Auth's default cookie names. Both are tried in order so a deployment
93
- * running over HTTP locally and HTTPS in production reads the same code path.
94
- */
95
- const COOKIE_NAMES = [
96
- "better-auth.session_token",
97
- "__Secure-better-auth.session_token",
98
- ] as const;
99
-
100
- const UNAUTHORIZED = { error: "unauthorized" } as const;
101
- const DATABASE_NOT_CONFIGURED = { error: "database_not_configured" } as const;
102
- const NOT_FOUND = {
103
- ok: false,
104
- error: { code: "not_found", message: "Not found" },
105
- } as const;
106
- const SPOKE_KEY_HEADER = "x-ailk-spoke-key";
107
- const FORBIDDEN = {
108
- ok: false,
109
- error: { code: "forbidden", message: "Forbidden" },
110
- } as const;
111
- /**
112
- * How stale `Agent.lastActiveAt` may be before an admitted request writes it
113
- * again: the column is "active within the last minute", not a request log,
114
- * so a busy agent costs one UPDATE a minute rather than one per request.
115
- */
116
- const AGENT_ACTIVITY_WRITE_INTERVAL_MS = 60_000;
117
-
118
- const RATE_LIMITED = {
119
- ok: false,
120
- error: { code: "rate_limited", message: "Too many requests" },
121
- } as const;
122
-
123
- declare module "fastify" {
124
- interface FastifyRequest {
125
- userId?: Id<"user">;
126
- sessionTenant?: SessionTenant;
127
- /** Set ONLY on the agent path (#3591 D4). */
128
- agent?: AgentGrant;
129
- /** Who made the request — set on every authenticated request (#3591 D9). */
130
- actor?: RequestActor;
131
- /**
132
- * Set ONLY on the hub-credential path (#7894 D6): the spoke the credential
133
- * and the request's spoke key both name, and that spoke's workspace.
134
- */
135
- hubSession?: HubSession;
136
- }
137
- interface FastifyContextConfig {
138
- /**
139
- * Set by both tenant guards' `onRoute` hook on every route registered
140
- * after them in their scope (#3591 D6). An agent is refused on any route
141
- * without it.
142
- */
143
- tenantGuarded?: boolean;
144
- }
145
- }
146
-
147
- /** The spoke a hub-issued credential was admitted for (#7894 D6). */
148
- export type HubSession = {
149
- spokeId: Id<"spoke">;
150
- workspaceId: Id<"workspace">;
151
- };
152
-
153
- /**
154
- * The only routes a hub-issued credential reaches (#7894 D6, #8220 D1): an
155
- * EXACT list of method + registered URL, never a prefix and never computed,
156
- * so a route added later joins only by a reviewed edit of this list
157
- * (SEC-7894-3). Six groups, in this order: the credential's own `me` and
158
- * #7877's three member reads; #8169's eight member reads; #8170's six member
159
- * writes (roadmap §7.2 row order); #8438's member consents read and write;
160
- * #8765's account-agreements read and write; #8515's onboarding read and
161
- * write — 26 in all.
162
- * `GET /v1/consent/platform` is not admitted — it is the Admin owner's own
163
- * consent to Working Theory, on the hub's cookie session. Nor is the
164
- * anonymous `GET /v1/consent/sign-up` (#8765 D2): it takes no credential. The member profile write IS admitted — it
165
- * is the person's own account, which roadmap D9 saves through it. Not
166
- * admitted, deliberately: `GET /v1/waitlist-signups/me`,
167
- * `GET /v1/flow-checkouts/me`, `GET /v1/subscription/me` and
168
- * `POST /v1/subscription/portal-session` (they answer for the HUB's site and
169
- * the person's platform billing, SEC-7894-1, -2), the token routes and the
170
- * hub's own-account `/v1/user/*` routes, `PATCH /v1/user/profile` among them.
171
- */
172
- export const HUB_SESSION_ROUTES: readonly string[] = Object.freeze([
173
- "GET /v1/hub-sign-in/me",
174
- "GET /v1/member/pieces",
175
- "GET /v1/member/messages",
176
- "GET /v1/member/messages/:id/file",
177
- "GET /v1/member/purchases",
178
- "GET /v1/member/appointments",
179
- "GET /v1/member/artifacts",
180
- "GET /v1/member/artifacts/:pieceId/versions/:version/file",
181
- "GET /v1/member/meeting-types",
182
- "GET /v1/member/availability",
183
- "GET /v1/member/request-form",
184
- "GET /v1/member/billing",
185
- "POST /v1/member/artifacts/:pieceId/review",
186
- "POST /v1/member/bookings",
187
- "POST /v1/member/requests",
188
- "POST /v1/member/messages",
189
- "PATCH /v1/member/profile",
190
- "GET /v1/member/profile/contact",
191
- "PATCH /v1/member/profile/contact",
192
- "POST /v1/member/deletion-request",
193
- // #8438 D6 — the member's own consents. `GET /v1/consent/platform` is NOT
194
- // admitted: those are the Admin owner's, on the hub's own cookie session.
195
- "GET /v1/member/consents",
196
- "POST /v1/member/consents",
197
- // #8765 D5 — the caller's account-level sign-up agreements, so a spoke's
198
- // account layout reads and accepts the one grant every site shares.
199
- "GET /v1/consent/account",
200
- "POST /v1/consent/account",
201
- // #8515 D13 — the client's onboarding read and write.
202
- "GET /v1/member/onboarding",
203
- "POST /v1/member/onboarding",
204
- ]);
205
-
206
- /**
207
- * Who made a request (#3591 D9). A cookie session and a personal `ApiToken`
208
- * act as their person; an agent acts on behalf of its responsible person.
209
- */
210
- export type RequestActor =
211
- | { type: "user"; userId: Id<"user"> }
212
- | { type: "agent"; agentId: Id<"agent">; onBehalfOfUserId: Id<"user"> };
213
-
214
- /**
215
- * Active tenant context carried by a cookie session (multi-tenant module).
216
- * Set ONLY on the cookie path — a personal `ApiToken` carries no workspace
217
- * binding, and an agent carries its own (`request.agent`, #3591), so neither
218
- * ever sets this. Code that needs a cookie session (the billing portal, the
219
- * workspace switch) can rely on its presence. `sessionRowId` is the Session
220
- * PK, used by the workspace-switch service to update the active tenant
221
- * server-side.
222
- */
223
- export type SessionTenant = {
224
- sessionRowId: Id<"session">;
225
- activeOrganizationId: Id<"organization"> | null;
226
- activeWorkspaceId: Id<"workspace"> | null;
227
- };
228
-
229
- /**
230
- * Hash an opaque bearer-token string the same way `tokens.ts` does at create
231
- * time. Centralised here so the lookup contract stays identical.
232
- */
233
- function hashBearer(token: string): string {
234
- return createHash("sha256").update(token).digest("hex");
235
- }
236
-
237
- /**
238
- * Parse the request's Cookie header for the first Better Auth session-token
239
- * variant present. Returns the cookie value (decoded) or undefined.
240
- *
241
- * Cookie parsing intentionally avoids decoding `+` as space — session tokens
242
- * are URL-safe and don't contain `+`. Use decodeURIComponent only on the value
243
- * to handle the rare case Better Auth percent-encodes the token.
244
- */
245
- function readSessionCookie(request: FastifyRequest): string | undefined {
246
- const raw = request.headers.cookie;
247
- if (!raw) return undefined;
248
- for (const part of raw.split(";")) {
249
- const eq = part.indexOf("=");
250
- if (eq < 0) continue;
251
- const name = part.slice(0, eq).trim();
252
- if (!COOKIE_NAMES.includes(name as (typeof COOKIE_NAMES)[number])) continue;
253
- const value = part.slice(eq + 1).trim();
254
- if (!value) continue;
255
- try {
256
- // Better Auth signs the cookie as <token>.<signature>; the dual-mode
257
- // preHandler only needs the token portion to look up the Session row.
258
- // Strip the signature if present.
259
- const decoded = decodeURIComponent(value);
260
- const dot = decoded.indexOf(".");
261
- return dot > 0 ? decoded.slice(0, dot) : decoded;
262
- } catch {
263
- return undefined;
264
- }
265
- }
266
- return undefined;
267
- }
268
-
269
- function readBearer(request: FastifyRequest): string | undefined {
270
- const auth = request.headers.authorization;
271
- if (!auth) return undefined;
272
- const match = /^Bearer\s+(\S+)$/i.exec(auth);
273
- return match ? match[1] : undefined;
274
- }
275
-
276
- /**
277
- * CSRF check for cookie-authenticated mutating requests (#7395).
278
- * Returns true if the request is safe (GET/HEAD/OPTIONS) or the Origin header
279
- * is exactly one of the trusted origins. False otherwise — including for any
280
- * write when the list is empty, in every environment.
281
- */
282
- function isOriginAllowed(
283
- request: FastifyRequest,
284
- trustedOrigins: readonly string[],
285
- ): boolean {
286
- const method = request.method.toUpperCase();
287
- if (method === "GET" || method === "HEAD" || method === "OPTIONS")
288
- return true;
289
- const origin = request.headers.origin;
290
- if (!origin) return false;
291
- return trustedOrigins.includes(origin);
292
- }
293
-
294
- type SessionRow = {
295
- id: string;
296
- userId: string;
297
- expiresAt: Date;
298
- activeOrganizationId: string | null;
299
- activeWorkspaceId: string | null;
300
- };
301
-
302
- async function resolveCookieSession(
303
- cookieToken: string,
304
- ): Promise<SessionRow | undefined> {
305
- const row = await prisma.session.findUnique({
306
- where: { token: cookieToken },
307
- select: {
308
- id: true,
309
- userId: true,
310
- expiresAt: true,
311
- activeOrganizationId: true,
312
- activeWorkspaceId: true,
313
- },
314
- });
315
- if (!row) return undefined;
316
- return row;
317
- }
318
-
319
- type BearerRow = {
320
- userId: string;
321
- expiresAt: Date | null;
322
- revokedAt: Date | null;
323
- id: string;
324
- };
325
-
326
- async function resolveBearerToken(
327
- bearer: string,
328
- ): Promise<BearerRow | undefined> {
329
- const hash = hashBearer(bearer);
330
- const row = await prisma.apiToken.findUnique({
331
- where: { token: hash },
332
- select: { id: true, userId: true, expiresAt: true, revokedAt: true },
333
- });
334
- return row ?? undefined;
335
- }
336
-
337
- /**
338
- * Decorate `request.actor` and rebind the request and reply loggers to name
339
- * it (#3591 D9). Fastify writes its "request completed" line through
340
- * `reply.log`, so that line carries the actor too.
341
- */
342
- function bindActor(
343
- request: FastifyRequest,
344
- reply: FastifyReply,
345
- actor: RequestActor,
346
- ): void {
347
- request.actor = actor;
348
- request.log = request.log.child(
349
- actor.type === "agent"
350
- ? {
351
- actorType: "agent",
352
- agentId: actor.agentId,
353
- onBehalfOfUserId: actor.onBehalfOfUserId,
354
- }
355
- : { actorType: "user" },
356
- );
357
- reply.log = request.log;
358
- }
359
-
360
- // ── The per-caller limits (#3591 D4 step 5, SEC-3591-04/-14; #8220 D2) ──────
361
- //
362
- // In the handler, not `onRequest`: the shared limiter runs before any
363
- // identity exists, and a bucket must only ever be keyed on a VERIFIED id, so
364
- // an invented key or credential never gets one. The shape of
365
- // `routes/subscription/portal.ts`'s per-user budget. An agent and a person
366
- // never share a map.
367
-
368
- const AGENT_WINDOW_MS = 60_000;
369
- const AGENT_REQUESTS_PER_WINDOW = 60;
370
- const AGENT_WRITES_PER_WINDOW = 10;
371
- /** Cap on distinct buckets — the oldest is evicted at capacity (DEF-011's shape). */
372
- const MAX_AGENT_BUCKETS = 5_000;
373
-
374
- const HUB_SESSION_WINDOW_MS = 60_000;
375
- const HUB_SESSION_REQUESTS_PER_WINDOW = 60;
376
- const HUB_SESSION_WRITES_PER_WINDOW = 10;
377
- const HUB_SESSION_WRITE_HOUR_MS = 3_600_000;
378
- const HUB_SESSION_WRITES_PER_HOUR = 30;
379
- const MAX_HUB_SESSION_BUCKETS = 5_000;
380
-
381
- type CallerBucket = {
382
- windowStartMs: number;
383
- requests: number;
384
- writes: number;
385
- /** The hourly write window — set by the bucket's first admitted write. */
386
- hourStartMs: number | null;
387
- hourWrites: number;
388
- };
389
-
390
- type CallerLimits = {
391
- windowMs: number;
392
- maxBuckets: number;
393
- requests: number;
394
- writes: number;
395
- /** An hourly write ceiling — the hub-credential path's alone. */
396
- writesPerHour?: { limit: number; windowMs: number };
397
- /** When true, the write limits refuse only writes; a read meets only `requests`. */
398
- refuseOnlyWrites: boolean;
399
- /** When true, a refused request still counts toward every limit. */
400
- countRefused: boolean;
401
- };
402
-
403
- const AGENT_LIMITS: CallerLimits = {
404
- windowMs: AGENT_WINDOW_MS,
405
- maxBuckets: MAX_AGENT_BUCKETS,
406
- requests: AGENT_REQUESTS_PER_WINDOW,
407
- writes: AGENT_WRITES_PER_WINDOW,
408
- refuseOnlyWrites: false,
409
- countRefused: true,
410
- };
411
-
412
- const HUB_SESSION_LIMITS: CallerLimits = {
413
- windowMs: HUB_SESSION_WINDOW_MS,
414
- maxBuckets: MAX_HUB_SESSION_BUCKETS,
415
- requests: HUB_SESSION_REQUESTS_PER_WINDOW,
416
- writes: HUB_SESSION_WRITES_PER_WINDOW,
417
- writesPerHour: {
418
- limit: HUB_SESSION_WRITES_PER_HOUR,
419
- windowMs: HUB_SESSION_WRITE_HOUR_MS,
420
- },
421
- refuseOnlyWrites: true,
422
- countRefused: false,
423
- };
424
-
425
- const agentBuckets = new Map<string, CallerBucket>();
426
- const hubSessionBuckets = new Map<string, CallerBucket>();
427
-
428
- /** Test-only reset. Not intended for production use. */
429
- export function __resetAgentRateBuckets(): void {
430
- agentBuckets.clear();
431
- }
432
-
433
- /** Test-only reset. Not intended for production use. */
434
- export function __resetHubSessionRateBuckets(): void {
435
- hubSessionBuckets.clear();
436
- }
437
-
438
- function isWrite(method: string): boolean {
439
- const m = method.toUpperCase();
440
- return m !== "GET" && m !== "HEAD";
441
- }
442
-
443
- /** A bucket no window still holds — the eviction pass may drop it. */
444
- function isStale(
445
- bucket: CallerBucket,
446
- now: number,
447
- limits: CallerLimits,
448
- ): boolean {
449
- if (now - bucket.windowStartMs < limits.windowMs) return false;
450
- return (
451
- bucket.hourStartMs === null ||
452
- limits.writesPerHour === undefined ||
453
- now - bucket.hourStartMs >= limits.writesPerHour.windowMs
454
- );
455
- }
456
-
457
- /**
458
- * Charge one request to `key`'s bucket. `undefined` when admitted; the
459
- * seconds to wait — to the end of the latest window that refused, at least
460
- * 1 — when over budget.
461
- */
462
- function chargeCallerBucket(
463
- buckets: Map<string, CallerBucket>,
464
- key: string,
465
- method: string,
466
- now: number,
467
- limits: CallerLimits,
468
- ): number | undefined {
469
- let bucket = buckets.get(key);
470
- if (!bucket || now - bucket.windowStartMs >= limits.windowMs) {
471
- if (!bucket && buckets.size >= limits.maxBuckets) {
472
- for (const [k, b] of buckets) {
473
- if (isStale(b, now, limits)) buckets.delete(k);
474
- }
475
- if (buckets.size >= limits.maxBuckets) {
476
- const oldest = buckets.keys().next().value;
477
- if (oldest !== undefined) buckets.delete(oldest);
478
- }
479
- }
480
- // Re-insert, so Map order stays oldest-window-first for the eviction
481
- // above. A new minute keeps the hour's start and count.
482
- buckets.delete(key);
483
- bucket = {
484
- windowStartMs: now,
485
- requests: 0,
486
- writes: 0,
487
- hourStartMs: bucket?.hourStartMs ?? null,
488
- hourWrites: bucket?.hourWrites ?? 0,
489
- };
490
- buckets.set(key, bucket);
491
- }
492
- const hour = limits.writesPerHour;
493
- if (
494
- hour !== undefined &&
495
- bucket.hourStartMs !== null &&
496
- now - bucket.hourStartMs >= hour.windowMs
497
- ) {
498
- bucket.hourStartMs = null;
499
- bucket.hourWrites = 0;
500
- }
501
-
502
- const write = isWrite(method);
503
- const requests = bucket.requests + 1;
504
- const writes = bucket.writes + (write ? 1 : 0);
505
- const hourWrites = bucket.hourWrites + (write ? 1 : 0);
506
- const minuteEnd = bucket.windowStartMs + limits.windowMs;
507
- const refusedUntil: number[] = [];
508
- if (requests > limits.requests) refusedUntil.push(minuteEnd);
509
- if (write || !limits.refuseOnlyWrites) {
510
- if (writes > limits.writes) refusedUntil.push(minuteEnd);
511
- if (write && hour !== undefined && hourWrites > hour.limit) {
512
- refusedUntil.push((bucket.hourStartMs ?? now) + hour.windowMs);
513
- }
514
- }
515
-
516
- const refused = refusedUntil.length > 0;
517
- if (!refused || limits.countRefused) {
518
- bucket.requests = requests;
519
- bucket.writes = writes;
520
- if (write && hour !== undefined) {
521
- bucket.hourWrites = hourWrites;
522
- bucket.hourStartMs ??= now;
523
- }
524
- }
525
- if (!refused) return undefined;
526
- return Math.max(1, Math.ceil((Math.max(...refusedUntil) - now) / 1_000));
527
- }
528
-
529
- /**
530
- * The agent path (#3591 D4, D6). The key is never logged and never echoed:
531
- * every refusal is a fixed body.
532
- */
533
- async function authenticateAgent(
534
- request: FastifyRequest,
535
- reply: FastifyReply,
536
- key: string,
537
- now: number,
538
- ): Promise<void> {
539
- const row = await findAgentByKeyHash(hashAgentKey(key)).catch(() => null);
540
- // Unknown, switched off, or its key-holder has left the organization (the
541
- // FK set the column null) — one body for each, so none is an oracle.
542
- if (!row || !row.enabled || !row.keyIssuedToMembershipId) {
543
- reply.code(401).send(UNAUTHORIZED);
544
- return;
545
- }
546
-
547
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5): the agents
548
- // table's format CHECKs guarantee these columns hold well-formed ids.
549
- const agentId = row.id as Id<"agent">;
550
- const personId = row.responsible.userId as Id<"user">;
551
- request.userId = personId;
552
- request.agent = {
553
- agentId,
554
- workspaceId: row.workspaceId as Id<"workspace">,
555
- organizationId: row.organizationId as Id<"organization">,
556
- responsibleMembershipId: row.responsible.id as Id<"membership">,
557
- permissionLevel: row.permissionLevel,
558
- // #7864 D3 — the one place a missing value becomes none; the tenant
559
- // guards keep only the grants its person's role also holds.
560
- grants: row.grants ?? [],
561
- };
562
- bindActor(request, reply, {
563
- type: "agent",
564
- agentId,
565
- onBehalfOfUserId: personId,
566
- });
567
-
568
- const retryAfter = chargeCallerBucket(
569
- agentBuckets,
570
- agentId,
571
- request.method,
572
- now,
573
- AGENT_LIMITS,
574
- );
575
- if (retryAfter !== undefined) {
576
- reply.code(429).header("Retry-After", String(retryAfter)).send(RATE_LIMITED);
577
- return;
578
- }
579
-
580
- // D6 — an agent reaches only routes a tenant guard runs on. Every other
581
- // route in the auth scope (tokens, user data, `/me` reads, the leads union,
582
- // the workspace switcher), and any route added outside a tenant scope
583
- // later, is closed to it by default.
584
- if (request.routeOptions.config?.tenantGuarded !== true) {
585
- reply.code(403).send(FORBIDDEN);
586
- return;
587
- }
588
-
589
- // Best-effort, as the token path bumps lastUsedAt: a failure never 401s.
590
- // Only an admitted request writes, and only when the stamp is a minute old.
591
- if (
592
- !row.lastActiveAt ||
593
- now - row.lastActiveAt.getTime() >= AGENT_ACTIVITY_WRITE_INTERVAL_MS
594
- ) {
595
- touchAgentLastActive(agentId, new Date(now)).catch(() => undefined);
596
- }
597
- }
598
-
599
- /**
600
- * The hub-credential path (#7894 D6). The credential is never logged and
601
- * never echoed: every refusal is a fixed body. (1)-(3) failing is the one 401;
602
- * (4), the route not being on `HUB_SESSION_ROUTES`, is 404 with no handler run;
603
- * (5), the person over budget at this spoke (#8220 D2), is 429 with no
604
- * handler run.
605
- */
606
- async function authenticateHubSession(
607
- request: FastifyRequest,
608
- reply: FastifyReply,
609
- credential: string,
610
- now: number,
611
- ): Promise<void> {
612
- // (1) The spoke key resolves to an unrevoked spoke.
613
- const rawKey = request.headers[SPOKE_KEY_HEADER];
614
- const spokeKey = typeof rawKey === "string" ? rawKey.trim() : "";
615
- const spoke = spokeKey
616
- ? await findSpokeByKeyHash(hashSpokeKey(spokeKey)).catch(() => null)
617
- : null;
618
- if (!spoke || spoke.revokedAt) {
619
- reply.code(401).send(UNAUTHORIZED);
620
- return;
621
- }
622
-
623
- // (2) The credential's grant is live, and its client IS this spoke's.
624
- // (3) Its person's email is verified (read in the same query).
625
- const grant = await findAuthorizationByCredentialHash(
626
- hashBearer(credential),
627
- ).catch(() => null);
628
- if (
629
- !grant ||
630
- grant.revokedAt ||
631
- grant.expiresAt.getTime() <= now ||
632
- !grant.client.enabled ||
633
- grant.client.clientId !== spoke.id ||
634
- grant.user.emailVerified !== true
635
- ) {
636
- reply.code(401).send(UNAUTHORIZED);
637
- return;
638
- }
639
-
640
- // (4) Method and registered URL are on the list.
641
- const route = `${request.method.toUpperCase()} ${request.routeOptions.url ?? ""}`;
642
- if (!HUB_SESSION_ROUTES.includes(route)) {
643
- reply.code(404).send(NOT_FOUND);
644
- return;
645
- }
646
-
647
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5), as above.
648
- request.userId = grant.userId as Id<"user">;
649
- request.hubSession = {
650
- spokeId: spoke.id as Id<"spoke">,
651
- workspaceId: spoke.workspaceId as Id<"workspace">,
652
- };
653
- bindActor(request, reply, { type: "user", userId: request.userId });
654
- // #8220 D10 — every line the request logger writes after admission names
655
- // the spoke that acted: its id, never its key or the credential. (Lines a
656
- // helper writes through the module-level `logger` carry no request binding.)
657
- request.log = request.log.child({ spokeId: spoke.id });
658
- reply.log = request.log;
659
-
660
- // (5) The verified (spoke, person) is within its budget.
661
- const retryAfter = chargeCallerBucket(
662
- hubSessionBuckets,
663
- `${spoke.id}:${grant.userId}`,
664
- request.method,
665
- now,
666
- HUB_SESSION_LIMITS,
667
- );
668
- if (retryAfter !== undefined) {
669
- reply.code(429).header("Retry-After", String(retryAfter)).send(RATE_LIMITED);
670
- return;
671
- }
672
- }
673
-
674
- export type AuthPreHandlerOptions = {
675
- /**
676
- * The browser origins a cookie-authenticated write may come from — exact
677
- * matches only. server.ts passes `config.TRUSTED_ORIGINS` (#7395).
678
- */
679
- trustedOrigins: readonly string[];
680
- };
681
-
682
- /**
683
- * The result of `resolveOptionalCookieUser` — a signed-in user, or the reason
684
- * there is none (#6866 D2).
685
- */
686
- export type OptionalCookieUser =
687
- | { kind: "user"; userId: Id<"user"> }
688
- | {
689
- kind: "none";
690
- reason:
691
- | "no_cookie"
692
- | "no_database"
693
- | "unknown_session"
694
- | "expired"
695
- | "cross_origin"
696
- | "lookup_failed";
697
- };
698
-
699
- /**
700
- * Non-rejecting cookie resolver for routes that stay open to anonymous callers
701
- * but act on a signed-in user when there is one (#6866 D2 — the checkout
702
- * route's buyer stamp).
703
- *
704
- * Applies the cookie path's own rules — the same private cookie reader,
705
- * Session lookup, expiry comparison and trusted-origin check as
706
- * `makeAuthPreHandler` — so the two paths cannot drift. Reads the COOKIE only:
707
- * a bearer `ApiToken` never identifies a user here. Never writes a reply and
708
- * never throws; every failure is a `none` with its reason. The cookie is
709
- * checked first, so an anonymous call on a database-less boot is `no_cookie`.
710
- */
711
- export async function resolveOptionalCookieUser(
712
- request: FastifyRequest,
713
- opts: AuthPreHandlerOptions,
714
- ): Promise<OptionalCookieUser> {
715
- const cookieToken = readSessionCookie(request);
716
- if (!cookieToken) return { kind: "none", reason: "no_cookie" };
717
- if (!process.env.DATABASE_URL) return { kind: "none", reason: "no_database" };
718
-
719
- let session: SessionRow | undefined;
720
- try {
721
- session = await resolveCookieSession(cookieToken);
722
- } catch {
723
- return { kind: "none", reason: "lookup_failed" };
724
- }
725
- if (!session) return { kind: "none", reason: "unknown_session" };
726
- if (session.expiresAt.getTime() <= getClock().now()) {
727
- return { kind: "none", reason: "expired" };
728
- }
729
- if (!isOriginAllowed(request, opts.trustedOrigins)) {
730
- return { kind: "none", reason: "cross_origin" };
731
- }
732
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5), as above.
733
- return { kind: "user", userId: session.userId as Id<"user"> };
734
- }
735
-
736
- /**
737
- * The person a member read answers for (#7877 D5): the hub's own cookie
738
- * session or a hub-issued credential (#7894 D6) — never a personal ApiToken,
739
- * never an agent. `null` → the route answers 401 with `UNAUTHORIZED`.
740
- *
741
- * This is the ONE place the member reads (`services/member-area.ts`) learn
742
- * who is calling: a later credential kind joins them by an edit of this
743
- * function and nothing in `routes/member/`. An agent never gets here — the
744
- * member routes are not tenant-guarded, so this preHandler answers it 403.
745
- */
746
- export function memberCaller(request: FastifyRequest): Id<"user"> | null {
747
- if (request.userId === undefined) return null;
748
- if (request.sessionTenant !== undefined || request.hubSession !== undefined) {
749
- return request.userId;
750
- }
751
- return null;
752
- }
753
-
754
- /**
755
- * Fastify preHandler implementing the dual-mode contract.
756
- *
757
- * Exported standalone so route-test suites can register it directly without
758
- * spinning up the full server module.
759
- */
760
- export function makeAuthPreHandler(opts: AuthPreHandlerOptions) {
761
- const { trustedOrigins } = opts;
762
- return async function authPreHandler(
763
- request: FastifyRequest,
764
- reply: FastifyReply,
765
- ): Promise<void> {
766
- // A database-less dev boot (#6482). First, and read per request: after
767
- // the lookups below, their `.catch` would already have answered 401.
768
- if (!process.env.DATABASE_URL) {
769
- reply.code(503).send(DATABASE_NOT_CONFIGURED);
770
- return;
771
- }
772
-
773
- const now = getClock().now();
774
- const cookieToken = readSessionCookie(request);
775
-
776
- if (cookieToken) {
777
- const session = await resolveCookieSession(cookieToken).catch(
778
- () => undefined,
779
- );
780
- if (session && session.expiresAt.getTime() > now) {
781
- if (!isOriginAllowed(request, trustedOrigins)) {
782
- reply.code(401).send(UNAUTHORIZED);
783
- return;
784
- }
785
- // Brand assertion at the Prisma read edge (ADR-0031 rule 5): the DB
786
- // CHECK constraints guarantee these columns hold well-formed
787
- // prefixed ids, so the compile-time brand is asserted once here.
788
- request.userId = session.userId as Id<"user">;
789
- request.sessionTenant = {
790
- sessionRowId: session.id as Id<"session">,
791
- activeOrganizationId: session.activeOrganizationId as Id<"organization"> | null,
792
- activeWorkspaceId: session.activeWorkspaceId as Id<"workspace"> | null,
793
- };
794
- bindActor(request, reply, { type: "user", userId: request.userId });
795
- return;
796
- }
797
- // Cookie present but invalid / expired → 401 (no silent bearer fallback).
798
- reply.code(401).send(UNAUTHORIZED);
799
- return;
800
- }
801
-
802
- const bearer = readBearer(request);
803
- if (bearer?.startsWith(HUB_SESSION_PREFIX)) {
804
- await authenticateHubSession(request, reply, bearer, now);
805
- return;
806
- }
807
- if (bearer?.startsWith(AGENT_KEY_PREFIX)) {
808
- await authenticateAgent(request, reply, bearer, now);
809
- return;
810
- }
811
- if (bearer) {
812
- const row = await resolveBearerToken(bearer).catch(() => undefined);
813
- if (!row) {
814
- reply.code(401).send(UNAUTHORIZED);
815
- return;
816
- }
817
- if (row.revokedAt) {
818
- reply.code(401).send(UNAUTHORIZED);
819
- return;
820
- }
821
- if (row.expiresAt && row.expiresAt.getTime() <= now) {
822
- reply.code(401).send(UNAUTHORIZED);
823
- return;
824
- }
825
- request.userId = row.userId as Id<"user">;
826
- // A personal token acts as its person, as before (#3591 D9).
827
- bindActor(request, reply, { type: "user", userId: request.userId });
828
- // Best-effort: bump lastUsedAt. Failure here MUST NOT 401 — the caller
829
- // is already authenticated. Swallow the error and continue.
830
- prisma.apiToken
831
- .update({ where: { id: row.id }, data: { lastUsedAt: new Date(now) } })
832
- .catch(() => undefined);
833
- return;
834
- }
835
-
836
- // Neither present.
837
- reply.code(401).send(UNAUTHORIZED);
838
- };
839
- }
840
-
841
- /**
842
- * Plugin form — exposes the preHandler via Fastify's `addHook`. The
843
- * encapsulation override (Symbol.for("skip-override")) lets the hook
844
- * propagate to sibling routes registered in the SAME parent scope after the
845
- * plugin — matching the rate-limit.ts pattern. Without it, the hook would
846
- * only fire for routes registered INSIDE the plugin, which is not the calling
847
- * convention (server.ts registers the plugin first, then sibling routes).
848
- */
849
- export const authPreHandlerPlugin: FastifyPluginCallback<
850
- AuthPreHandlerOptions
851
- > = (fastify, opts, done) => {
852
- const handler = makeAuthPreHandler(opts);
853
- fastify.decorateRequest("userId", undefined);
854
- fastify.decorateRequest("sessionTenant", undefined);
855
- fastify.decorateRequest("agent", undefined);
856
- fastify.decorateRequest("actor", undefined);
857
- fastify.decorateRequest("hubSession", undefined);
858
- fastify.addHook("preHandler", handler);
859
- done();
860
- };
861
-
862
- (authPreHandlerPlugin as FastifyPluginCallback & { [key: symbol]: boolean })[
863
- Symbol.for("skip-override")
864
- ] = true;