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,715 +0,0 @@
1
- /**
2
- * @file fence-fan-out.db.test.ts
3
- *
4
- * LIVE-DATABASE proof of the #6374 fence fan-out constraints (AC-4, AC-5).
5
- * Named after — and modelled line-for-line on — its sibling
6
- * `composite-fence.db.test.ts`, which proves the same shape for the #6254
7
- * keystone's four already-keyed models. Requires DATABASE_URL pointing at a
8
- * Postgres with the migrations applied (`prisma migrate deploy`); the whole
9
- * suite SKIPS when DATABASE_URL is unset, matching every other `.db.test.ts`
10
- * here and keeping the workspace Jest run DB-free.
11
- *
12
- * Raw SQL through `pg`, for the same reason those files give: the generated
13
- * Prisma client is ESM-only and cannot load under this package's CJS ts-jest
14
- * transform.
15
- *
16
- * What it proves (spec § 6 AC-4 / AC-5):
17
- *
18
- * AC-4, for EACH of the eleven `site_id` tables the amendment fences:
19
- * 1. an insert with `workspace_id` NULL is rejected — 23502;
20
- * 2. an insert whose `workspace_id` differs from its `site_id` is
21
- * rejected BY `<table>_workspace_site_agree` — 23514, and the
22
- * constraint is named in the assertion, so a row rejected by some
23
- * OTHER constraint cannot pass this test;
24
- * 3. an insert where the two AGREE succeeds — without this, assertion 2
25
- * would also pass against a table that rejects everything;
26
- * 4. no row carries `site_id = 'default'` after the migration — the
27
- * normalization (D1 step 2) actually ran.
28
- *
29
- * AC-5, the four shapes that are NOT the eleven-table pattern:
30
- * 5. `site_settings` is keyed by (workspace_id, key), not by key alone
31
- * (D3): two workspaces may each hold `feature_flags`; a SECOND one in
32
- * ONE workspace is rejected — 23505.
33
- * 6. `listing_csv_sends` rejects a NULL `workspace_id` (D2) — it has no
34
- * `site_id`, so it takes NOT NULL + the format CHECK and no equality
35
- * CHECK.
36
- * 7. `deliverable_grants` (D4) stays NULL-TOLERANT — NULL is the serve
37
- * path's fail-closed sentinel — while a STAMPED grant may never
38
- * disagree with its site.
39
- * 8. Deleting a `workspaces` row leaves that workspace's `consent_grants`
40
- * rows in place: the fence is a CHECK, never a foreign key (NFR1,
41
- * ADR-0009 invariant #1). A cascade here would be silent data loss.
42
- */
43
- import { randomUUID } from "node:crypto";
44
- import { readFileSync } from "node:fs";
45
- import { join } from "node:path";
46
-
47
- import { Client } from "pg";
48
-
49
- /**
50
- * The SHIPPED migration text. AC-14's two cases execute the guard block out of
51
- * this file rather than a re-typed copy, so an edit to the guard that changed
52
- * its classification would fail here — the same technique
53
- * `composite-fence.db.test.ts` uses to pin the sentinel literal.
54
- */
55
- const MIGRATION_SQL = readFileSync(
56
- join(__dirname, "..", "migrations", "20260922120000_tenancy_fence", "migration.sql"),
57
- "utf-8",
58
- );
59
-
60
- /**
61
- * The WHOLE `site_settings` attribution sequence (AC-14), extracted from the
62
- * shipped migration: the abort-not-guess guard, then the sentinel UPDATE,
63
- * then the post-backfill assertion — the migration's first, second and third
64
- * statements after the marker, in order.
65
- *
66
- * Slicing through the SECOND `END $$;` rather than the first is what makes the
67
- * non-abort cases meaningful: the guard alone only proves a row was not
68
- * REJECTED, while the guard-plus-UPDATE proves it was actually ATTRIBUTED, and
69
- * to which workspace. A slice that stopped at the first block would pass
70
- * identically whether the UPDATE backfilled the sentinel or did nothing at all.
71
- */
72
- function siteSettingsGuardSql(): string {
73
- const marker = "-- ── ABORT, NEVER GUESS";
74
- const start = MIGRATION_SQL.indexOf(marker);
75
- if (start < 0) throw new Error("AC-14 guard block not found in migration.sql");
76
- const doStart = MIGRATION_SQL.indexOf("DO $$", start);
77
- if (doStart < 0) throw new Error("AC-14 guard DO block malformed");
78
- const END = "END $$;";
79
- // The guard's own terminator, then the post-backfill assertion's.
80
- const firstEnd = MIGRATION_SQL.indexOf(END, doStart);
81
- const secondEnd = firstEnd < 0 ? -1 : MIGRATION_SQL.indexOf(END, firstEnd + END.length);
82
- if (firstEnd < 0 || secondEnd < 0) {
83
- throw new Error("AC-14 slice malformed — expected two DO blocks after the marker");
84
- }
85
- const sql = MIGRATION_SQL.slice(doStart, secondEnd + END.length);
86
- // Fail loudly if the migration is reshaped so the slice no longer spans the
87
- // UPDATE — otherwise the non-abort cases would silently stop proving it.
88
- if (!sql.includes('UPDATE "site_settings"')) {
89
- throw new Error("AC-14 slice no longer spans the sentinel UPDATE");
90
- }
91
- return sql;
92
- }
93
-
94
- /** Postgres's own error shape carries `code` as a 5-character SQLSTATE. */
95
- type PgError = { code?: string; constraint?: string };
96
-
97
- const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
98
-
99
- /** A run-unique suffix so repeated local runs never collide. */
100
- const RUN = randomUUID().slice(0, 8);
101
-
102
- /** The seeded sentinel workspace — `database/tenancy.ts`'s DEFAULT_WORKSPACE_ID. */
103
- const SENTINEL_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
104
-
105
- /**
106
- * The eleven `site_id` tables of spec § 1.1, each with the columns its own
107
- * NOT NULLs require BESIDES `site_id`/`workspace_id`. `cols` and `vals` are
108
- * positional twins: `vals` starts at `$3` because `$1`/`$2` are always
109
- * `site_id`/`workspace_id`, which every case below supplies itself.
110
- */
111
- interface FenceTable {
112
- /** The Postgres table name — also the constraint-name stem. */
113
- table: string;
114
- /** Extra NOT NULL columns, already quoted-safe (plain lower_snake). */
115
- cols: string[];
116
- /** One value per `cols` entry, in order. */
117
- vals: () => unknown[];
118
- }
119
-
120
- const TABLES: FenceTable[] = [
121
- {
122
- table: "subscribers",
123
- cols: ["email", "source"],
124
- vals: () => [`sub-${RUN}-${randomUUID()}@example.com`, `6374-${RUN}`],
125
- },
126
- {
127
- table: "callbacks",
128
- cols: ["name", "phone", "payload"],
129
- vals: () => [`6374 ${RUN}`, "+10000000000", "{}"],
130
- },
131
- {
132
- table: "consent_grants",
133
- cols: [
134
- "subject_type",
135
- "subject_id",
136
- "purpose_id",
137
- "granted",
138
- "locale",
139
- "statement",
140
- "statement_hash",
141
- "source",
142
- "policy_version",
143
- ],
144
- vals: () => [
145
- "subscriber",
146
- `subj-${RUN}-${randomUUID()}`,
147
- `purpose-${RUN}`,
148
- true,
149
- "en",
150
- "I agree",
151
- `hash-${RUN}`,
152
- "api",
153
- 1,
154
- ],
155
- },
156
- {
157
- table: "appointments",
158
- cols: [
159
- "provider_booking_uid",
160
- "start_time",
161
- "end_time",
162
- "attendee_name",
163
- "attendee_email",
164
- "status",
165
- "lifecycle_event_at",
166
- "updated_at",
167
- ],
168
- vals: () => [
169
- `uid-${RUN}-${randomUUID().slice(0, 8)}`,
170
- new Date("2026-10-01T10:00:00Z"),
171
- new Date("2026-10-01T11:00:00Z"),
172
- "Attendee",
173
- `appt-${RUN}@example.com`,
174
- "confirmed",
175
- new Date("2026-09-22T00:00:00Z"),
176
- new Date("2026-09-22T00:00:00Z"),
177
- ],
178
- },
179
- {
180
- table: "deliverable_assets",
181
- cols: ["key", "filename", "content_type", "bytes", "size", "updated_at"],
182
- vals: () => [
183
- `key-${RUN}-${randomUUID().slice(0, 8)}`,
184
- "file.pdf",
185
- "application/pdf",
186
- Buffer.from("bytes"),
187
- 5,
188
- new Date("2026-09-22T00:00:00Z"),
189
- ],
190
- },
191
- {
192
- table: "flow_checkouts",
193
- cols: [
194
- "flow_slug",
195
- "experiment_key",
196
- "session_id",
197
- "lead_id",
198
- "offer_key",
199
- "seats",
200
- "period",
201
- "credit_stop",
202
- "amount_minor",
203
- "currency",
204
- "quote",
205
- "permalink",
206
- "status",
207
- "updated_at",
208
- ],
209
- vals: () => [
210
- `flow-${RUN}`,
211
- `exp-${RUN}`,
212
- `sess-${RUN}-${randomUUID().slice(0, 8)}`,
213
- `lead-${RUN}-${randomUUID().slice(0, 8)}`,
214
- "offer",
215
- 1,
216
- "monthly",
217
- "{}",
218
- 1000,
219
- "usd",
220
- "{}",
221
- "permalink",
222
- "created",
223
- new Date("2026-09-22T00:00:00Z"),
224
- ],
225
- },
226
- {
227
- table: "waitlist_experiments",
228
- cols: ["key", "mode", "status", "config", "updated_at"],
229
- vals: () => [
230
- `exp-${RUN}-${randomUUID().slice(0, 8)}`,
231
- "lead-capture",
232
- "live",
233
- "{}",
234
- new Date("2026-09-22T00:00:00Z"),
235
- ],
236
- },
237
- {
238
- table: "waitlist_scores",
239
- cols: [
240
- "lead_id",
241
- "experiment_key",
242
- "waitlist_key",
243
- "tier",
244
- "score",
245
- "category_scores",
246
- "rule_trace",
247
- "updated_at",
248
- ],
249
- vals: () => [
250
- `lead-${RUN}-${randomUUID().slice(0, 8)}`,
251
- `exp-${RUN}`,
252
- `wl-${RUN}`,
253
- "a",
254
- 10,
255
- "{}",
256
- "{}",
257
- new Date("2026-09-22T00:00:00Z"),
258
- ],
259
- },
260
- {
261
- table: "waitlist_followup_sends",
262
- cols: ["lead_id", "follow_up_key", "sent_at", "status"],
263
- vals: () => [
264
- `lead-${RUN}-${randomUUID().slice(0, 8)}`,
265
- `fu-${RUN}`,
266
- new Date("2026-09-22T00:00:00Z"),
267
- "sent",
268
- ],
269
- },
270
- {
271
- table: "playbook_instances",
272
- cols: [
273
- "lead_id",
274
- "experiment_key",
275
- "score_id",
276
- "tier",
277
- "level",
278
- "rule_trace",
279
- "source",
280
- "markdown",
281
- ],
282
- vals: () => [
283
- `lead-${RUN}-${randomUUID().slice(0, 8)}`,
284
- `exp-${RUN}`,
285
- `score-${RUN}-${randomUUID().slice(0, 8)}`,
286
- "a",
287
- 1,
288
- "{}",
289
- "{}",
290
- "# playbook",
291
- ],
292
- },
293
- {
294
- table: "site_listing_configs",
295
- cols: ["data", "updated_at"],
296
- vals: () => ["{}", new Date("2026-09-22T00:00:00Z")],
297
- },
298
- ];
299
-
300
- /** `INSERT INTO t (site_id, workspace_id, …) VALUES ($1, $2, …)`. */
301
- function insertSql(spec: FenceTable): string {
302
- const cols = ['"site_id"', '"workspace_id"', ...spec.cols.map((c) => `"${c}"`)];
303
- const params = cols.map((_, i) => `$${i + 1}`);
304
- return `INSERT INTO "${spec.table}" (${cols.join(", ")}) VALUES (${params.join(", ")})`;
305
- }
306
-
307
- /** The same INSERT with `workspace_id` omitted entirely — the NULL case. */
308
- function insertSqlWithoutWorkspace(spec: FenceTable): string {
309
- const cols = ['"site_id"', ...spec.cols.map((c) => `"${c}"`)];
310
- const params = cols.map((_, i) => `$${i + 1}`);
311
- return `INSERT INTO "${spec.table}" (${cols.join(", ")}) VALUES (${params.join(", ")})`;
312
- }
313
-
314
- describeDb("the #6374 fence fan-out constraints (live DB)", () => {
315
- let db: Client;
316
- let workspaceAId: string;
317
- let workspaceBId: string;
318
- /** A workspace used ONLY by the cascade proof, so its delete is unblocked. */
319
- let workspaceCId: string;
320
- let orgId: string;
321
-
322
- async function makeWorkspace(label: string): Promise<string> {
323
- const row = await db.query<{ id: string }>(
324
- `INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
325
- VALUES ($1, $2, $3, now()) RETURNING "id"`,
326
- [orgId, `6374 Workspace ${label} ${RUN}`, `6374-ws-${label}-${RUN}`],
327
- );
328
- return row.rows[0]!.id;
329
- }
330
-
331
- beforeAll(async () => {
332
- db = new Client({ connectionString: process.env.DATABASE_URL });
333
- await db.connect();
334
-
335
- // Two REAL, distinct workspaces — A and B — so a "disagreement" insert
336
- // names two values that both actually exist, not a real one versus
337
- // garbage (which the FORMAT CHECK, not the equality CHECK, would catch;
338
- // the assertions below name the equality constraint for that reason).
339
- orgId = (
340
- await db.query<{ id: string }>(
341
- `INSERT INTO "organizations" ("name", "slug", "updated_at")
342
- VALUES ($1, $2, now()) RETURNING "id"`,
343
- [`6374 Org ${RUN}`, `6374-org-${RUN}`],
344
- )
345
- ).rows[0]!.id;
346
-
347
- workspaceAId = await makeWorkspace("a");
348
- workspaceBId = await makeWorkspace("b");
349
- workspaceCId = await makeWorkspace("c");
350
- });
351
-
352
- afterAll(async () => {
353
- // Every statement here is best-effort and the connection is closed in a
354
- // `finally`: when an assertion above has already failed because a column
355
- // or constraint this unit adds is ABSENT, the cleanup statement naming it
356
- // fails too — and an un-ended `pg` Client is an open handle that hangs
357
- // the whole Jest run instead of reporting the real failure.
358
- const cleanup = [
359
- ...TABLES.map(
360
- (spec) =>
361
- [
362
- `DELETE FROM "${spec.table}" WHERE "site_id" = ANY($1)`,
363
- [[workspaceAId, workspaceBId, workspaceCId]],
364
- ] as const,
365
- ),
366
- [
367
- `DELETE FROM "listing_csv_sends" WHERE "listing_id" = $1`,
368
- [`listing-${RUN}`],
369
- ] as const,
370
- [
371
- `DELETE FROM "deliverable_grants" WHERE "site_id" = ANY($1)`,
372
- [[workspaceAId, workspaceBId]],
373
- ] as const,
374
- [
375
- `DELETE FROM "site_settings" WHERE "workspace_id" = ANY($1)`,
376
- [[workspaceAId, workspaceBId]],
377
- ] as const,
378
- [`DELETE FROM "workspaces" WHERE "slug" LIKE $1`, [`6374-ws-%-${RUN}`]] as const,
379
- [`DELETE FROM "organizations" WHERE "slug" = $1`, [`6374-org-${RUN}`]] as const,
380
- ];
381
-
382
- try {
383
- for (const [sql, params] of cleanup) {
384
- try {
385
- await db.query(sql, params as unknown[]);
386
- } catch {
387
- // Best-effort — see the note above.
388
- }
389
- }
390
- } finally {
391
- await db.end();
392
- }
393
- });
394
-
395
- describe.each(TABLES.map((spec) => [spec.table, spec] as const))(
396
- "AC-4 — %s",
397
- (table, spec) => {
398
- it("rejects an insert with workspace_id NULL (23502)", async () => {
399
- await expect(
400
- db.query(insertSqlWithoutWorkspace(spec), [
401
- workspaceAId,
402
- ...spec.vals(),
403
- ]),
404
- ).rejects.toMatchObject<PgError>({ code: "23502" });
405
- });
406
-
407
- it(`rejects workspace_id <> site_id, BY ${table}_workspace_site_agree (23514)`, async () => {
408
- await expect(
409
- db.query(insertSql(spec), [
410
- workspaceAId,
411
- workspaceBId,
412
- ...spec.vals(),
413
- ]),
414
- ).rejects.toMatchObject<PgError>({
415
- code: "23514",
416
- constraint: `${table}_workspace_site_agree`,
417
- });
418
- });
419
-
420
- it("accepts an insert where workspace_id AGREES with site_id", async () => {
421
- const result = await db.query(
422
- `${insertSql(spec)} RETURNING "id"`,
423
- [workspaceAId, workspaceAId, ...spec.vals()],
424
- );
425
- expect(result.rows).toHaveLength(1);
426
- });
427
-
428
- it("carries no row with site_id = 'default' after the migration", async () => {
429
- const result = await db.query<{ count: string }>(
430
- `SELECT count(*)::text AS count FROM "${table}" WHERE "site_id" = 'default'`,
431
- );
432
- expect(result.rows[0]!.count).toBe("0");
433
- });
434
- },
435
- );
436
-
437
- describe("AC-5 — site_settings is keyed by (workspace_id, key), not by key alone (D3)", () => {
438
- it("two DIFFERENT workspaces may each hold a feature_flags row", async () => {
439
- const a = await db.query(
440
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
441
- VALUES ($1, $2, '{}'::jsonb, now()) RETURNING "id"`,
442
- [workspaceAId, "feature_flags"],
443
- );
444
- const b = await db.query(
445
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
446
- VALUES ($1, $2, '{}'::jsonb, now()) RETURNING "id"`,
447
- [workspaceBId, "feature_flags"],
448
- );
449
- expect(a.rows).toHaveLength(1);
450
- expect(b.rows).toHaveLength(1);
451
- });
452
-
453
- it("a SECOND feature_flags row in ONE workspace is rejected (23505)", async () => {
454
- await expect(
455
- db.query(
456
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
457
- VALUES ($1, $2, '{}'::jsonb, now())`,
458
- [workspaceAId, "feature_flags"],
459
- ),
460
- ).rejects.toMatchObject<PgError>({ code: "23505" });
461
- });
462
-
463
- it("rejects a site_settings row with workspace_id NULL (23502)", async () => {
464
- await expect(
465
- db.query(
466
- `INSERT INTO "site_settings" ("key", "value", "updated_at")
467
- VALUES ($1, '{}'::jsonb, now())`,
468
- [`nullws-${RUN}`],
469
- ),
470
- ).rejects.toMatchObject<PgError>({ code: "23502" });
471
- });
472
- });
473
-
474
- describe("AC-5 — listing_csv_sends takes NOT NULL + the format CHECK, and no equality CHECK (D2)", () => {
475
- it("rejects an insert with workspace_id NULL (23502)", async () => {
476
- await expect(
477
- db.query(
478
- `INSERT INTO "listing_csv_sends" ("listing_id", "sent_at", "row_count", "grant_id")
479
- VALUES ($1, now(), 1, $2)`,
480
- [`listing-${RUN}`, `grant-${RUN}`],
481
- ),
482
- ).rejects.toMatchObject<PgError>({ code: "23502" });
483
- });
484
-
485
- it("accepts an insert stamped with a well-formed workspace_id", async () => {
486
- const result = await db.query(
487
- `INSERT INTO "listing_csv_sends"
488
- ("listing_id", "workspace_id", "sent_at", "row_count", "grant_id")
489
- VALUES ($1, $2, now(), 1, $3) RETURNING "id"`,
490
- [`listing-${RUN}`, workspaceAId, `grant-${RUN}`],
491
- );
492
- expect(result.rows).toHaveLength(1);
493
- });
494
-
495
- it("has NO <table>_workspace_site_agree constraint — it carries no site_id (D2)", async () => {
496
- const result = await db.query<{ conname: string }>(
497
- `SELECT c.conname FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid
498
- WHERE t.relname = 'listing_csv_sends'
499
- AND c.conname = 'listing_csv_sends_workspace_site_agree'`,
500
- );
501
- expect(result.rows).toHaveLength(0);
502
- });
503
- });
504
-
505
- describe("AC-5 — deliverable_grants stays NULL-tolerant but never disagreeing (D4)", () => {
506
- it("ACCEPTS a row with workspace_id NULL — the serve path's fail-closed sentinel", async () => {
507
- const result = await db.query(
508
- `INSERT INTO "deliverable_grants" ("site_id", "asset_id", "lead_id", "session_id")
509
- VALUES ($1, $2, $3, $4) RETURNING "id"`,
510
- [
511
- workspaceAId,
512
- `dasset-${RUN}`,
513
- `lead-${RUN}`,
514
- `sess-${RUN}-${randomUUID().slice(0, 8)}`,
515
- ],
516
- );
517
- expect(result.rows).toHaveLength(1);
518
- });
519
-
520
- it("rejects a STAMPED row whose workspace_id differs from its site_id (23514)", async () => {
521
- await expect(
522
- db.query(
523
- `INSERT INTO "deliverable_grants"
524
- ("site_id", "workspace_id", "asset_id", "lead_id", "session_id")
525
- VALUES ($1, $2, $3, $4, $5)`,
526
- [
527
- workspaceAId,
528
- workspaceBId,
529
- `asset-${RUN}`,
530
- `lead-${RUN}`,
531
- `sess-${RUN}-${randomUUID().slice(0, 8)}`,
532
- ],
533
- ),
534
- ).rejects.toMatchObject<PgError>({
535
- code: "23514",
536
- constraint: "deliverable_grants_workspace_site_agree",
537
- });
538
- });
539
-
540
- it("accepts a STAMPED row whose workspace_id agrees with its site_id", async () => {
541
- const result = await db.query(
542
- `INSERT INTO "deliverable_grants"
543
- ("site_id", "workspace_id", "asset_id", "lead_id", "session_id")
544
- VALUES ($1, $1, $2, $3, $4) RETURNING "id"`,
545
- [
546
- workspaceAId,
547
- `asset-${RUN}`,
548
- `lead-${RUN}`,
549
- `sess-${RUN}-${randomUUID().slice(0, 8)}`,
550
- ],
551
- );
552
- expect(result.rows).toHaveLength(1);
553
- });
554
- });
555
-
556
- describe("AC-14 — site_settings: abort, never guess (the key carries the tenant)", () => {
557
- /** The sentinel Organization the sentinel workspace must hang off. */
558
- const SENTINEL_ORGANIZATION_ID = "org_00000000-0000-7000-8000-000000000001";
559
-
560
- // Every case runs inside a transaction that is ROLLED BACK, and each first
561
- // drops NOT NULL on `workspace_id` so the PRE-migration row shape (an
562
- // unattributed row) can exist at all. Nothing escapes the rollback.
563
- async function withPreMigrationShape<T>(
564
- body: () => Promise<T>,
565
- ): Promise<T> {
566
- await db.query("BEGIN");
567
- try {
568
- await db.query(
569
- `ALTER TABLE "site_settings" ALTER COLUMN "workspace_id" DROP NOT NULL`,
570
- );
571
- return await body();
572
- } finally {
573
- await db.query("ROLLBACK");
574
- }
575
- }
576
-
577
- /**
578
- * The sentinel UPDATE joins to `workspaces`, so a database without the
579
- * sentinel row backfills NOTHING and the post-backfill assertion aborts.
580
- * A fresh `prisma migrate deploy` seeds no sentinel, so the non-abort
581
- * cases create it here — inside the same rolled-back transaction.
582
- */
583
- async function ensureSentinelWorkspace(): Promise<void> {
584
- await db.query(
585
- `INSERT INTO "organizations" ("id", "name", "slug", "updated_at")
586
- VALUES ($1, $2, $3, now()) ON CONFLICT DO NOTHING`,
587
- [SENTINEL_ORGANIZATION_ID, `Sentinel Org ${RUN}`, `sentinel-org-${RUN}`],
588
- );
589
- await db.query(
590
- `INSERT INTO "workspaces" ("id", "organization_id", "name", "slug", "updated_at")
591
- VALUES ($1, $2, $3, $4, now()) ON CONFLICT DO NOTHING`,
592
- [
593
- SENTINEL_WORKSPACE_ID,
594
- SENTINEL_ORGANIZATION_ID,
595
- `Sentinel WS ${RUN}`,
596
- `sentinel-ws-${RUN}`,
597
- ],
598
- );
599
- }
600
-
601
- /** The `workspace_id` the one row carrying `key` ended up attributed to. */
602
- async function attributedWorkspaceOf(key: string): Promise<string | null> {
603
- const rows = await db.query<{ workspace_id: string | null }>(
604
- `SELECT "workspace_id" FROM "site_settings" WHERE "key" = $1`,
605
- [key],
606
- );
607
- expect(rows.rows).toHaveLength(1);
608
- return rows.rows[0]!.workspace_id;
609
- }
610
-
611
- it("(a) ABORTS on an unattributed row whose key embeds a NON-sentinel workspace id", async () => {
612
- await withPreMigrationShape(async () => {
613
- await ensureSentinelWorkspace();
614
- await db.query(
615
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
616
- VALUES (NULL, $1, '{}'::jsonb, now())`,
617
- [`commerce.dunning_policy:${workspaceAId}`],
618
- );
619
-
620
- // workspaceAId is a real, well-formed, NON-sentinel wksp id — exactly
621
- // the row the sentinel assignment would misfile. The guard aborts
622
- // BEFORE the UPDATE, so a sentinel workspace being available here
623
- // changes nothing.
624
- // No explicit type argument here: `PgError` declares no `message`,
625
- // and pinning one would force a cast. The matcher checks both the
626
- // SQLSTATE and that the exception names the table it is about, so a
627
- // P0001 raised by some other block cannot satisfy this.
628
- await expect(db.query(siteSettingsGuardSql())).rejects.toMatchObject({
629
- code: "P0001",
630
- message: expect.stringContaining("site_settings"),
631
- });
632
- });
633
- });
634
-
635
- it("(b) ATTRIBUTES an unattributed row whose key embeds NO workspace id to the sentinel", async () => {
636
- const aliasKey = `commerce.dunning_policy:default-${RUN}`;
637
- const plainKey = `feature_flags-${RUN}`;
638
- await withPreMigrationShape(async () => {
639
- await ensureSentinelWorkspace();
640
- // Both shapes the guard must let through: the bare alias the
641
- // single-tenant build writes, and an ordinary config key naming no
642
- // site at all.
643
- await db.query(
644
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
645
- VALUES (NULL, $1, '{}'::jsonb, now()), (NULL, $2, '{}'::jsonb, now())`,
646
- [aliasKey, plainKey],
647
- );
648
-
649
- await expect(db.query(siteSettingsGuardSql())).resolves.toBeDefined();
650
-
651
- // The slice ran guard -> UPDATE -> assertion, so these are ATTRIBUTED,
652
- // not merely un-rejected.
653
- expect(await attributedWorkspaceOf(aliasKey)).toBe(SENTINEL_WORKSPACE_ID);
654
- expect(await attributedWorkspaceOf(plainKey)).toBe(SENTINEL_WORKSPACE_ID);
655
- });
656
- });
657
-
658
- it("(c) ATTRIBUTES an unattributed row whose key embeds the SENTINEL id to the sentinel", async () => {
659
- const key = `commerce.dunning_policy:${SENTINEL_WORKSPACE_ID}`;
660
- await withPreMigrationShape(async () => {
661
- await ensureSentinelWorkspace();
662
- await db.query(
663
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
664
- VALUES (NULL, $1, '{}'::jsonb, now())`,
665
- [key],
666
- );
667
-
668
- await expect(db.query(siteSettingsGuardSql())).resolves.toBeDefined();
669
-
670
- // A key already naming the sentinel is not a misfiling risk — it
671
- // backfills to exactly the workspace its key names.
672
- expect(await attributedWorkspaceOf(key)).toBe(SENTINEL_WORKSPACE_ID);
673
- });
674
- });
675
-
676
- it("(d) is INERT on an already-migrated database — the verbatim re-run stays a no-op (NFR2)", async () => {
677
- // No transaction and no DROP NOT NULL: this is the real, migrated table,
678
- // where every row carries a workspace_id. The `IS NULL` guard makes the
679
- // check match nothing, the UPDATE match nothing, and the assertion pass
680
- // — which is what keeps AC-3's re-run clean even on a database whose
681
- // keys embed non-sentinel ids.
682
- const key = `commerce.dunning_policy:${workspaceAId}`;
683
- await db.query(
684
- `INSERT INTO "site_settings" ("workspace_id", "key", "value", "updated_at")
685
- VALUES ($1, $2, '{}'::jsonb, now())`,
686
- [workspaceAId, key],
687
- );
688
-
689
- await expect(db.query(siteSettingsGuardSql())).resolves.toBeDefined();
690
-
691
- // Untouched: still its OWN workspace, never rewritten to the sentinel.
692
- expect(await attributedWorkspaceOf(key)).toBe(workspaceAId);
693
- });
694
- });
695
-
696
- describe("AC-5 — the fence is a CHECK, never a foreign key (NFR1)", () => {
697
- it("deleting a workspaces row leaves that workspace's consent_grants rows in place", async () => {
698
- await db.query(
699
- `INSERT INTO "consent_grants"
700
- ("site_id", "workspace_id", "subject_type", "subject_id", "purpose_id",
701
- "granted", "locale", "statement", "statement_hash", "source", "policy_version")
702
- VALUES ($1, $1, 'subscriber', $2, $3, true, 'en', 'I agree', $4, 'api', 1)`,
703
- [workspaceCId, `cascade-${RUN}`, `purpose-${RUN}`, `hash-${RUN}`],
704
- );
705
-
706
- await db.query(`DELETE FROM "workspaces" WHERE "id" = $1`, [workspaceCId]);
707
-
708
- const remaining = await db.query<{ count: string }>(
709
- `SELECT count(*)::text AS count FROM "consent_grants" WHERE "site_id" = $1`,
710
- [workspaceCId],
711
- );
712
- expect(remaining.rows[0]!.count).toBe("1");
713
- });
714
- });
715
- });