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,877 +0,0 @@
1
- /**
2
- * @file deliverable-fulfillment.test.ts
3
- * @description Unit tests for the deliverable-fulfillment service (#3961
4
- * D3/D4/D5/D7).
5
- *
6
- * External dependency boundaries mocked:
7
- * - @working-theory/database (via moduleNameMapper → prisma-client.js mock; prisma singleton via __mocks__/prisma.ts)
8
- * - @working-theory/email sendDeliverableEmail
9
- * - @working-theory/observability logger
10
- */
11
-
12
- // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
13
- jest.mock("../../lib/prisma.js");
14
-
15
- const mockSendDeliverableEmail = jest.fn();
16
- jest.mock("@working-theory/email", () => ({
17
- sendDeliverableEmail: (...args: unknown[]) =>
18
- mockSendDeliverableEmail(...args),
19
- }));
20
-
21
- const mockLoggerInfo = jest.fn();
22
- const mockLoggerWarn = jest.fn();
23
- const mockLoggerError = jest.fn();
24
- jest.mock("@working-theory/observability", () => ({
25
- logger: {
26
- info: (...args: unknown[]) => mockLoggerInfo(...args),
27
- warn: (...args: unknown[]) => mockLoggerWarn(...args),
28
- error: (...args: unknown[]) => mockLoggerError(...args),
29
- debug: jest.fn(),
30
- },
31
- }));
32
-
33
- import { prisma } from "../../lib/prisma.js";
34
- import {
35
- fulfillDeliverable,
36
- listSendlessGrants,
37
- type FulfillDeliverableInput,
38
- } from "../deliverable-fulfillment";
39
-
40
- // ─── Fixtures ─────────────────────────────────────────────────────────────────
41
-
42
- const en = (s: string) => ({ en: s });
43
-
44
- const SITE_ID = "site-abc";
45
- const SESSION_ID = "flsess_00000000-0000-7000-8000-0000000000c1";
46
- const LEAD_ID = "lead_00000000-0000-7000-8000-0000000000e1";
47
- const GRANT_ID = "dgrant_00000000-0000-7000-8000-0000000000g1";
48
-
49
- // #6257 — the seeded sentinel workspace the `"default"` alias resolves to
50
- // (`@working-theory/database/tenancy`'s DEFAULT_WORKSPACE_ID), with a
51
- // sentinel-shaped organizationId so `resolveSentinelWorkspaceRow`'s own
52
- // `isSentinel` check passes. Used by ONE test — the `listSendlessGrants`
53
- // resolution test, the only test in this file that exercises an alias at
54
- // all — to prove that read scopes by the id it RESOLVED rather than by the
55
- // alias string the CLI was given. The other fence tests below run no
56
- // resolution: `fulfillDeliverable` is handed an already-resolved id.
57
- const SENTINEL_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
58
- const SENTINEL_ORGANIZATION_ID = "org_00000000-0000-7000-8000-000000000001";
59
-
60
- function deliverablesBlock() {
61
- return {
62
- assets: [
63
- { segment: "small", assetKey: "guide-small" },
64
- { segment: "large", assetKey: "guide-large" },
65
- { assetKey: "guide-default" },
66
- ],
67
- copy: {
68
- subject: en("Here's your guide"),
69
- body: en("Thanks — your download is attached."),
70
- },
71
- };
72
- }
73
-
74
- function baseConfigSnapshot() {
75
- return {
76
- slug: "get-a-quote",
77
- defaultLocale: "en",
78
- steps: [],
79
- deliverables: deliverablesBlock(),
80
- };
81
- }
82
-
83
- function baseInput(
84
- overrides: Partial<FulfillDeliverableInput> = {},
85
- ): FulfillDeliverableInput {
86
- return {
87
- siteId: SITE_ID,
88
- flowSlug: "get-a-quote",
89
- configSnapshot: baseConfigSnapshot(),
90
- segment: "small",
91
- leadId: LEAD_ID,
92
- sessionId: SESSION_ID,
93
- email: "lead@example.com",
94
- locale: "en",
95
- ...overrides,
96
- };
97
- }
98
-
99
- describe("fulfillDeliverable", () => {
100
- const originalEnv = process.env;
101
-
102
- beforeEach(() => {
103
- jest.clearAllMocks();
104
- process.env = {
105
- ...originalEnv,
106
- AILK_PUBLIC_API_URL: "https://api.example.com",
107
- AILK_DELIVERABLE_EMAIL_FROM: "deliverable-default@example.com",
108
- };
109
- mockSendDeliverableEmail.mockResolvedValue({
110
- ok: true,
111
- messageId: "msg-test",
112
- });
113
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue({
114
- id: "dasset_00000000-0000-7000-8000-0000000000a1",
115
- siteId: SITE_ID,
116
- key: "guide-small",
117
- });
118
- (prisma.deliverableGrant.create as jest.Mock).mockResolvedValue({
119
- id: GRANT_ID,
120
- });
121
- (prisma.deliverableGrant.update as jest.Mock).mockResolvedValue({
122
- id: GRANT_ID,
123
- });
124
- });
125
-
126
- afterEach(() => {
127
- process.env = originalEnv;
128
- });
129
-
130
- // ── D3 — segment → entry resolution ──────────────────────────────────────
131
-
132
- it("resolves the segment-matched entry and mints a grant for its asset", async () => {
133
- await fulfillDeliverable(baseInput({ segment: "small" }));
134
-
135
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
136
- expect.objectContaining({
137
- where: {
138
- siteId_key: { siteId: SITE_ID, key: "guide-small" },
139
- // #6257 — the legacy store's read is fenced by the same resolved
140
- // workspace id the grant is minted under.
141
- workspaceId: SITE_ID,
142
- },
143
- }),
144
- );
145
- });
146
-
147
- it("selects only the id on the legacy DeliverableAsset lookup, never the bytes blob (#9248)", async () => {
148
- await fulfillDeliverable(baseInput({ segment: "small" }));
149
-
150
- // `DeliverableAsset.bytes` is the file itself; this read exists only to
151
- // learn the row's id, so the query must name that one column.
152
- const [[args]] = (prisma.deliverableAsset.findUnique as jest.Mock).mock
153
- .calls as [[{ select?: Record<string, unknown> }]];
154
- expect(args.select).toEqual({ id: true });
155
- });
156
-
157
- it("resolves the OTHER segment's entry when segment differs (two-segment divergence, AC1)", async () => {
158
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue({
159
- id: "dasset_00000000-0000-7000-8000-0000000000a2",
160
- siteId: SITE_ID,
161
- key: "guide-large",
162
- });
163
-
164
- await fulfillDeliverable(baseInput({ segment: "large" }));
165
-
166
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
167
- expect.objectContaining({
168
- where: {
169
- siteId_key: { siteId: SITE_ID, key: "guide-large" },
170
- workspaceId: SITE_ID,
171
- },
172
- }),
173
- );
174
- });
175
-
176
- it("falls back to the default entry when segment is undefined (N=1 degenerate case)", async () => {
177
- await fulfillDeliverable(baseInput({ segment: undefined }));
178
-
179
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
180
- expect.objectContaining({
181
- where: {
182
- siteId_key: { siteId: SITE_ID, key: "guide-default" },
183
- workspaceId: SITE_ID,
184
- },
185
- }),
186
- );
187
- });
188
-
189
- it("falls back to the default entry when segment is derived but unmapped", async () => {
190
- await fulfillDeliverable(baseInput({ segment: "unmapped-segment" }));
191
-
192
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
193
- expect.objectContaining({
194
- where: {
195
- siteId_key: { siteId: SITE_ID, key: "guide-default" },
196
- workspaceId: SITE_ID,
197
- },
198
- }),
199
- );
200
- });
201
-
202
- it("skips (no grant, no send) when no entry matches and there is no default", async () => {
203
- await fulfillDeliverable(
204
- baseInput({
205
- segment: "unmapped-segment",
206
- configSnapshot: {
207
- ...baseConfigSnapshot(),
208
- deliverables: {
209
- assets: [{ segment: "small", assetKey: "guide-small" }],
210
- copy: deliverablesBlock().copy,
211
- },
212
- },
213
- }),
214
- );
215
-
216
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
217
- // #6310 FR8: a no_entry resolution exits before EITHER store is read.
218
- expect(prisma.flowDefinition.findUnique).not.toHaveBeenCalled();
219
- expect(prisma.asset.findFirst).not.toHaveBeenCalled();
220
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
221
- expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
222
- // AC4: no_entry is a pre-mint outcome — no grant exists to stamp.
223
- expect(prisma.deliverableGrant.update).not.toHaveBeenCalled();
224
- expect(mockLoggerInfo).toHaveBeenCalledWith(
225
- expect.objectContaining({
226
- siteId: SITE_ID,
227
- flowSlug: "get-a-quote",
228
- sessionId: SESSION_ID,
229
- }),
230
- expect.any(String),
231
- );
232
- });
233
-
234
- // ── D3 — missing asset row ────────────────────────────────────────────────
235
-
236
- it("skips (no grant, no send) when the config-referenced asset row is missing", async () => {
237
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
238
-
239
- await fulfillDeliverable(baseInput());
240
-
241
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
242
- expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
243
- // AC4: asset_missing is a pre-mint outcome — no grant exists to stamp.
244
- expect(prisma.deliverableGrant.update).not.toHaveBeenCalled();
245
- expect(mockLoggerInfo).toHaveBeenCalled();
246
- });
247
-
248
- // ── FEAT-074 U4b (#4718) D4 — the playbook: virtual asset ────────────────
249
-
250
- function playbookConfigSnapshot() {
251
- return {
252
- slug: "targeting-maturity",
253
- defaultLocale: "en",
254
- steps: [],
255
- deliverables: {
256
- assets: [{ assetKey: "playbook:targeting-maturity" }],
257
- copy: {
258
- subject: en("Your Playbook is ready"),
259
- body: en("Your Playbook is ready to download."),
260
- },
261
- },
262
- };
263
- }
264
-
265
- it("skips the DeliverableAsset lookup and mints the grant with assetId = entry.assetKey when a PlaybookInstance exists (AC-3)", async () => {
266
- (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({
267
- id: "pbk_00000000-0000-7000-8000-0000000000p1",
268
- });
269
-
270
- await fulfillDeliverable(
271
- baseInput({ segment: undefined, configSnapshot: playbookConfigSnapshot() }),
272
- );
273
-
274
- expect(prisma.playbookInstance.findFirst).toHaveBeenCalledWith(
275
- expect.objectContaining({
276
- where: {
277
- siteId: SITE_ID,
278
- // #6257 — the playbook existence check is fenced too.
279
- workspaceId: SITE_ID,
280
- leadId: LEAD_ID,
281
- experimentKey: "targeting-maturity",
282
- },
283
- }),
284
- );
285
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
286
- // AC-3: EXACTLY one grant -- toHaveBeenCalledTimes, not merely
287
- // toHaveBeenCalledWith (which would pass even on a second, unasserted
288
- // call).
289
- expect(prisma.deliverableGrant.create).toHaveBeenCalledTimes(1);
290
- expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
291
- expect.objectContaining({
292
- data: expect.objectContaining({
293
- siteId: SITE_ID,
294
- assetId: "playbook:targeting-maturity",
295
- leadId: LEAD_ID,
296
- sessionId: SESSION_ID,
297
- }),
298
- }),
299
- );
300
- // AC-3: the send-and-stamp tail reaches sendStatus = sent WITH the
301
- // /v1/deliverables/<grantId> link -- asserted on the actual email call,
302
- // not merely "an email was sent."
303
- expect(mockSendDeliverableEmail).toHaveBeenCalledTimes(1);
304
- expect(mockSendDeliverableEmail).toHaveBeenCalledWith(
305
- expect.objectContaining({
306
- downloadLink: `https://api.example.com/v1/deliverables/${GRANT_ID}`,
307
- }),
308
- );
309
- expect(prisma.deliverableGrant.update).toHaveBeenCalledWith({
310
- where: { id: GRANT_ID },
311
- data: { sendStatus: "sent", sendAt: expect.any(Date) },
312
- });
313
- });
314
-
315
- it("logs playbook_missing, mints no grant, and sends no email when the lead has no PlaybookInstance (AC-4)", async () => {
316
- (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue(null);
317
-
318
- await fulfillDeliverable(
319
- baseInput({ segment: undefined, configSnapshot: playbookConfigSnapshot() }),
320
- );
321
-
322
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
323
- expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
324
- // playbook_missing is a pre-mint outcome — no grant exists to stamp
325
- // (the same posture as asset_missing).
326
- expect(prisma.deliverableGrant.update).not.toHaveBeenCalled();
327
- expect(mockLoggerInfo).toHaveBeenCalledWith(
328
- expect.objectContaining({
329
- siteId: SITE_ID,
330
- sessionId: SESSION_ID,
331
- outcome: "playbook_missing",
332
- }),
333
- expect.any(String),
334
- );
335
- });
336
-
337
- it("an ordinary (non-playbook:) assetKey never queries playbookInstance (no false-positive branch)", async () => {
338
- await fulfillDeliverable(baseInput());
339
-
340
- expect(prisma.playbookInstance.findFirst).not.toHaveBeenCalled();
341
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
342
- });
343
-
344
- it("a playbook: deliverable issues zero Asset or FlowDefinition reads (#6310 FR8)", async () => {
345
- (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({
346
- id: "pbk_00000000-0000-7000-8000-0000000000p1",
347
- });
348
-
349
- await fulfillDeliverable(
350
- baseInput({ segment: undefined, configSnapshot: playbookConfigSnapshot() }),
351
- );
352
-
353
- expect(prisma.flowDefinition.findUnique).not.toHaveBeenCalled();
354
- expect(prisma.asset.findFirst).not.toHaveBeenCalled();
355
- });
356
-
357
- // ── #6310 D1/D2/D4/D5/D15 — Asset-first resolution ───────────────────────
358
-
359
- const WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
360
- const OTHER_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000002";
361
- const NEW_ASSET_ID = "asset_00000000-0000-7000-8000-0000000000b1";
362
-
363
- it("resolves against the workspace Asset store first, and mints the grant with the asset_ id AND workspaceId (D1/D2/D15)", async () => {
364
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
365
- workspaceId: WORKSPACE_ID,
366
- });
367
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID, tags: [] });
368
-
369
- await fulfillDeliverable(baseInput());
370
-
371
- expect(prisma.flowDefinition.findUnique).toHaveBeenCalledWith(
372
- expect.objectContaining({
373
- where: { siteId_slug: { siteId: SITE_ID, slug: "get-a-quote" } },
374
- select: { workspaceId: true },
375
- }),
376
- );
377
- expect(prisma.asset.findFirst).toHaveBeenCalledWith({
378
- where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
379
- select: { id: true, tags: true },
380
- });
381
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
382
- expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
383
- expect.objectContaining({
384
- data: expect.objectContaining({
385
- siteId: SITE_ID,
386
- assetId: NEW_ASSET_ID,
387
- workspaceId: WORKSPACE_ID,
388
- leadId: LEAD_ID,
389
- sessionId: SESSION_ID,
390
- }),
391
- }),
392
- );
393
- });
394
-
395
- it("falls back to the legacy DeliverableAsset store on an Asset miss, and mints a grant with NO workspaceId (D2/D3)", async () => {
396
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
397
- workspaceId: WORKSPACE_ID,
398
- });
399
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
400
-
401
- await fulfillDeliverable(baseInput());
402
-
403
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
404
- expect.objectContaining({
405
- where: {
406
- siteId_key: { siteId: SITE_ID, key: "guide-small" },
407
- // #6257 — the legacy store's read is fenced by the same resolved
408
- // workspace id the grant is minted under.
409
- workspaceId: SITE_ID,
410
- },
411
- }),
412
- );
413
- const [[createArgs]] = (prisma.deliverableGrant.create as jest.Mock).mock
414
- .calls as [[{ data: Record<string, unknown> }]];
415
- expect(createArgs.data["assetId"]).toBe(
416
- "dasset_00000000-0000-7000-8000-0000000000a1",
417
- );
418
- // #6374 D4 / AC-8 — this mint site now ALWAYS stamps `workspaceId`,
419
- // including on the legacy `dasset_` arm this test exercises, and the
420
- // value is the grant's own `siteId`. NULL remains the sentinel for the
421
- // rows that already EXIST (the migration touches none of them); it is no
422
- // longer the sentinel for a row this code mints, because a mint always
423
- // knows its workspace. The download route is unaffected: it branches on
424
- // the `assetId` PREFIX and consults `workspaceId` only on the `asset_`
425
- // arm.
426
- expect(createArgs.data["workspaceId"]).toBe(createArgs.data["siteId"]);
427
- expect(createArgs.data["workspaceId"]).toBe(SITE_ID);
428
- });
429
-
430
- it("prefers the Asset match over an existing legacy DeliverableAsset row at the SAME key (D2 order)", async () => {
431
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
432
- workspaceId: WORKSPACE_ID,
433
- });
434
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID, tags: [] });
435
- // beforeEach's default deliverableAsset.findUnique mock already resolves
436
- // a row at this key — it must never even be consulted.
437
-
438
- await fulfillDeliverable(baseInput());
439
-
440
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
441
- expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
442
- expect.objectContaining({
443
- data: expect.objectContaining({ assetId: NEW_ASSET_ID }),
444
- }),
445
- );
446
- });
447
-
448
- it("skips the Asset lookup entirely when the definition's workspaceId is null — never guesses one (D5)", async () => {
449
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
450
- workspaceId: null,
451
- });
452
-
453
- await fulfillDeliverable(baseInput());
454
-
455
- expect(prisma.asset.findFirst).not.toHaveBeenCalled();
456
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
457
- });
458
-
459
- it("skips the Asset lookup entirely when no FlowDefinition row resolves (D5)", async () => {
460
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue(null);
461
-
462
- await fulfillDeliverable(baseInput());
463
-
464
- expect(prisma.asset.findFirst).not.toHaveBeenCalled();
465
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
466
- });
467
-
468
- it("mints no grant and logs asset_missing when NEITHER store resolves the key", async () => {
469
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
470
- workspaceId: WORKSPACE_ID,
471
- });
472
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
473
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
474
-
475
- await fulfillDeliverable(baseInput());
476
-
477
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
478
- expect(mockLoggerInfo).toHaveBeenCalledWith(
479
- expect.objectContaining({ outcome: "asset_missing" }),
480
- expect.any(String),
481
- );
482
- });
483
-
484
- // ── #8848 D3 — the minted grant id, and a deliverable flow's Asset-only store ──
485
-
486
- it("resolves { grantId } equal to the created row's id on a successful mint", async () => {
487
- await expect(fulfillDeliverable(baseInput())).resolves.toEqual({ grantId: GRANT_ID });
488
- });
489
-
490
- it("resolves undefined on the asset_missing path", async () => {
491
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({ workspaceId: WORKSPACE_ID });
492
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
493
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
494
-
495
- await expect(fulfillDeliverable(baseInput())).resolves.toBeUndefined();
496
- });
497
-
498
- it("a gated file snapshot whose key names only a legacy DeliverableAsset mints no grant (SEC-8848-B)", async () => {
499
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({ workspaceId: WORKSPACE_ID });
500
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
501
- const configSnapshot = {
502
- ...baseConfigSnapshot(),
503
- deliverables: { ...deliverablesBlock(), assets: [{ assetKey: "listing-acme-2026-10-12" }] },
504
- deliverable: { kind: "file", gated: true, title: en("Q3"), geometry: "letter", pages: 3 },
505
- };
506
-
507
- await expect(
508
- fulfillDeliverable(baseInput({ segment: undefined, configSnapshot })),
509
- ).resolves.toBeUndefined();
510
-
511
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
512
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
513
- expect(mockLoggerInfo).toHaveBeenCalledWith(
514
- expect.objectContaining({ outcome: "asset_missing" }),
515
- expect.any(String),
516
- );
517
- });
518
-
519
- it("the same legacy-only key on a flow with no deliverable block still mints (NFR1)", async () => {
520
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({ workspaceId: WORKSPACE_ID });
521
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
522
- const configSnapshot = {
523
- ...baseConfigSnapshot(),
524
- deliverables: { ...deliverablesBlock(), assets: [{ assetKey: "listing-acme-2026-10-12" }] },
525
- };
526
-
527
- await expect(
528
- fulfillDeliverable(baseInput({ segment: undefined, configSnapshot })),
529
- ).resolves.toEqual({ grantId: GRANT_ID });
530
- expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
531
- });
532
-
533
- it("never reaches a key that exists only in ANOTHER workspace (NFR3) — the lookup is scoped to this flow's own workspaceId alone", async () => {
534
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
535
- workspaceId: WORKSPACE_ID,
536
- });
537
- (prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
538
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
539
-
540
- await fulfillDeliverable(baseInput());
541
-
542
- expect(prisma.asset.findFirst).toHaveBeenCalledWith({
543
- where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
544
- select: { id: true, tags: true },
545
- });
546
- expect(prisma.asset.findFirst).not.toHaveBeenCalledWith(
547
- expect.objectContaining({
548
- where: expect.objectContaining({ workspaceId: OTHER_WORKSPACE_ID }),
549
- }),
550
- );
551
- });
552
-
553
- // ── D5 — grant mint ───────────────────────────────────────────────────────
554
-
555
- it("mints the grant with assetId/leadId/sessionId", async () => {
556
- await fulfillDeliverable(baseInput());
557
-
558
- expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
559
- expect.objectContaining({
560
- data: expect.objectContaining({
561
- siteId: SITE_ID,
562
- assetId: "dasset_00000000-0000-7000-8000-0000000000a1",
563
- leadId: LEAD_ID,
564
- sessionId: SESSION_ID,
565
- }),
566
- }),
567
- );
568
- });
569
-
570
- // ── AC5 — mint-before-send is preserved (regression floor) ──────────────
571
-
572
- it("still calls deliverableGrant.create before sendDeliverableEmail (AC5)", async () => {
573
- await fulfillDeliverable(baseInput());
574
-
575
- const createOrder = (prisma.deliverableGrant.create as jest.Mock).mock
576
- .invocationCallOrder[0];
577
- const sendOrder = mockSendDeliverableEmail.mock.invocationCallOrder[0];
578
- expect(createOrder).toBeLessThan(sendOrder);
579
- });
580
-
581
- // ── AC4 — the three post-mint outcomes all stamp ─────────────────────────
582
-
583
- it("stamps sendStatus: 'sent' + a non-null sendAt on a successful send", async () => {
584
- await fulfillDeliverable(baseInput());
585
-
586
- expect(prisma.deliverableGrant.update).toHaveBeenCalledWith({
587
- where: { id: GRANT_ID },
588
- data: { sendStatus: "sent", sendAt: expect.any(Date) },
589
- });
590
- });
591
-
592
- // ── AC2 — link = base + grant id, no PII ────────────────────────────────
593
-
594
- it("builds the download link from AILK_PUBLIC_API_URL + the grant id, with no PII", async () => {
595
- await fulfillDeliverable(baseInput({ email: "lead-with-pii@example.com" }));
596
-
597
- const [call] = mockSendDeliverableEmail.mock.calls;
598
- expect(call[0].downloadLink).toBe(
599
- `https://api.example.com/v1/deliverables/${GRANT_ID}`,
600
- );
601
- expect(call[0].downloadLink).not.toContain("lead-with-pii@example.com");
602
- expect(call[0].downloadLink).not.toContain(LEAD_ID);
603
- });
604
-
605
- it("sends to the lead's email (recipient)", async () => {
606
- await fulfillDeliverable(baseInput());
607
-
608
- const [call] = mockSendDeliverableEmail.mock.calls;
609
- expect(call[0].to).toBe("lead@example.com");
610
- });
611
-
612
- // ── D7 — copy resolution at locale, with defaultLocale fallback ────────────
613
-
614
- it("resolves copy at the completion request's locale", async () => {
615
- const configSnapshot = {
616
- ...baseConfigSnapshot(),
617
- deliverables: {
618
- assets: [{ assetKey: "guide-default" }],
619
- copy: {
620
- subject: { en: "Here's your guide", "pt-BR": "Aqui está seu guia" },
621
- body: {
622
- en: "Thanks — your download is attached.",
623
- "pt-BR": "Obrigado — seu download está anexado.",
624
- },
625
- },
626
- },
627
- };
628
-
629
- await fulfillDeliverable(
630
- baseInput({ configSnapshot, locale: "pt-BR", segment: undefined }),
631
- );
632
-
633
- const [call] = mockSendDeliverableEmail.mock.calls;
634
- expect(call[0].subject).toBe("Aqui está seu guia");
635
- expect(call[0].body).toBe("Obrigado — seu download está anexado.");
636
- });
637
-
638
- it("falls back to defaultLocale when the completion locale has no copy key", async () => {
639
- const configSnapshot = {
640
- ...baseConfigSnapshot(),
641
- deliverables: {
642
- assets: [{ assetKey: "guide-default" }],
643
- copy: {
644
- subject: { en: "Here's your guide" },
645
- body: { en: "Thanks — your download is attached." },
646
- },
647
- },
648
- };
649
-
650
- await fulfillDeliverable(
651
- baseInput({ configSnapshot, locale: "fr", segment: undefined }),
652
- );
653
-
654
- const [call] = mockSendDeliverableEmail.mock.calls;
655
- expect(call[0].subject).toBe("Here's your guide");
656
- });
657
-
658
- it("resolves cta when present", async () => {
659
- const configSnapshot = {
660
- ...baseConfigSnapshot(),
661
- deliverables: {
662
- assets: [{ assetKey: "guide-default" }],
663
- copy: {
664
- subject: en("Here's your guide"),
665
- body: en("Thanks — your download is attached."),
666
- cta: en("Download now"),
667
- },
668
- },
669
- };
670
-
671
- await fulfillDeliverable(baseInput({ configSnapshot, segment: undefined }));
672
-
673
- const [call] = mockSendDeliverableEmail.mock.calls;
674
- expect(call[0].cta).toBe("Download now");
675
- });
676
-
677
- // ── sender-env fallback chain ────────────────────────────────────────────
678
-
679
- it("uses AILK_DELIVERABLE_EMAIL_FROM when set", async () => {
680
- process.env["AILK_DELIVERABLE_EMAIL_FROM"] = "deliverable@example.com";
681
-
682
- await fulfillDeliverable(baseInput());
683
-
684
- const [call] = mockSendDeliverableEmail.mock.calls;
685
- expect(call[0].fromAddress).toBe("deliverable@example.com");
686
- });
687
-
688
- it("falls back to AILK_LEAD_NOTIFICATION_FROM when AILK_DELIVERABLE_EMAIL_FROM is unset", async () => {
689
- delete process.env["AILK_DELIVERABLE_EMAIL_FROM"];
690
- process.env["AILK_LEAD_NOTIFICATION_FROM"] = "owner-noreply@example.com";
691
-
692
- await fulfillDeliverable(baseInput());
693
-
694
- const [call] = mockSendDeliverableEmail.mock.calls;
695
- expect(call[0].fromAddress).toBe("owner-noreply@example.com");
696
- });
697
-
698
- it("skips the send (grant NOT wasted) when neither from-address env is set", async () => {
699
- delete process.env["AILK_DELIVERABLE_EMAIL_FROM"];
700
- delete process.env["AILK_LEAD_NOTIFICATION_FROM"];
701
-
702
- await fulfillDeliverable(baseInput());
703
-
704
- expect(prisma.deliverableGrant.create).toHaveBeenCalled();
705
- expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
706
- expect(mockLoggerInfo).toHaveBeenCalled();
707
- // AC4: sender_unconfigured is a post-mint outcome — it stamps too.
708
- expect(prisma.deliverableGrant.update).toHaveBeenCalledWith({
709
- where: { id: GRANT_ID },
710
- data: { sendStatus: "sender_unconfigured", sendAt: expect.any(Date) },
711
- });
712
- });
713
-
714
- // ── D4 — no deliverables block: opt-in per flow ─────────────────────────
715
-
716
- it("does nothing when configSnapshot.deliverables is absent (opt-in per flow)", async () => {
717
- await fulfillDeliverable(
718
- baseInput({
719
- configSnapshot: { ...baseConfigSnapshot(), deliverables: undefined },
720
- }),
721
- );
722
-
723
- expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
724
- expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
725
- expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
726
- });
727
-
728
- // ── PII-safe logging ─────────────────────────────────────────────────────
729
-
730
- it("never logs email or copy values", async () => {
731
- delete process.env["AILK_DELIVERABLE_EMAIL_FROM"];
732
- delete process.env["AILK_LEAD_NOTIFICATION_FROM"];
733
-
734
- await fulfillDeliverable(baseInput({ email: "sentinel-pii@example.com" }));
735
-
736
- const allLogCalls = [
737
- ...mockLoggerInfo.mock.calls,
738
- ...mockLoggerWarn.mock.calls,
739
- ...mockLoggerError.mock.calls,
740
- ];
741
- for (const call of allLogCalls) {
742
- expect(JSON.stringify(call)).not.toContain("sentinel-pii@example.com");
743
- }
744
- });
745
-
746
- // ── Email send failure — logged, not thrown ──────────────────────────────
747
-
748
- it("logs (does not throw) when the email send fails", async () => {
749
- mockSendDeliverableEmail.mockResolvedValue({
750
- ok: false,
751
- error: { kind: "send_failed", message: "Resend down" },
752
- });
753
-
754
- await expect(fulfillDeliverable(baseInput())).resolves.toEqual({ grantId: GRANT_ID });
755
- expect(mockLoggerInfo).toHaveBeenCalled();
756
- // AC4: send_failed is a post-mint outcome — it stamps too.
757
- expect(prisma.deliverableGrant.update).toHaveBeenCalledWith({
758
- where: { id: GRANT_ID },
759
- data: { sendStatus: "send_failed", sendAt: expect.any(Date) },
760
- });
761
- });
762
-
763
- // ── #3998 D5 — a stamp failure is caught, never escalated ────────────────
764
-
765
- it("does not throw when the stamp write itself rejects", async () => {
766
- (prisma.deliverableGrant.update as jest.Mock).mockRejectedValue(
767
- new Error("connection reset"),
768
- );
769
-
770
- await expect(fulfillDeliverable(baseInput())).resolves.toEqual({ grantId: GRANT_ID });
771
- expect(mockSendDeliverableEmail).toHaveBeenCalled();
772
- expect(mockLoggerWarn).toHaveBeenCalledWith(
773
- expect.objectContaining({ grantId: GRANT_ID, outcome: "sent" }),
774
- expect.any(String),
775
- );
776
- });
777
- });
778
-
779
- // ─── #6257 — the fence: every fenced read carries workspaceId ───────────────
780
-
781
- /**
782
- * Unit C2's contract for this service (#6257 AC-1/AC-2). Three of its reads
783
- * are workspace-scopable and are scoped here; the rest are keyed on the grant
784
- * id itself — the capability token — and carry a global-key marker comment
785
- * at the call site instead.
786
- *
787
- * `fulfillDeliverable`'s `siteId` is the session's own, resolved before it
788
- * reaches this service (#6254/#6374 AC-7); `listSendlessGrants` takes an
789
- * OPERATOR-SUPPLIED alias off the `deliverable:resend` CLI, so it resolves
790
- * that alias itself and fails closed when it names no workspace.
791
- */
792
- describe("#6257 — every fenced read carries workspaceId", () => {
793
- const originalEnv = process.env;
794
-
795
- beforeEach(() => {
796
- jest.clearAllMocks();
797
- process.env = {
798
- ...originalEnv,
799
- AILK_PUBLIC_API_URL: "https://api.example.com",
800
- AILK_DELIVERABLE_EMAIL_FROM: "deliverable-default@example.com",
801
- };
802
- mockSendDeliverableEmail.mockResolvedValue({ ok: true, messageId: "msg-test" });
803
- (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue({
804
- id: "dasset_00000000-0000-7000-8000-0000000000a1",
805
- siteId: SITE_ID,
806
- key: "guide-small",
807
- });
808
- (prisma.deliverableGrant.create as jest.Mock).mockResolvedValue({ id: GRANT_ID });
809
- (prisma.deliverableGrant.update as jest.Mock).mockResolvedValue({ id: GRANT_ID });
810
- (prisma.deliverableGrant.findMany as jest.Mock).mockResolvedValue([]);
811
- });
812
-
813
- afterEach(() => {
814
- process.env = originalEnv;
815
- });
816
-
817
- it("scopes the LEGACY DeliverableAsset lookup by the session's workspace id", async () => {
818
- await fulfillDeliverable(baseInput());
819
-
820
- const { where } = (prisma.deliverableAsset.findUnique as jest.Mock).mock.calls[0][0];
821
- expect(where.workspaceId).toBe(SITE_ID);
822
- expect(where.siteId_key).toEqual({ siteId: SITE_ID, key: "guide-small" });
823
- });
824
-
825
- it("scopes the PlaybookInstance existence check by the session's workspace id", async () => {
826
- (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({ id: "pbk_1" });
827
-
828
- await fulfillDeliverable(
829
- baseInput({
830
- segment: undefined,
831
- configSnapshot: {
832
- slug: "get-a-quote",
833
- defaultLocale: "en",
834
- steps: [],
835
- deliverables: {
836
- assets: [{ assetKey: "playbook:targeting-maturity" }],
837
- copy: { subject: en("x"), body: en("y") },
838
- },
839
- },
840
- }),
841
- );
842
-
843
- expect(
844
- (prisma.playbookInstance.findFirst as jest.Mock).mock.calls[0][0].where.workspaceId,
845
- ).toBe(SITE_ID);
846
- });
847
-
848
- it("listSendlessGrants resolves the CLI alias and scopes the list by the RESOLVED workspace id", async () => {
849
- // The `"default"` alias resolves to the seeded sentinel workspace — the
850
- // list must be scoped by THAT id, never by the alias string the operator
851
- // typed on the command line. Passing the alias and asserting the SENTINEL
852
- // id back is what discriminates a resolution from a passthrough: a test
853
- // that passed the workspace id as the alias would assert the same string
854
- // either way and could not fail (security review, round 1).
855
- (prisma.workspace.findUnique as jest.Mock).mockImplementation(
856
- async ({ where }: { where: { id: string } }) =>
857
- where.id === SENTINEL_WORKSPACE_ID
858
- ? { id: SENTINEL_WORKSPACE_ID, organizationId: SENTINEL_ORGANIZATION_ID }
859
- : null,
860
- );
861
-
862
- await listSendlessGrants({ siteId: "default" });
863
-
864
- const { where } = (prisma.deliverableGrant.findMany as jest.Mock).mock.calls[0][0];
865
- expect(where.workspaceId).toBe(SENTINEL_WORKSPACE_ID);
866
- expect(where.siteId).toBe(SENTINEL_WORKSPACE_ID);
867
- expect(where.workspaceId).not.toBe("default");
868
- expect(where.sendStatus).toBeNull();
869
- });
870
-
871
- it("listSendlessGrants fails closed on an alias that resolves to no workspace", async () => {
872
- (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
873
-
874
- await expect(listSendlessGrants({ siteId: "no-such-site" })).resolves.toEqual([]);
875
- expect(prisma.deliverableGrant.findMany).not.toHaveBeenCalled();
876
- });
877
- });