create-ailk 0.24.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 (599) hide show
  1. package/README.md +42 -20
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +4 -2
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +31 -3
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +65 -29
  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/module-architecture.d.ts +20 -3
  20. package/dist/module-architecture.js +117 -550
  21. package/dist/paid-sections.d.ts +25 -1
  22. package/dist/paid-sections.js +88 -3
  23. package/dist/surfaces.js +10 -2
  24. package/dist/template-record.d.ts +14 -4
  25. package/dist/template-record.js +23 -5
  26. package/dist/topology.d.ts +16 -3
  27. package/dist/topology.js +31 -19
  28. package/dist/update.d.ts +10 -1
  29. package/dist/update.js +540 -16
  30. package/package.json +3 -3
  31. package/release-scope.json +1 -1
  32. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  33. package/templates/.claude/hooks/template-guard.sh +137 -0
  34. package/templates/.claude/rules/extending.md +106 -0
  35. package/templates/.claude/rules/loop-contract.md +2 -2
  36. package/templates/.claude/settings.json +11 -0
  37. package/templates/.env.example +5 -5
  38. package/templates/.github/workflows/ci.yml +7 -4
  39. package/templates/CLAUDE.md +11 -1
  40. package/templates/CONVENTIONS.md +8 -10
  41. package/templates/README.md +14 -13
  42. package/templates/apps/api/.env.example +2 -2
  43. package/templates/apps/api/CLAUDE.md +102 -76
  44. package/templates/apps/api/jest.config.cjs +15 -5
  45. package/templates/apps/api/package.json +16 -20
  46. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  47. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  48. package/templates/apps/api/src/index.ts +1 -1
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
  50. package/templates/apps/api/src/modules/commerce.ts +10 -0
  51. package/templates/apps/api/src/modules/content-write.ts +10 -0
  52. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  53. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  54. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  55. package/templates/apps/api/src/modules/workspace.ts +10 -0
  56. package/templates/apps/api/src/server.ts +117 -1882
  57. package/templates/apps/api/vercel.json +2 -2
  58. package/templates/apps/mcp/CLAUDE.md +67 -20
  59. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  60. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  61. package/templates/apps/mcp/jest.config.cjs +6 -1
  62. package/templates/apps/mcp/package.json +5 -13
  63. package/templates/apps/mcp/src/app.ts +8 -7
  64. package/templates/apps/mcp/src/index.ts +6 -34
  65. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  66. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  67. package/templates/apps/mcp/src/site.ts +12 -0
  68. package/templates/apps/web/.env.example +1 -1
  69. package/templates/apps/web/CLAUDE.md +10 -3
  70. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  71. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  72. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  73. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
  74. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  75. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  76. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  77. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  78. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  79. package/templates/apps/web/auth.ts +2 -2
  80. package/templates/apps/web/jest.config.cjs +1 -0
  81. package/templates/apps/web/lib/auth-email.ts +1 -1
  82. package/templates/apps/web/lib/relay.ts +2 -2
  83. package/templates/apps/web/lib/release-scope.ts +47 -0
  84. package/templates/apps/web/lib/server-api.ts +1 -1
  85. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  86. package/templates/customizations.yaml +37 -0
  87. package/templates/database/prisma.config.ts +10 -14
  88. package/templates/database/seed-test.ts +2 -2
  89. package/templates/database/seed.ts +5 -5
  90. package/templates/package.json +12 -6
  91. package/templates/pnpm-workspace.yaml +0 -1
  92. package/templates/project.yaml +16 -8
  93. package/templates/tools/jest/reporters.cjs +65 -0
  94. package/templates/tools/lint-architecture.ts +553 -7
  95. package/templates/tsconfig.json +1 -2
  96. package/versions.json +48 -35
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  98. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  99. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  100. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  101. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  102. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  103. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  104. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  105. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  106. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  107. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  108. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  109. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  110. package/templates/apps/api/src/bin/spokes.ts +0 -198
  111. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  112. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  113. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  114. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  115. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  116. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  117. package/templates/apps/api/src/config/index.ts +0 -596
  118. package/templates/apps/api/src/config/modules.ts +0 -83
  119. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  120. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  121. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  122. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  123. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  124. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  125. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  126. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  127. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  128. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  129. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  130. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  131. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  132. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  133. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  134. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  135. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  136. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  137. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  138. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
  139. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  140. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  141. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  142. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  143. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  144. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  145. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  146. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  147. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  148. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  149. package/templates/apps/api/src/lib/errors.ts +0 -95
  150. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  151. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  152. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  153. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  154. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  155. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  156. package/templates/apps/api/src/lib/locales.ts +0 -201
  157. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  158. package/templates/apps/api/src/lib/prisma.ts +0 -21
  159. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  160. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  161. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  162. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  163. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  164. package/templates/apps/api/src/lib/site.ts +0 -257
  165. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  166. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  167. package/templates/apps/api/src/lib/stripe.ts +0 -321
  168. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  169. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  170. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  171. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  172. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  173. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  174. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  175. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  176. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  177. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  178. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  179. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  180. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  181. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  182. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  183. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  184. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  185. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  186. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  187. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  188. package/templates/apps/api/src/middleware/auth.ts +0 -864
  189. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  190. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  191. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
  192. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  193. package/templates/apps/api/src/openapi/index.ts +0 -18
  194. package/templates/apps/api/src/openapi/spec.ts +0 -2148
  195. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  196. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  197. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  198. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  199. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  200. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  201. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  202. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  203. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  204. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  205. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  206. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  207. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  208. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
  209. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  210. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
  211. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  212. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  213. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  214. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  215. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  216. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  217. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  218. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  219. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  220. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  221. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  222. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  223. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  224. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  225. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  226. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  227. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  228. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  229. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  230. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  231. package/templates/apps/api/src/routes/content/get-page.ts +0 -183
  232. package/templates/apps/api/src/routes/content/index.ts +0 -49
  233. package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
  234. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  235. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  236. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  237. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  238. package/templates/apps/api/src/routes/content/search.ts +0 -100
  239. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  240. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  241. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  242. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  243. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  244. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  245. package/templates/apps/api/src/routes/leads/index.ts +0 -639
  246. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  247. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  248. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  249. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  250. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
  251. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
  252. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  253. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  254. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  255. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  256. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  257. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  258. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  259. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  260. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  261. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  262. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  263. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  264. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  265. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  266. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  267. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  268. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  269. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  270. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  271. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  272. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  273. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  274. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
  275. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  276. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  277. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  278. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  279. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  280. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  281. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  282. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  285. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  286. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  287. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
  288. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
  289. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  290. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  291. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  292. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  293. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  294. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  295. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  296. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
  297. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  298. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  299. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  300. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  301. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  302. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  303. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  304. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  305. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  306. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
  307. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
  308. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
  309. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
  310. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  311. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  312. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  313. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  314. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  315. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  316. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  317. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  318. package/templates/apps/api/src/services/abandon-email.ts +0 -336
  319. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  320. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  321. package/templates/apps/api/src/services/audit-log.ts +0 -961
  322. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  323. package/templates/apps/api/src/services/consent.ts +0 -834
  324. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  325. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
  326. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  327. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  328. package/templates/apps/api/src/services/entitlements.ts +0 -338
  329. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  330. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  331. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  332. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  333. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  334. package/templates/apps/api/src/services/listing-config.ts +0 -178
  335. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  336. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
  337. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  338. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
  339. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  340. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
  341. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
  342. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  343. package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
  344. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
  345. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  346. package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
  347. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  348. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  349. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  350. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  351. package/templates/apps/api/src/services/person.ts +0 -1211
  352. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  353. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  354. package/templates/apps/api/src/services/project-listings.ts +0 -2085
  355. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  356. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  357. package/templates/apps/api/src/services/revalidation.ts +0 -235
  358. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  359. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  360. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  361. package/templates/apps/api/src/services/verified-email.ts +0 -71
  362. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  363. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  364. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  365. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  366. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  367. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  368. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  369. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  370. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  371. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  372. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  373. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  374. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  375. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  376. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  377. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  378. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
  379. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  380. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  381. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  382. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
  383. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
  384. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  385. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  386. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
  387. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  388. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
  389. package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
  390. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  391. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  392. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
  393. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
  394. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
  395. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
  396. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  397. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  398. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
  399. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
  400. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  401. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  402. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  403. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  404. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  405. package/templates/apps/mcp/src/abuse/index.ts +0 -349
  406. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  407. package/templates/apps/mcp/src/boot.ts +0 -167
  408. package/templates/apps/mcp/src/config/modules.ts +0 -43
  409. package/templates/apps/mcp/src/http-transport.ts +0 -564
  410. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  411. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  412. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
  413. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
  414. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  415. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  416. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  417. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  418. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  419. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  420. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  421. package/templates/apps/mcp/src/lib/site-config.ts +0 -53
  422. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  423. package/templates/apps/mcp/src/request-context.ts +0 -52
  424. package/templates/apps/mcp/src/server.ts +0 -343
  425. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  426. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  427. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
  428. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  429. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  430. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  431. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  432. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  433. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  434. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  435. package/templates/apps/mcp/src/tools/index.ts +0 -138
  436. package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
  437. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  438. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  439. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  440. package/templates/apps/mcp/src/tools/read_content.ts +0 -81
  441. package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
  442. package/templates/apps/mcp/src/tools/search_content.ts +0 -63
  443. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
  444. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  445. package/templates/database/CHANGELOG.md +0 -981
  446. package/templates/database/LICENSE +0 -202
  447. package/templates/database/NOTICE +0 -2
  448. package/templates/database/README.md +0 -243
  449. package/templates/database/__tests__/agents.db.test.ts +0 -320
  450. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  451. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  452. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  453. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  454. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  455. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  456. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  457. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  458. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  459. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  460. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  461. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  462. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  463. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  464. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  465. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  466. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  467. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  468. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  469. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  470. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  471. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  472. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  473. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  474. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  475. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  476. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  477. package/templates/database/__tests__/person.db.test.ts +0 -586
  478. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  479. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  480. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  481. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  482. package/templates/database/__tests__/seed.test.ts +0 -272
  483. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  484. package/templates/database/__tests__/tenancy.test.ts +0 -141
  485. package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
  486. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  487. package/templates/database/_gitignore +0 -8
  488. package/templates/database/auth/.gitkeep +0 -0
  489. package/templates/database/auth/schema.prisma +0 -641
  490. package/templates/database/config/.gitkeep +0 -0
  491. package/templates/database/config/schema.prisma +0 -441
  492. package/templates/database/content/.gitkeep +0 -0
  493. package/templates/database/content/schema.prisma +0 -339
  494. package/templates/database/eslint.config.mjs +0 -69
  495. package/templates/database/flows/listing-diagnostic.ts +0 -182
  496. package/templates/database/flows/presets.ts +0 -315
  497. package/templates/database/inbox/.gitkeep +0 -0
  498. package/templates/database/inbox/README.md +0 -3
  499. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  500. package/templates/database/inbox/schema.prisma +0 -1447
  501. package/templates/database/jest.config.cjs +0 -36
  502. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  503. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  504. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  505. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  506. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  507. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  508. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  509. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  510. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  511. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  512. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  513. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  514. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  515. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  516. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  517. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  518. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  519. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  520. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  521. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  522. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  523. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  524. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  525. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  526. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  527. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  528. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  529. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  530. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  531. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  532. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  533. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  534. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  535. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  536. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  537. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  538. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  539. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  540. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  541. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  542. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  543. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  544. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  545. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  546. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  547. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  548. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  549. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  550. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  551. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  552. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  553. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  554. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  555. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  556. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  557. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  558. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  559. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  560. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  561. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  562. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  563. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  564. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  565. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  566. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  567. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  568. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  569. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  570. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  571. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  572. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  573. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  574. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  575. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  576. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  577. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  578. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  579. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  580. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  581. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  582. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  583. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  584. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  585. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  586. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
  587. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
  588. package/templates/database/migrations/migration_lock.toml +0 -3
  589. package/templates/database/ops/.gitkeep +0 -0
  590. package/templates/database/ops/schema.prisma +0 -43
  591. package/templates/database/package.json +0 -89
  592. package/templates/database/schema.prisma +0 -71
  593. package/templates/database/scripts/db-generate-locked.sh +0 -80
  594. package/templates/database/scripts/person-backfill.sql +0 -132
  595. package/templates/database/tenancy.ts +0 -183
  596. package/templates/database/tsconfig.build.json +0 -16
  597. package/templates/database/tsconfig.flows.json +0 -15
  598. package/templates/database/tsconfig.json +0 -16
  599. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,294 +0,0 @@
1
- /**
2
- * @file dunning-policy.ts
3
- * @description The per-tenant dunning grace policy (#6034 §§ D4, D5) — one
4
- * function that resolves how many days a `past_due`/`unpaid` subscription
5
- * keeps access before a subsequent `canceled` revokes it, plus the two
6
- * read-side predicates that decide whether a subscriber has lost access or
7
- * merely needs to act.
8
- *
9
- * NOT marked `oss: false` (§ D1) — `routes/webhooks/stripe.ts`, this file's
10
- * only caller, deliberately left `commerce.ownedFiles`
11
- * (`packages/create-ailk/src/module-architecture.ts:1818-1846`) so a keyless
12
- * scaffold still carries and registers it; gating happens at CALL time on the
13
- * commerce claim already settled at `stripe.ts:174` (NFR-3), not at file
14
- * presence.
15
- *
16
- * The policy blob lives in `SiteSetting` (`database/config/schema.prisma:15`)
17
- * — a model with NO reader anywhere in the tree before this file (§ 1.7) and
18
- * a globally-`@unique` `key` column with no `siteId` column of its own. Per-
19
- * tenant scoping therefore lives in the key STRING:
20
- * `commerce.dunning_policy:<siteId>` — never an unsuffixed fallback, which
21
- * would let one site's policy silently govern another's money.
22
- *
23
- * The read never throws (AC-3) — see the resolution table on
24
- * `dunningPolicyFor` below, which mirrors `listing-config.ts`'s
25
- * absent/malformed → documented-default contract for the same reason: an
26
- * anonymous webhook delivery must not 500 because an operator (or nothing at
27
- * all) wrote a bad policy row.
28
- */
29
-
30
- import { logger } from "@working-theory/observability";
31
- import { z } from "zod";
32
-
33
- import { isModuleEnabled } from "../config/modules.js";
34
- import { prisma } from "../lib/prisma.js";
35
- import { resolveWorkspaceIdForAlias } from "../lib/site.js";
36
-
37
- /** The lifecycle values this record persists. A narrower set than
38
- * `SubscriptionLifecycleStatus` (`@working-theory/stripe-client`): `trialing`
39
- * never persists on its own — an arriving `active` OR `trialing` status is
40
- * always rewritten to a recorded `status: "active"` (#6034 § D7's clean-
41
- * recovery contract), so the record's own vocabulary only ever needs three
42
- * members. */
43
- export type DunningRecordedStatus = "past_due" | "active" | "canceled";
44
-
45
- /** One subscription's recorded dunning state (#6034 § D2). */
46
- export interface DunningEntry {
47
- status: DunningRecordedStatus;
48
- accessState: "granted" | "revoked";
49
- /** ISO-8601, or `null` before the subscription has ever failed. Set once,
50
- * on first entry into a failing state, and never moved while it stays
51
- * failing (§ D5) — re-arming on every retry would slide the grace window
52
- * forward with every Stripe attempt and it would never expire. */
53
- dunningArmedAt: string | null;
54
- /** ISO-8601, or `null` while nothing has scheduled a revocation. Present
55
- * (possibly in the future) the instant a `canceled` event is recorded —
56
- * the read-side lazily evaluates it rather than any scheduler waking up to
57
- * flip the row (§ D5 — NFR-7 forbids one). */
58
- accessRevokesAt: string | null;
59
- /** Stripe `event.created`, seconds — the ordering watermark § D6's
60
- * admission rule compares incoming events against. */
61
- lastEventAt: number;
62
- /** The durable twin of the in-process `hasSeen` map (§ D6 rule 1) — a
63
- * replay of the SAME event id is a no-op even after a process restart. */
64
- lastEventId: string;
65
- }
66
-
67
- /** The whole `UserData` blob for the reserved
68
- * `commerce.subscription_dunning` key — one row per subscriber, keyed by
69
- * Stripe subscription id because one customer can hold more than one
70
- * subscription (§ D2 — the map is what stops a second subscription's
71
- * `active` clobbering the first's `past_due`). */
72
- export interface DunningRecord {
73
- v: 1;
74
- subscriptions: Record<string, DunningEntry>;
75
- }
76
-
77
- /** The reserved `UserData` key this record lives at (§ D12 — refused on
78
- * `GET`/`PUT`/`DELETE /v1/user/data/:key`; the subject of the access
79
- * decision may not read, write or erase it). */
80
- export const DUNNING_USER_DATA_KEY = "commerce.subscription_dunning";
81
-
82
- /** The map is capped so a per-event read-and-rewrite of the whole blob
83
- * doesn't grow with a customer's entire subscription history (§ D2, § 9
84
- * SEC-08). Eviction is oldest-first by `lastEventAt`, skipping any entry
85
- * that is not yet terminal-and-settled (`status: "active"` with
86
- * `accessState: "granted"`) — an entry still being dunned is never evicted
87
- * out from under an in-flight decision. */
88
- export const DUNNING_MAX_SUBSCRIPTIONS = 50;
89
-
90
- export interface DunningPolicy {
91
- graceDays: number;
92
- }
93
-
94
- /** 14 days — chosen because Stripe's Smart Retries schedule runs up to
95
- * roughly three weeks and the grace clock starts at the FIRST failure, not
96
- * at cancellation (§ D5). A shorter default would have the platform cut a
97
- * buyer off while Stripe is still actively recovering the payment. */
98
- export const DUNNING_POLICY_DEFAULTS: DunningPolicy = { graceDays: 14 };
99
-
100
- /** A tenant may not configure a grace window longer than this. Clamped, not
101
- * rejected — an operator's out-of-range number is a mistake to correct, not
102
- * grounds to fall back to a platform default that may be MORE permissive
103
- * than what they wrote (`listing-config.ts:55-62`'s disposition). */
104
- export const DUNNING_GRACE_DAYS_MAX = 90;
105
-
106
- /** Validated shape of the `SiteSetting.value` blob. Accepts any object with
107
- * unknown extra keys (`.passthrough()`) so the "is this a JSON object at
108
- * all" check is separated from the fine-grained `graceDays` validation below
109
- * — zod v4's own `z.number()` already refuses `NaN`/`Infinity` outright,
110
- * which would collapse the resolution table's distinct `malformed` and
111
- * `non_finite` outcomes into one, so `graceDays` is read as `unknown` here
112
- * and narrowed by hand. Deliberately NOT promoted to
113
- * `@working-theory/validation` (§ D4) — one consumer, no cross-package
114
- * reader; promoting it now would widen the published surface for nothing. */
115
- const DunningPolicyBlobSchema = z.object({}).passthrough();
116
-
117
- /** Logged once per process, the first time a `multi-tenant` deployment reads
118
- * the policy (§ D4) — an operator should learn that one grace window governs
119
- * every seller from a log line, not from a wrongful revocation. Process
120
- * restart re-arms it, mirroring `enrichment-shim.ts`'s pro-sentinel flag. */
121
- let deploymentWidePolicyWarned = false;
122
-
123
- /**
124
- * Resolve `siteId`'s dunning grace policy. Never throws (AC-3) — every
125
- * branch below resolves to a value, logged at the level the § D4 resolution
126
- * table specifies:
127
- *
128
- * no row → default, info "no_policy_row"
129
- * not a JSON object → default, error "malformed"
130
- * `graceDays` absent / wrong type → default, error "malformed"
131
- * `graceDays` non-finite (e.g. `1e999`) → default, error "non_finite"
132
- * `graceDays` negative and finite → clamp 0, error "clamped_low"
133
- * `graceDays` finite and > the ceiling → clamp max, error "clamped_high"
134
- * `graceDays` non-integer (e.g. `7.5`) → floored, warn "floored"
135
- * the query itself throws → default, error "read_failed"
136
- * a valid integer in `[0, DUNNING_GRACE_DAYS_MAX]` → that value, no log
137
- *
138
- * `siteId` is a PARAMETER, never read via `currentSiteId()` inside this
139
- * function (§ D4) — a later per-seller resolution is then a caller change
140
- * only, no signature churn.
141
- */
142
- export async function dunningPolicyFor(siteId: string): Promise<DunningPolicy> {
143
- // #6374 D3 — the key is built from the UNRESOLVED `siteId` parameter, and
144
- // that is deliberate, not an oversight. `site_settings` never had a
145
- // `site_id` column, so the site alias was embedded in the key STRING
146
- // (`commerce.dunning_policy:default`); the migration's backfill leaves
147
- // every `key` verbatim. Rebuilding it from the RESOLVED workspace id would
148
- // match no row and hand every seller the built-in default policy —
149
- // silently, which is the failure mode this whole function's log table
150
- // exists to make visible.
151
- const key = `commerce.dunning_policy:${siteId}`;
152
-
153
- if (isModuleEnabled("multi-tenant") && !deploymentWidePolicyWarned) {
154
- deploymentWidePolicyWarned = true;
155
- logger.warn(
156
- { key, outcome: "deployment_wide_policy" },
157
- "[dunning-policy] this deployment's grace policy is shared by every seller — the multi-tenant module is enabled but the policy key carries no per-seller resolution yet",
158
- );
159
- }
160
-
161
- // #6374 D3 — `site_settings` is now keyed by (workspace_id, key): `key`
162
- // alone is no longer unique, because two sellers may each hold a
163
- // `feature_flags` row. The WORKSPACE half is resolved from the alias here;
164
- // the KEY half keeps its embedded alias (see the note above). An alias
165
- // that resolves to nothing takes the same branch a missing row already
166
- // takes — the built-in default policy — rather than throwing on a
167
- // payment-webhook path.
168
- const workspaceId = await resolveWorkspaceIdForAlias(siteId);
169
- if (workspaceId === null) {
170
- logger.info(
171
- { siteId, key, outcome: "no_policy_row" },
172
- "[dunning-policy] site alias resolves to no workspace — using the platform default",
173
- );
174
- return DUNNING_POLICY_DEFAULTS;
175
- }
176
-
177
- let row: { value: unknown } | null;
178
- try {
179
- row = await prisma.siteSetting.findUnique({
180
- where: { workspaceId_key: { workspaceId, key } },
181
- select: { value: true },
182
- });
183
- } catch (err) {
184
- logger.error(
185
- {
186
- siteId,
187
- key,
188
- outcome: "read_failed",
189
- errName: err instanceof Error ? err.name : "UnknownError",
190
- },
191
- "[dunning-policy] policy read failed — using the platform default",
192
- );
193
- return DUNNING_POLICY_DEFAULTS;
194
- }
195
-
196
- if (!row) {
197
- logger.info(
198
- { siteId, key, outcome: "no_policy_row" },
199
- "[dunning-policy] no policy row — using the platform default",
200
- );
201
- return DUNNING_POLICY_DEFAULTS;
202
- }
203
-
204
- const parsed = DunningPolicyBlobSchema.safeParse(row.value);
205
- if (!parsed.success) {
206
- logger.error(
207
- { siteId, key, outcome: "malformed" },
208
- "[dunning-policy] stored policy is not a JSON object — using the platform default",
209
- );
210
- return DUNNING_POLICY_DEFAULTS;
211
- }
212
-
213
- const graceDaysRaw: unknown = (parsed.data as Record<string, unknown>)[
214
- "graceDays"
215
- ];
216
- if (typeof graceDaysRaw !== "number") {
217
- logger.error(
218
- { siteId, key, outcome: "malformed" },
219
- "[dunning-policy] stored graceDays is missing or not a number — using the platform default",
220
- );
221
- return DUNNING_POLICY_DEFAULTS;
222
- }
223
-
224
- // `Number.isFinite` covers both the reachable `1e999 → Infinity` overflow
225
- // AND `NaN`, which cannot arrive from `jsonb` in the first place (there is
226
- // no `NaN` literal in JSON) — one guard, both cases (§ 9 SEC-10).
227
- if (!Number.isFinite(graceDaysRaw)) {
228
- logger.error(
229
- { siteId, key, outcome: "non_finite" },
230
- "[dunning-policy] stored graceDays is not finite — using the platform default",
231
- );
232
- return DUNNING_POLICY_DEFAULTS;
233
- }
234
-
235
- if (graceDaysRaw < 0) {
236
- logger.error(
237
- { siteId, key, outcome: "clamped_low", stored: graceDaysRaw },
238
- "[dunning-policy] stored graceDays is negative — clamped to 0",
239
- );
240
- return { graceDays: 0 };
241
- }
242
-
243
- if (graceDaysRaw > DUNNING_GRACE_DAYS_MAX) {
244
- logger.error(
245
- { siteId, key, outcome: "clamped_high", stored: graceDaysRaw },
246
- "[dunning-policy] stored graceDays exceeds the ceiling — clamped",
247
- );
248
- return { graceDays: DUNNING_GRACE_DAYS_MAX };
249
- }
250
-
251
- if (!Number.isInteger(graceDaysRaw)) {
252
- const floored = Math.floor(graceDaysRaw);
253
- logger.warn(
254
- { siteId, key, outcome: "floored", stored: graceDaysRaw },
255
- "[dunning-policy] stored graceDays is not an integer — floored",
256
- );
257
- return { graceDays: floored };
258
- }
259
-
260
- return { graceDays: graceDaysRaw };
261
- }
262
-
263
- /**
264
- * Access is gone for this subscription: either already stamped `revoked`, or
265
- * the lazily-evaluated `accessRevokesAt` instant has passed. No scheduler
266
- * flips this — every reader evaluates it fresh (§ D5, NFR-7).
267
- */
268
- export function isSubscriberRevoked(
269
- entry: DunningEntry,
270
- now: number = Date.now(),
271
- ): boolean {
272
- return (
273
- entry.accessState === "revoked" ||
274
- (entry.accessRevokesAt !== null && now >= Date.parse(entry.accessRevokesAt))
275
- );
276
- }
277
-
278
- /**
279
- * The buyer must act — #6038's banner predicate (§ D5), exported so #6038
280
- * CALLS it rather than re-deriving it. Deliberately WIDER than
281
- * `status === "past_due"`: a subscription cancelled with its grace window
282
- * still open carries `status: "canceled"`, `accessState: "granted"` — a
283
- * buyer about to lose access who has not been told. Both cases are covered
284
- * here; neither alone is (§ 9 SEC-09).
285
- */
286
- export function isSubscriberActionRequired(
287
- entry: DunningEntry,
288
- now: number = Date.now(),
289
- ): boolean {
290
- return (
291
- !isSubscriberRevoked(entry, now) &&
292
- (entry.status === "past_due" || entry.accessRevokesAt !== null)
293
- );
294
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * @file enrichment-shim.ts
3
- * @description License-gated lead enrichment stub.
4
- *
5
- * Today: checkLicense()-gated branch — log a deduped sentinel for pro tier, return
6
- * lead unchanged. Phase 3: real enrichment replaces this file; the call site
7
- * (B6 lead-routing pipeline) stays stable.
8
- *
9
- * ADR 0008: license key is never logged raw — only the SHA-256 hash prefix
10
- * (first 8 chars) appears in log output.
11
- * ADR 0009 §D3: service-layer placement at apps/api/src/services/.
12
- * ADR 0014: license is a JWT verified locally; tier read via `result.tier`.
13
- */
14
-
15
- import { createHash } from "crypto";
16
-
17
- import { checkLicense } from "@working-theory/license";
18
- import { logger } from "@working-theory/observability";
19
- // TODO(#214): swap to canonical Lead from @working-theory/validation when B1 lands.
20
- import type { CreateLead as Lead } from "@working-theory/validation";
21
-
22
- // Routes the once-per-process Pro sentinel through @working-theory/observability so it
23
- // shares the redact + entropy pipeline (S17). The signature stays unchanged.
24
-
25
- type EnrichedLead = Lead & { _enrichedAt?: string }; // type union shape; today equals Lead
26
-
27
- // Module-scoped sentinel flag — one log per process lifecycle. Process restart re-arms it.
28
- let proSentinelLogged = false;
29
-
30
- function hashPrefix(key: string): string {
31
- return createHash("sha256").update(key).digest("hex").substring(0, 8);
32
- }
33
-
34
- export async function enrichLead(lead: Lead): Promise<EnrichedLead | Lead> {
35
- let result;
36
- try {
37
- result = await checkLicense();
38
- } catch {
39
- return lead;
40
- }
41
-
42
- if (result.tier === "pro" && !proSentinelLogged) {
43
- const key = process.env["AILK_LICENSE_KEY"];
44
- const prefix = key ? hashPrefix(key) : "no-key";
45
- logger.info(
46
- { licensePrefix: prefix },
47
- "[enrichment-shim] Pro server processing not yet operational — falling back to OSS path",
48
- );
49
- proSentinelLogged = true;
50
- }
51
-
52
- return lead;
53
- }
@@ -1,338 +0,0 @@
1
- /**
2
- * Per-Organization entitlement resolver (org-scoped billing, #3601).
3
- *
4
- * The TENANT-layer entitlement answer: "what is this Organization entitled to?"
5
- * — distinct from the DEPLOYMENT/operator license layer (`require-paid-tier` →
6
- * `checkLicense()`, "does this deployment hold the paid module"). The two
7
- * layers compose; neither is folded into the other (spec D6).
8
- *
9
- * ADR-0009 D3: this is cross-namespace service-layer assembly (it reads the
10
- * auth-namespace Organization billing record), so it lives in
11
- * `apps/api/src/services/` — NOT in `@working-theory/license`, which stays
12
- * JWT-pure / DB-free (only the `Entitlements` TYPE lives there; spec D4).
13
- *
14
- * Keyed on the Organization; members inherit (no per-user billing read). This
15
- * foundation fills `features` + `limits.static` (tier + seat count) and returns
16
- * `limits.metered` as the reserved empty shape FEAT-047 (#3679) fills — this
17
- * spec builds NO metering engine (spec D5).
18
- *
19
- * #9067 D5 — the Billing tab's plan card also reads here: the licence expiry,
20
- * the Organization's one open plan Checkout, and the customer's billing
21
- * summary. That summary is the one Stripe read this resolver makes, through
22
- * `lib/stripe.ts`.
23
- */
24
- import { checkLicense, type Entitlements } from "@working-theory/license";
25
- import { logger } from "@working-theory/observability";
26
- import {
27
- MAGNET_KIND_MIN_RANK,
28
- orgBillingDetailsSchema,
29
- planRank,
30
- type FlowConfig,
31
- type OrgBillingDetails,
32
- } from "@working-theory/validation";
33
-
34
- import { prisma } from "../lib/prisma.js";
35
- import { readCustomerBillingSummary } from "../lib/stripe.js";
36
-
37
- /**
38
- * Feature flags unlocked per tier. Free unlocks none; every paid tier unlocks
39
- * the Pro v1 feature set (mirrors `ProFeature` in `@working-theory/license`).
40
- * A flat projection, matching AILK's flat billing shape (spec D1) — the richer
41
- * per-plan allowance model is FEAT-047's, not this foundation's.
42
- */
43
- const PAID_FEATURES: readonly string[] = [
44
- "lead-destinations",
45
- "lead-provenance",
46
- "schema-pack-registry",
47
- "onboard-site",
48
- ];
49
-
50
- function featuresForTier(tier: string): readonly string[] {
51
- return tier === "free" ? [] : PAID_FEATURES;
52
- }
53
-
54
- /**
55
- * Resolve the entitlements for one Organization. An unknown / un-billed org
56
- * (no row, or defaulted columns) resolves to the free-tier baseline
57
- * (`seats: 1`, `tier: "free"`, no features) — safe-by-default.
58
- *
59
- * `limits.metered` is returned as the reserved empty `MeteredLimits` (`{}`);
60
- * FEAT-047 (#3679) fills it when it ports the metering engine.
61
- */
62
- export async function resolveEntitlements(
63
- organizationId: string,
64
- ): Promise<Entitlements> {
65
- const org = await prisma.organization.findUnique({
66
- where: { id: organizationId },
67
- select: { tier: true, seatCount: true },
68
- });
69
-
70
- const tier = org?.tier ?? "free";
71
- const seats = org?.seatCount ?? 1;
72
-
73
- return {
74
- features: featuresForTier(tier),
75
- limits: {
76
- static: { seats, tier },
77
- // RESERVED — FEAT-047 (#3679) fills the metered side; empty here.
78
- metered: {},
79
- },
80
- };
81
- }
82
-
83
- /**
84
- * #6287 — the org's plan RANK (never the deployment-wide `checkLicense()`),
85
- * for the magnet routes' server-side plan gate. Composes `resolveEntitlements`
86
- * above + `planRank` (`@working-theory/validation`, the same table the
87
- * Admin's own UI-only gate reads) and NEVER throws: any failure (a DB
88
- * error, or a tier string `planRank` doesn't recognize) collapses to
89
- * `undefined` — the caller's fail-closed signal for a gated kind (spec D
90
- * "the plan-rank resolver never throws").
91
- */
92
- export async function resolveOrgPlanRank(
93
- organizationId: string,
94
- ): Promise<number | undefined> {
95
- try {
96
- const entitlements = await resolveEntitlements(organizationId);
97
- return planRank(entitlements.limits.static.tier);
98
- } catch {
99
- return undefined;
100
- }
101
- }
102
-
103
- /**
104
- * #6305 — the minimum plan rank a GATED-SHAPE config requires, independent
105
- * of the `kind` label the caller chose. Derived from `MAGNET_KIND_MIN_RANK`
106
- * (never hand-duplicated) as the lowest non-zero rank in that table: today
107
- * that is the same Pro-tier rank every gated kind already requires, so a
108
- * gated-shape config on an ungated kind is refused exactly as a gated-kind
109
- * refusal would be (#6305 AC-1).
110
- */
111
- export const GATED_FLOW_SHAPE_MIN_RANK = Math.min(
112
- ...Object.values(MAGNET_KIND_MIN_RANK).filter((rank) => rank > 0),
113
- );
114
-
115
- /**
116
- * #6305 — whether a `FlowConfig` carries the shape the flow runtime actually
117
- * branches on for gated (recommending-mode) behavior, regardless of which
118
- * `kind` label the row carries. This is the SAME predicate family
119
- * `deriveFlowKind`-style logic elsewhere in this codebase reads
120
- * (`mode === "recommending"`, or the presence of `deliverables` / `offers` /
121
- * `scoring`) — the label and the capability it confers can no longer diverge.
122
- */
123
- export function configHasGatedShape(config: FlowConfig | undefined): boolean {
124
- if (!config) return false;
125
- return (
126
- config.mode === "recommending" ||
127
- config.deliverables !== undefined ||
128
- config.offers !== undefined ||
129
- config.scoring !== undefined
130
- );
131
- }
132
-
133
- /**
134
- * The org's Stripe billing-customer identity (#3701 — the apps/api
135
- * predecessor to the org-billing UI, #3697).
136
- *
137
- * `billingCustomerId` is the RAW Stripe customer id — a server-side-only
138
- * value. Callers (the `/v1/billing` route family) must never return it to the
139
- * client: the read route projects it to a boolean `hasBillingCustomer`
140
- * presence flag (spec D2), and the portal route uses it only as an input to
141
- * `createBillingPortalSession`, never in a response body or log line.
142
- */
143
- export interface OrgBillingIdentity {
144
- /** Raw Stripe customer id (`cus_xxx`), or `null` when the org has none yet. */
145
- billingCustomerId: string | null;
146
- }
147
-
148
- /**
149
- * Reads the org's Stripe billing-customer id by explicit organization id.
150
- * Touches only the `Organization.billingCustomerId` field — the sanctioned
151
- * tenant-scope exception for this file (architecture.yaml § Tenant-scope
152
- * boundary, #3601): no un-scoped tenant/content enumeration.
153
- *
154
- * An unknown org resolves to `{ billingCustomerId: null }` — safe-by-default,
155
- * mirroring `resolveEntitlements`'s un-billed-org handling.
156
- */
157
- export async function getOrgBillingIdentity(
158
- organizationId: string,
159
- ): Promise<OrgBillingIdentity> {
160
- const org = await prisma.organization.findUnique({
161
- where: { id: organizationId },
162
- select: { billingCustomerId: true },
163
- });
164
-
165
- return { billingCustomerId: org?.billingCustomerId ?? null };
166
- }
167
-
168
- /**
169
- * Whether ANY Organization row carries the given Stripe customer id (#6038
170
- * D3.2, closes SA-4). Touches only `Organization.billingCustomerId` by
171
- * explicit value — the same sanctioned-exception shape as
172
- * `getOrgBillingIdentity` above (by id) and `resolveEntitlements` (by id),
173
- * never an un-scoped tenant/content query.
174
- *
175
- * Exists because no code in this repo stamps `metadata.organizationId` onto
176
- * a Stripe customer, so every `customer.created` takes the `User` branch
177
- * (`webhooks/stripe.ts`'s `upsertBillingIdentity`) and a single `cus_…` can
178
- * end up projected onto BOTH a `User` row and an `Organization` row. A
179
- * caller resolving "the caller's own portal" from `User.billingCustomerId`
180
- * alone would then risk opening the ORGANIZATION's billing record instead —
181
- * this is the collision check that refuses that case.
182
- */
183
- export async function isBillingCustomerAnOrganization(
184
- billingCustomerId: string,
185
- ): Promise<boolean> {
186
- const org = await prisma.organization.findFirst({
187
- where: { billingCustomerId },
188
- select: { id: true },
189
- });
190
- return org !== null;
191
- }
192
-
193
- /**
194
- * #9067 D5 — the deployment licence's expiry, as an ISO string, or `null`
195
- * for a keyless, expired or invalid licence. The trial state's date
196
- * (C2). Never throws: a failed read is `null`.
197
- */
198
- export async function resolveLicenseExpiresAt(): Promise<string | null> {
199
- try {
200
- const license = await checkLicense();
201
- return license.expiresAt?.toISOString() ?? null;
202
- } catch {
203
- return null;
204
- }
205
- }
206
-
207
- /**
208
- * #9067 D13 — the one plan Checkout this Organization has open, or `null`.
209
- * Server-side only: the session id is never served. An unknown
210
- * Organization reads `null`.
211
- */
212
- export async function readOrgPlanCheckoutSession(
213
- organizationId: string,
214
- ): Promise<string | null> {
215
- const org = await prisma.organization.findUnique({
216
- where: { id: organizationId },
217
- select: { planCheckoutSessionId: true },
218
- });
219
- return org?.planCheckoutSessionId ?? null;
220
- }
221
-
222
- /**
223
- * #9067 D3 step 9b — record the plan Checkout the route just opened, as one
224
- * compare-and-set: only while the column still holds `expected` and no
225
- * customer is bound. Answers whether one row moved; throws on a database
226
- * error.
227
- */
228
- export async function setOrgPlanCheckoutSession(
229
- organizationId: string,
230
- expected: string | null,
231
- next: string,
232
- ): Promise<boolean> {
233
- const { count } = await prisma.organization.updateMany({
234
- where: { id: organizationId, planCheckoutSessionId: expected, billingCustomerId: null },
235
- data: { planCheckoutSessionId: next },
236
- });
237
- return count === 1;
238
- }
239
-
240
- /**
241
- * #9067 7b (code review) — bind the Organization to the customer a COMPLETED
242
- * plan Checkout created, when the webhook's bind never landed (a lost event,
243
- * a database error the webhook swallowed). The same guarded compare-and-set
244
- * the webhook runs: only while the column still holds `sessionId` and no
245
- * customer is bound, and it clears the session. Answers whether one row
246
- * moved; throws on a database error.
247
- */
248
- export async function bindOrgPlanCheckout(
249
- organizationId: string,
250
- sessionId: string,
251
- customerId: string,
252
- ): Promise<boolean> {
253
- const { count } = await prisma.organization.updateMany({
254
- where: { id: organizationId, planCheckoutSessionId: sessionId, billingCustomerId: null },
255
- data: { billingProvider: "stripe", billingCustomerId: customerId, planCheckoutSessionId: null },
256
- });
257
- return count === 1;
258
- }
259
-
260
- /** How long one customer's summary is reused, a failure included (#9067 NFR-1). */
261
- export const ORG_BILLING_SUMMARY_TTL_MS = 60_000;
262
- /** The most customers held at once; the oldest entry is evicted at the cap. */
263
- export const ORG_BILLING_SUMMARY_CACHE_MAX = 2_000;
264
- /** The longest one summary read may take before the keys are left absent (NFR-2). */
265
- export const ORG_BILLING_SUMMARY_TIMEOUT_MS = 3_000;
266
-
267
- type OrgBillingSummaryKeys = Partial<Pick<OrgBillingDetails, "renewsAt" | "sites" | "paymentMethod">>;
268
-
269
- /** Per customer: the settled keys and when they expire. Insertion order is age. */
270
- const orgBillingSummaryCache = new Map<string, { keys: OrgBillingSummaryKeys; expiresAt: number }>();
271
- /** Per customer: the one read in flight, joined by every concurrent miss. */
272
- const orgBillingSummaryInFlight = new Map<string, Promise<OrgBillingSummaryKeys>>();
273
-
274
- /**
275
- * #9067 D5 — the Organization customer's renewal date, site count and card,
276
- * from the cache when fresh, else one read shared by every concurrent caller
277
- * of the same customer. The member read's wrapper (`services/subscription.ts`),
278
- * rebuilt here because importing it would be a cycle. A throw or a timeout
279
- * answers `{}` (every key absent) and is cached like a success; a value
280
- * outside `orgBillingDetailsSchema`'s bounds is `null`.
281
- */
282
- export async function readOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
283
- const cached = orgBillingSummaryCache.get(customerId);
284
- if (cached && cached.expiresAt > Date.now()) return cached.keys;
285
- const pending = orgBillingSummaryInFlight.get(customerId);
286
- if (pending) return pending;
287
-
288
- const read = settleOrgBillingSummary(customerId).then((keys) => {
289
- orgBillingSummaryCache.delete(customerId);
290
- if (orgBillingSummaryCache.size >= ORG_BILLING_SUMMARY_CACHE_MAX) {
291
- const oldest = orgBillingSummaryCache.keys().next().value;
292
- if (oldest !== undefined) orgBillingSummaryCache.delete(oldest);
293
- }
294
- orgBillingSummaryCache.set(customerId, {
295
- keys,
296
- expiresAt: Date.now() + ORG_BILLING_SUMMARY_TTL_MS,
297
- });
298
- return keys;
299
- });
300
- // Cleared however the read ends, so a rejection is never served forever.
301
- void read.finally(() => orgBillingSummaryInFlight.delete(customerId)).catch(() => undefined);
302
- orgBillingSummaryInFlight.set(customerId, read);
303
- return read;
304
- }
305
-
306
- async function settleOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
307
- let timer: ReturnType<typeof setTimeout> | undefined;
308
- try {
309
- const timedOut = new Promise<"timeout">((resolve) => {
310
- timer = setTimeout(() => resolve("timeout"), ORG_BILLING_SUMMARY_TIMEOUT_MS);
311
- });
312
- const summary = await Promise.race([readCustomerBillingSummary(customerId), timedOut]);
313
- if (summary === "timeout") throw new Error("timeout");
314
- const { shape } = orgBillingDetailsSchema;
315
- const renewsAt = shape.renewsAt.safeParse(summary.renewsAt);
316
- const sites = shape.sites.safeParse(summary.sites);
317
- const paymentMethod = shape.paymentMethod.safeParse(summary.paymentMethod);
318
- return {
319
- renewsAt: renewsAt.success ? (renewsAt.data ?? null) : null,
320
- sites: sites.success ? (sites.data ?? null) : null,
321
- paymentMethod: paymentMethod.success ? (paymentMethod.data ?? null) : null,
322
- };
323
- } catch {
324
- logger.warn(
325
- { route: "billing", outcome: "summary_unavailable" },
326
- "[billing] organization billing summary unavailable",
327
- );
328
- return {};
329
- } finally {
330
- clearTimeout(timer);
331
- }
332
- }
333
-
334
- /** Test seam: forget every cached and in-flight Organization summary. */
335
- export function __resetOrgBillingSummaryCache(): void {
336
- orgBillingSummaryCache.clear();
337
- orgBillingSummaryInFlight.clear();
338
- }