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,1022 +0,0 @@
1
- // Not marked oss: false (D-48/#5366 escalation) — tests the
2
- // always-shipping project-listings anonymous surface; see
3
- // routes/project-listings/me.ts's header comment.
4
- /**
5
- * @file site-answers.test.ts
6
- *
7
- * THE ACCEPTANCE GATE for the anonymous-write bounds on the site-answers
8
- * store (#5285), and for the three contract fields that became writable
9
- * beside it.
10
- *
11
- * The store is what lets a site ask a question the platform does not read —
12
- * no column, no migration, no release. The write path is ANONYMOUS: a resume
13
- * token and nothing else. So every bound on it is read from the site's
14
- * SERVER-side `SiteListingConfig`, exactly as `wordCaps` and `collectedFields`
15
- * already are, and the tests below pin each bound to the SITE's number so a
16
- * cap that came from the package could not pass by coincidence.
17
- *
18
- * The tests that would fail QUIETLY, and are the reason this file exists:
19
- *
20
- * - an answer for an undeclared id must 400 with NOTHING written. Dropping
21
- * the unknown key instead is indistinguishable, from the client's side,
22
- * from a successful save — and hands the size of the store to whoever is
23
- * posting into it. Asserted on the ROW, not only on the status code.
24
- * - an `operator-only` answer must be absent from the PUBLIC read. A
25
- * visibility flag that is declared and never consulted looks identical to
26
- * one that is, right up until a founder's private answer is on a page.
27
- * - `requestedWindowDays` must be CLAMPED and not stored as posted. The
28
- * site's maximum is its commercial terms; a founder who could post their
29
- * own bound could post an unbounded one.
30
- * - adding three optional fields widens the DEFAULT `collectedFields`
31
- * (it derives from the application schema's own shape), so an existing
32
- * site's submit floor is pinned as unchanged.
33
- *
34
- * Same harness as `configured-application.test.ts`: real routes, real
35
- * services, mocked `lib/prisma.js` over a small in-memory row store.
36
- */
37
-
38
- // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock
39
- jest.mock("../../../lib/prisma.js");
40
-
41
- jest.mock("@working-theory/email", () => ({
42
- sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
43
- }));
44
-
45
- jest.mock("@working-theory/license", () => ({
46
- checkLicense: jest.fn().mockResolvedValue({
47
- tier: "free",
48
- plan: null,
49
- features: [],
50
- expiresAt: null,
51
- revoked: false,
52
- }),
53
- }));
54
-
55
- import Fastify from "fastify";
56
- import type { FastifyInstance } from "fastify";
57
- import { SITE_LISTING_CONFIG_DEFAULTS } from "@working-theory/validation";
58
-
59
- import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
60
- import {
61
- mockNoWorkspace,
62
- mockWorkspaceExists,
63
- } from "../../../lib/__mocks__/prisma.js";
64
- import { prisma } from "../../../lib/prisma.js";
65
- import { listProjectListings } from "../../../services/project-listings.js";
66
- import { patchProjectListingDraftRoute } from "../patch-draft.js";
67
- import { projectListingDirectoryRoute } from "../index.js";
68
- import { publicProjectListingsRoute } from "../public.js";
69
- import { startProjectListingRoute } from "../start.js";
70
- import { submitProjectListingRoute } from "../submit.js";
71
-
72
- // ─── Fixtures ───────────────────────────────────────────────────────────────
73
-
74
- const SITE_ID = "site-1";
75
- const SITE_KEY = "pk-site-1";
76
-
77
- const opt = (value: string, label: string) => ({ value, label });
78
-
79
- const VALID_QUESTIONS = {
80
- q2: {
81
- prompt: "Why are you here?",
82
- options: [opt("scale", "Scaling my team"), opt("just-curious", "Just curious")],
83
- },
84
- q3: { prompt: "Stage?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
85
- q4: { prompt: "Team size?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
86
- q5: {
87
- prompt: "Urgency?",
88
- options: [opt("urgent", "Now"), opt("planning", "Soon"), opt("exploring", "Someday")],
89
- },
90
- q6: {
91
- prompt: "Pain?",
92
- options: [
93
- opt("low", "Low"),
94
- opt("medium", "Medium"),
95
- opt("high", "High"),
96
- opt("critical", "Critical"),
97
- ],
98
- },
99
- q7: { prompt: "Anything else?" },
100
- extras: [],
101
- };
102
-
103
- const START_PAYLOAD = {
104
- siteKey: SITE_KEY,
105
- founderName: "Ada Founder",
106
- founderEmail: "ada@example.com",
107
- consent: true,
108
- };
109
-
110
- /** Everything the screens after step one collect, under the default config. */
111
- const COMPLETE_APPLICATION = {
112
- name: "My Project",
113
- headline: "Ten words or fewer for the tile title here",
114
- oneLiner: "A short sentence describing what the project does for its people.",
115
- problem: "The problem is real and this describes it.",
116
- product: "wireframes",
117
- commitment: "part-time",
118
- teamComposition: "co-founders",
119
- faqs: [
120
- { question: "Q one?", answer: "A one." },
121
- { question: "Q two?", answer: "A two." },
122
- { question: "Q three?", answer: "A three." },
123
- ],
124
- address: "1 Founder Way, Springfield",
125
- questions: VALID_QUESTIONS,
126
- };
127
-
128
- /**
129
- * The `collectedFields` an operator wrote BEFORE #5285 — the roster as it
130
- * stood, with none of the three new fields in it. A stored list like this is
131
- * what every configured site is actually running on.
132
- */
133
- /**
134
- * A site that DELIBERATELY asks for the three #5285 fields. They are opt-in:
135
- * excluded from the platform's default mask so that adding them to the payload
136
- * could not silently widen what an anonymous caller may write on a site that
137
- * never asked. Every round-trip test below therefore configures this rather
138
- * than leaning on the default.
139
- */
140
- const COLLECTED_WITH_OPT_IN = [
141
- ...SITE_LISTING_CONFIG_DEFAULTS.collectedFields,
142
- "logoMarkUrl",
143
- "logoWordmarkUrl",
144
- "requestedWindowDays",
145
- // #5096 — the fourth. Same opt-in treatment for the same reason.
146
- "solution",
147
- ];
148
-
149
- const COLLECTED_BEFORE_5285 = [
150
- "name",
151
- "headline",
152
- "oneLiner",
153
- "problem",
154
- "url",
155
- "product",
156
- "commitment",
157
- "teamComposition",
158
- "faqs",
159
- "anythingElse",
160
- "address",
161
- "founderCompany",
162
- "questions",
163
- ];
164
-
165
- /** A site that asks four questions of its own, one of them for itself only. */
166
- const SITE_ANSWERS_CONFIG = {
167
- maxQuestions: 3,
168
- maxAnswerLength: 40,
169
- questions: [
170
- { id: "why_now", control: "textarea", maxLength: 10 },
171
- { id: "referrer", control: "select", options: ["twitter", "friend"] },
172
- { id: "demo_url", control: "url" },
173
- { id: "billing_email", control: "email", visibility: "operator-only" },
174
- ],
175
- };
176
-
177
- // ─── The in-memory row store the Prisma mock is backed by ───────────────────
178
-
179
- type Row = Record<string, unknown>;
180
-
181
- let rows: Row[];
182
- let nextId: number;
183
- /** The `SiteListingConfig.data` blob this site runs on; `null` means no row. */
184
- let configData: Record<string, unknown> | null;
185
-
186
- function matches(row: Row, where: Record<string, unknown>): boolean {
187
- return Object.entries(where).every(([key, value]) => {
188
- if (key === "OR") {
189
- return (value as Record<string, unknown>[]).some((clause) => matches(row, clause));
190
- }
191
- if (value !== null && typeof value === "object" && !(value instanceof Date)) {
192
- const clause = value as Record<string, unknown>;
193
- if ("in" in clause) return (clause["in"] as unknown[]).includes(row[key]);
194
- if ("not" in clause) return row[key] !== clause["not"];
195
- if ("gt" in clause) return (row[key] as Date) > (clause["gt"] as Date);
196
- if ("startsWith" in clause) {
197
- return (
198
- typeof row[key] === "string" &&
199
- (row[key] as string).startsWith(clause["startsWith"] as string)
200
- );
201
- }
202
- }
203
- return row[key] === value;
204
- });
205
- }
206
-
207
- function installStore(): void {
208
- // #6335 — the shared prisma mock no longer resolves ANY requested
209
- // workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
210
- // by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
211
- // services/project-listings.ts), so this suite states the ONE site it is
212
- // built around. Keyed on the requested id — nothing else resolves.
213
- mockWorkspaceExists(prisma, SITE_ID);
214
-
215
- rows = [];
216
- nextId = 1;
217
- configData = null;
218
-
219
- (prisma.lead.create as jest.Mock).mockImplementation(() =>
220
- Promise.resolve({ id: `lead_00000000-0000-7000-8000-00000000000${nextId}` }),
221
- );
222
-
223
- (prisma.siteListingConfig.findUnique as jest.Mock).mockImplementation(
224
- ({ where }: { where: { siteId?: string; publicKey?: string } }) => {
225
- if (where.publicKey !== undefined) {
226
- return Promise.resolve(where.publicKey === SITE_KEY ? { siteId: SITE_ID } : null);
227
- }
228
- return Promise.resolve(configData === null ? null : { data: configData });
229
- },
230
- );
231
-
232
- (prisma.consentGrant.createMany as jest.Mock).mockImplementation(
233
- ({ data }: { data: Row[] }) => Promise.resolve({ count: data.length }),
234
- );
235
-
236
- (prisma.$transaction as jest.Mock).mockImplementation(
237
- async (fn: (tx: typeof prisma) => Promise<unknown>) => fn(prisma),
238
- );
239
-
240
- (prisma.projectListing.create as jest.Mock).mockImplementation(({ data }: { data: Row }) => {
241
- const now = new Date();
242
- const row: Row = {
243
- id: `prjl_00000000-0000-7000-8000-00000000000${nextId++}`,
244
- slug: null,
245
- name: null,
246
- oneLiner: null,
247
- problem: null,
248
- url: null,
249
- product: null,
250
- commitment: null,
251
- teamComposition: null,
252
- faqs: null,
253
- anythingElse: null,
254
- address: null,
255
- addressLine1: null,
256
- addressLine2: null,
257
- addressCity: null,
258
- addressRegion: null,
259
- addressPostalCode: null,
260
- addressCountry: null,
261
- questions: null,
262
- siteAnswers: null,
263
- // #5407 — a row written before the columns existed, and a freshly
264
- // started draft on a site that asks for one name line: parts null,
265
- // no phone, no verification stamp.
266
- founderFirstName: null,
267
- founderLastName: null,
268
- founderPhone: null,
269
- emailVerifiedAt: null,
270
- founderCompany: null,
271
- leadId: null,
272
- headline: null,
273
- logoMarkUrl: null,
274
- logoWordmarkUrl: null,
275
- solution: null,
276
- featured: false,
277
- decidedAt: null,
278
- decidedBy: null,
279
- issueUrl: null,
280
- experimentKey: null,
281
- windowDays: null,
282
- windowEndsAt: null,
283
- createdAt: now,
284
- updatedAt: now,
285
- ...data,
286
- };
287
- rows.push(row);
288
- return Promise.resolve(row);
289
- });
290
-
291
- (prisma.projectListing.findFirst as jest.Mock).mockImplementation(
292
- ({ where }: { where: Record<string, unknown> }) =>
293
- Promise.resolve(rows.find((row) => matches(row, where)) ?? null),
294
- );
295
-
296
- (prisma.projectListing.findMany as jest.Mock).mockImplementation(
297
- ({ where }: { where: Record<string, unknown> }) =>
298
- Promise.resolve(rows.filter((row) => matches(row, where))),
299
- );
300
-
301
- (prisma.projectListing.update as jest.Mock).mockImplementation(
302
- ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
303
- const row = rows.find((r) => matches(r, where));
304
- Object.assign(row!, data, { updatedAt: new Date() });
305
- return Promise.resolve(row);
306
- },
307
- );
308
-
309
- (prisma.projectListing.updateMany as jest.Mock).mockImplementation(
310
- ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
311
- const matched = rows.filter((r) => matches(r, where));
312
- matched.forEach((r) => Object.assign(r, data, { updatedAt: new Date() }));
313
- return Promise.resolve({ count: matched.length });
314
- },
315
- );
316
- }
317
-
318
- // ─── Helpers ────────────────────────────────────────────────────────────────
319
-
320
- const bearer = (token: string) => ({ authorization: `Bearer ${token}` });
321
-
322
- async function buildApp(): Promise<FastifyInstance> {
323
- const app = Fastify();
324
- await app.register(startProjectListingRoute);
325
- await app.register(patchProjectListingDraftRoute);
326
- await app.register(submitProjectListingRoute);
327
- await app.register(publicProjectListingsRoute);
328
- await app.ready();
329
- return app;
330
- }
331
-
332
- async function startDraft(
333
- app: FastifyInstance,
334
- ): Promise<{ id: string; resumeToken: string }> {
335
- const res = await app.inject({
336
- method: "POST",
337
- url: "/v1/project-listings",
338
- payload: START_PAYLOAD,
339
- });
340
- expect(res.statusCode).toBe(201);
341
- return (res.json() as { data: { id: string; resumeToken: string } }).data;
342
- }
343
-
344
- const patch = (
345
- app: FastifyInstance,
346
- draft: { id: string; resumeToken: string },
347
- payload: Record<string, unknown>,
348
- ) =>
349
- app.inject({
350
- method: "PATCH",
351
- url: `/v1/project-listings/drafts/${draft.id}`,
352
- headers: bearer(draft.resumeToken),
353
- payload,
354
- });
355
-
356
- const submit = (app: FastifyInstance, draft: { id: string; resumeToken: string }) =>
357
- app.inject({
358
- method: "POST",
359
- url: `/v1/project-listings/drafts/${draft.id}/submit`,
360
- headers: bearer(draft.resumeToken),
361
- });
362
-
363
- const publicRead = (app: FastifyInstance) =>
364
- app.inject({ method: "GET", url: `/v1/project-listings/public?siteKey=${SITE_KEY}` });
365
-
366
- const messageOf = (res: { json: () => unknown }) =>
367
- (res.json() as { error: { message: string } }).error.message;
368
-
369
- /** Approve the one row in the store, so it reaches the public read. */
370
- function publish(): void {
371
- Object.assign(rows[0]!, {
372
- status: "active",
373
- decidedAt: new Date(),
374
- windowEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
375
- });
376
- }
377
-
378
- // ─── Tests ──────────────────────────────────────────────────────────────────
379
-
380
- describe("the site-answers store's anonymous-write bounds (#5285)", () => {
381
- setupLeadFileEnv("project-listing-site-answers");
382
- let app: FastifyInstance;
383
-
384
- beforeEach(async () => {
385
- jest.clearAllMocks();
386
- installStore();
387
- app = await buildApp();
388
- });
389
-
390
- afterEach(async () => {
391
- await app.close();
392
- });
393
-
394
- // ── The declaration is the gate ──
395
-
396
- describe("the declaration list is the gate", () => {
397
- it("patch: refuses an answer for an id the site did NOT declare, and writes NOTHING", async () => {
398
- // THE property of this change. A dropped key looks like a saved key to
399
- // the client that sent it, and would leave the size of this store in the
400
- // hands of whoever is posting into it.
401
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
402
- const draft = await startDraft(app);
403
-
404
- const res = await patch(app, draft, {
405
- siteAnswers: { why_now: "June", what_colour: "blue" },
406
- });
407
-
408
- expect(res.statusCode).toBe(400);
409
- expect(messageOf(res)).toContain("what_colour");
410
- // The declared answer beside it did not land either — the body is
411
- // refused whole, not per key.
412
- expect(rows[0]!["siteAnswers"]).toBeNull();
413
- });
414
-
415
- it("patch: a site that declares NOTHING accepts no site answer at all", async () => {
416
- // The default, and what every unconfigured site runs on: the store
417
- // cannot be written by a caller who merely knows the field name.
418
- configData = null;
419
- const draft = await startDraft(app);
420
-
421
- const res = await patch(app, draft, { siteAnswers: { why_now: "June" } });
422
-
423
- expect(res.statusCode).toBe(400);
424
- expect(rows[0]!["siteAnswers"]).toBeNull();
425
- });
426
-
427
- it("patch: stores a declared answer", async () => {
428
- // The other half of every pair above — without it, a surface that
429
- // refused everything would pass them all.
430
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
431
- const draft = await startDraft(app);
432
-
433
- const res = await patch(app, draft, { siteAnswers: { why_now: "June" } });
434
-
435
- expect(res.statusCode).toBe(200);
436
- expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June" });
437
- });
438
-
439
- it("patch: MERGES answers rather than replacing them — a founder saves one screen at a time", async () => {
440
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
441
- const draft = await startDraft(app);
442
-
443
- await patch(app, draft, { siteAnswers: { why_now: "June" } });
444
- const res = await patch(app, draft, { siteAnswers: { referrer: "friend" } });
445
-
446
- expect(res.statusCode).toBe(200);
447
- expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June", referrer: "friend" });
448
- });
449
- });
450
-
451
- // ── The caps are the site's own numbers ──
452
-
453
- describe("the caps come from the site's config", () => {
454
- it("patch: refuses more answers than the site's maxQuestions, naming the site's number", async () => {
455
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
456
- const draft = await startDraft(app);
457
-
458
- const res = await patch(app, draft, {
459
- siteAnswers: {
460
- why_now: "June",
461
- referrer: "friend",
462
- demo_url: "https://example.com",
463
- billing_email: "ada@example.com",
464
- },
465
- });
466
-
467
- expect(res.statusCode).toBe(400);
468
- // Pinned to the SITE's three. A message naming the platform's twenty
469
- // would mean the cap never came from this site's row.
470
- expect(messageOf(res)).toContain("at most 3");
471
- expect(rows[0]!["siteAnswers"]).toBeNull();
472
- });
473
-
474
- it("patch: refuses an answer longer than the site's per-question cap", async () => {
475
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
476
- const draft = await startDraft(app);
477
-
478
- const over = await patch(app, draft, { siteAnswers: { why_now: "x".repeat(11) } });
479
- const at = await patch(app, draft, { siteAnswers: { why_now: "x".repeat(10) } });
480
-
481
- expect(over.statusCode).toBe(400);
482
- expect(messageOf(over)).toContain("10 characters");
483
- expect(at.statusCode).toBe(200);
484
- expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "x".repeat(10) });
485
- });
486
-
487
- it("patch: refuses a select answer outside the options the site declared", async () => {
488
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
489
- const draft = await startDraft(app);
490
-
491
- const outside = await patch(app, draft, { siteAnswers: { referrer: "billboard" } });
492
- const inside = await patch(app, draft, { siteAnswers: { referrer: "twitter" } });
493
-
494
- expect(outside.statusCode).toBe(400);
495
- expect(messageOf(outside)).toContain("billboard");
496
- expect(inside.statusCode).toBe(200);
497
- expect(rows[0]!["siteAnswers"]).toEqual({ referrer: "twitter" });
498
- });
499
-
500
- it("patch: enforces the declared control — a url answer must be http(s)", async () => {
501
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
502
- const draft = await startDraft(app);
503
-
504
- const bad = await patch(app, draft, { siteAnswers: { demo_url: "javascript:alert(1)" } });
505
-
506
- expect(bad.statusCode).toBe(400);
507
- expect(rows[0]!["siteAnswers"]).toBeNull();
508
- });
509
- });
510
-
511
- // ── Visibility ──
512
-
513
- describe("operator-only answers never reach the public read", () => {
514
- it("the public read carries the public answer and NOT the operator-only one", async () => {
515
- // A visibility flag that is declared and never consulted looks exactly
516
- // like one that is — until a founder's private answer is on a page.
517
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
518
- const draft = await startDraft(app);
519
- await patch(app, draft, { siteAnswers: { why_now: "June" } });
520
- await patch(app, draft, { siteAnswers: { billing_email: "ada@example.com" } });
521
- await patch(app, draft, COMPLETE_APPLICATION);
522
- await submit(app, draft);
523
- publish();
524
-
525
- const res = await publicRead(app);
526
-
527
- expect(res.statusCode).toBe(200);
528
- const [listing] = (res.json() as { data: { siteAnswers: Record<string, string> }[] }).data;
529
- expect(listing!.siteAnswers).toEqual({ why_now: "June" });
530
- expect(listing!.siteAnswers).not.toHaveProperty("billing_email");
531
- // And the value itself is nowhere in the serialized response at all.
532
- expect(res.payload).not.toContain("ada@example.com");
533
- });
534
-
535
- it("the tenant dashboard read carries BOTH — it is the audience operator-only exists for", async () => {
536
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
537
- const draft = await startDraft(app);
538
- await patch(app, draft, { siteAnswers: { why_now: "June" } });
539
- await patch(app, draft, { siteAnswers: { billing_email: "ada@example.com" } });
540
-
541
- const [listing] = await listProjectListings(SITE_ID);
542
-
543
- expect(listing!.siteAnswers).toEqual({
544
- why_now: "June",
545
- billing_email: "ada@example.com",
546
- });
547
- });
548
-
549
- it("an answer whose id the site no longer declares is dropped from the public read", async () => {
550
- // "No declaration" carries no visibility, and no visibility cannot be
551
- // read as public on the read that publishes.
552
- configData = { siteAnswers: SITE_ANSWERS_CONFIG };
553
- const draft = await startDraft(app);
554
- await patch(app, draft, { siteAnswers: { why_now: "June" } });
555
- await patch(app, draft, COMPLETE_APPLICATION);
556
- await submit(app, draft);
557
- publish();
558
-
559
- // The operator retires the question after the answer was written.
560
- configData = { siteAnswers: { ...SITE_ANSWERS_CONFIG, questions: [] } };
561
- const res = await publicRead(app);
562
-
563
- const [listing] = (res.json() as { data: { siteAnswers: Record<string, string> }[] }).data;
564
- expect(listing!.siteAnswers).toEqual({});
565
- // Still on the row — the founder's answer is not destroyed, only unpublished.
566
- expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June" });
567
- });
568
- });
569
-
570
- // ── The four contract fields ──
571
-
572
- describe("the four contract fields that became writable", () => {
573
- it("patch: the two logo urls round-trip onto the row, on a site that asked", async () => {
574
- configData = { collectedFields: COLLECTED_WITH_OPT_IN };
575
- const draft = await startDraft(app);
576
-
577
- const res = await patch(app, draft, {
578
- logoMarkUrl: "https://cdn.example.com/mark.svg",
579
- logoWordmarkUrl: "https://cdn.example.com/wordmark.svg",
580
- });
581
-
582
- expect(res.statusCode).toBe(200);
583
- expect(rows[0]!["logoMarkUrl"]).toBe("https://cdn.example.com/mark.svg");
584
- expect(rows[0]!["logoWordmarkUrl"]).toBe("https://cdn.example.com/wordmark.svg");
585
- });
586
-
587
- it("patch: REFUSES all four on a site that never opted in, writing nothing", async () => {
588
- // The defect the back-compat review found, at the route. `collectedFields`
589
- // defaults to every field the payload defines, so adding these three
590
- // silently made them writable on every site that had configured nothing —
591
- // and the logo fields ride the PUBLIC read as image sources. An anonymous
592
- // caller holding a resume token could have attached a third-party image
593
- // to the public listing card of a site that never asked for a logo.
594
- configData = null;
595
- const draft = await startDraft(app);
596
-
597
- for (const body of [
598
- { logoMarkUrl: "https://evil.example/a.png" },
599
- { logoWordmarkUrl: "https://evil.example/b.svg" },
600
- { requestedWindowDays: 9000 },
601
- { solution: "Text a site never asked for." },
602
- ]) {
603
- expect((await patch(app, draft, body)).statusCode).toBe(400);
604
- }
605
-
606
- expect(rows[0]!["logoMarkUrl"]).toBeNull();
607
- expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
608
- expect(rows[0]!["solution"]).toBeNull();
609
- });
610
-
611
- it("patch: refuses a logo url that is not http(s) — they render as image sources", async () => {
612
- configData = { collectedFields: COLLECTED_WITH_OPT_IN };
613
- const draft = await startDraft(app);
614
-
615
- const res = await patch(app, draft, { logoMarkUrl: "javascript:alert(1)" });
616
-
617
- expect(res.statusCode).toBe(400);
618
- expect(rows[0]!["logoMarkUrl"]).toBeNull();
619
- });
620
-
621
- it("patch: CLAMPS requestedWindowDays to the site's maximum rather than storing what was posted", async () => {
622
- // The one that would fail silently and expensively: an unclamped value
623
- // is a founder setting their own commercial terms. The three numbers are
624
- // deliberately distinct — 14 (the site's default, written at start), 90
625
- // (what was asked for) and 45 (the site's maximum, what must be stored).
626
- configData = {
627
- maxWindowDays: 45,
628
- defaultWindowDays: 14,
629
- collectedFields: COLLECTED_WITH_OPT_IN,
630
- };
631
- const draft = await startDraft(app);
632
- expect(rows[0]!["windowDays"]).toBe(14);
633
-
634
- const res = await patch(app, draft, { requestedWindowDays: "90" });
635
-
636
- expect(res.statusCode).toBe(200);
637
- expect(rows[0]!["windowDays"]).toBe(45);
638
- });
639
-
640
- it("patch: stores a requested window BELOW the site's maximum as asked", async () => {
641
- configData = {
642
- maxWindowDays: 45,
643
- defaultWindowDays: 14,
644
- collectedFields: COLLECTED_WITH_OPT_IN,
645
- };
646
- const draft = await startDraft(app);
647
-
648
- await patch(app, draft, { requestedWindowDays: 21 });
649
-
650
- expect(rows[0]!["windowDays"]).toBe(21);
651
- });
652
-
653
- it("patch: refuses a window that is not a positive whole number of days", async () => {
654
- configData = {
655
- maxWindowDays: 45,
656
- defaultWindowDays: 14,
657
- collectedFields: COLLECTED_WITH_OPT_IN,
658
- };
659
- const draft = await startDraft(app);
660
-
661
- expect((await patch(app, draft, { requestedWindowDays: 0 })).statusCode).toBe(400);
662
- expect((await patch(app, draft, { requestedWindowDays: -7 })).statusCode).toBe(400);
663
- expect((await patch(app, draft, { requestedWindowDays: "soon" })).statusCode).toBe(400);
664
- expect(rows[0]!["windowDays"]).toBe(14);
665
- });
666
- });
667
-
668
- // ── The floor the three new fields must not move ──
669
-
670
- describe("submit requirements are unchanged by the four added fields", () => {
671
- it("the four are OPT-IN, absent from the default collected set, and never required at submit", async () => {
672
- // This assertion was inverted by the back-compat review. The default
673
- // `collectedFields` derives from the application schema's own shape, so
674
- // adding a field WIDENED what an anonymous caller could write on every
675
- // site that never wrote an explicit mask — and the two logo fields ride
676
- // the public read and render as image sources. They are excluded from
677
- // the default now; a site that wants them names them.
678
- for (const field of [
679
- "logoMarkUrl",
680
- "logoWordmarkUrl",
681
- "requestedWindowDays",
682
- "solution",
683
- ]) {
684
- expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).not.toContain(field);
685
- }
686
-
687
- configData = null;
688
- const draft = await startDraft(app);
689
- await patch(app, draft, COMPLETE_APPLICATION); // carries none of the three
690
-
691
- const res = await submit(app, draft);
692
-
693
- expect(res.statusCode).toBe(200);
694
- expect(rows[0]!["status"]).toBe("requested");
695
- expect(rows[0]!["logoMarkUrl"]).toBeNull();
696
- expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
697
- expect(rows[0]!["solution"]).toBeNull();
698
- });
699
-
700
- it("a site whose stored collectedFields predates #5285 submits exactly as it did", async () => {
701
- // What every configured site is actually running on: a list written
702
- // before these three existed. It must keep working untouched.
703
- configData = { collectedFields: COLLECTED_BEFORE_5285 };
704
- const draft = await startDraft(app);
705
- await patch(app, draft, COMPLETE_APPLICATION);
706
-
707
- const res = await submit(app, draft);
708
-
709
- expect(res.statusCode).toBe(200);
710
- expect(rows[0]!["status"]).toBe("requested");
711
- });
712
-
713
- it("a site whose stored collectedFields predates #5285 cannot have a logo written into it", async () => {
714
- // The collected set is a WRITE gate as well as a completeness floor
715
- // (#5048 D5): a site that never asks for a logo must not have one
716
- // written by an anonymous caller who knows the field name.
717
- configData = { collectedFields: COLLECTED_BEFORE_5285 };
718
- const draft = await startDraft(app);
719
-
720
- const res = await patch(app, draft, { logoMarkUrl: "https://cdn.example.com/m.svg" });
721
-
722
- expect(res.statusCode).toBe(400);
723
- expect(rows[0]!["logoMarkUrl"]).toBeNull();
724
- });
725
-
726
- it("the missing-field list an empty draft is refused with names none of the three", async () => {
727
- configData = null;
728
- const draft = await startDraft(app);
729
- await patch(app, draft, { name: "My Project" });
730
-
731
- const res = await submit(app, draft);
732
-
733
- expect(res.statusCode).toBe(422);
734
- const body = res.json() as { error: { missingFields: string[] } };
735
- expect(body.error.missingFields).not.toContain("logoMarkUrl");
736
- expect(body.error.missingFields).not.toContain("logoWordmarkUrl");
737
- expect(body.error.missingFields).not.toContain("requestedWindowDays");
738
- expect(body.error.missingFields).not.toContain("solution");
739
- });
740
- });
741
-
742
- // ── #5096 — `solution` round-trips for a site that asked ──
743
-
744
- describe("the `solution` field (#5096)", () => {
745
- it("patch: persists it onto the row, on a site that asked", async () => {
746
- configData = { collectedFields: COLLECTED_WITH_OPT_IN };
747
- const draft = await startDraft(app);
748
-
749
- const res = await patch(app, draft, { solution: "We publish the answer, not the page." });
750
-
751
- expect(res.statusCode).toBe(200);
752
- expect(rows[0]!["solution"]).toBe("We publish the answer, not the page.");
753
- });
754
-
755
- it("patch: refuses an empty solution and one over 600 chars", async () => {
756
- configData = { collectedFields: COLLECTED_WITH_OPT_IN };
757
- const draft = await startDraft(app);
758
-
759
- expect((await patch(app, draft, { solution: "" })).statusCode).toBe(400);
760
- expect((await patch(app, draft, { solution: "x".repeat(601) })).statusCode).toBe(400);
761
- expect(rows[0]!["solution"]).toBeNull();
762
- });
763
-
764
- // The half that would fail QUIETLY: `submit` validates an application
765
- // reassembled from the ROW, and a stored value the reassembly drops is
766
- // indistinguishable — from the founder's side — from one that saved.
767
- it("submit: the stored solution survives the row-reassembly `submit` validates", async () => {
768
- configData = { collectedFields: COLLECTED_WITH_OPT_IN };
769
- const draft = await startDraft(app);
770
- await patch(app, draft, COMPLETE_APPLICATION);
771
- await patch(app, draft, { solution: "We publish the answer, not the page." });
772
-
773
- const res = await submit(app, draft);
774
-
775
- expect(res.statusCode).toBe(200);
776
- expect(rows[0]!["status"]).toBe("requested");
777
- expect(rows[0]!["solution"]).toBe("We publish the answer, not the page.");
778
- });
779
- });
780
- });
781
-
782
- // ── #6335 — the explicit workspace setup above is load-bearing ────────────
783
- //
784
- // Every case in this file states that this suite's site has a workspace.
785
- // This one takes that away, and proves the statement was doing work rather
786
- // than decorating the setup: `startProjectListingDraft` resolves the site it
787
- // is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
788
- // and refuses BEFORE any row is written. With the shared prisma mock's old
789
- // permissive default in place this case could not be written at all — every
790
- // id resolved, so the fail-closed path was unreachable from a route test.
791
- describe("#6335 — start fails closed when the site names no workspace", () => {
792
- let app: FastifyInstance;
793
-
794
- beforeEach(async () => {
795
- jest.clearAllMocks();
796
- installStore();
797
- mockNoWorkspace(prisma);
798
- app = await buildApp();
799
- });
800
-
801
- afterEach(async () => {
802
- await app.close();
803
- });
804
-
805
- it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
806
- const res = await app.inject({
807
- method: "POST",
808
- url: "/v1/project-listings",
809
- payload: START_PAYLOAD,
810
- });
811
-
812
- expect(res.statusCode).toBe(503);
813
- expect((res.json() as { error: { code: string } }).error.code).toBe(
814
- "tenant_unresolved",
815
- );
816
- expect(prisma.projectListing.create).not.toHaveBeenCalled();
817
- expect(rows).toHaveLength(0);
818
- });
819
- });
820
-
821
- // ─── #7521 — a listicle's own Application fields ────────────────────────────
822
- //
823
- // A draft a LIVE listicle captured (#6314's `capturedByFlowId`) may answer
824
- // that listicle's eligible Application fields — `text`/`textarea`/`url`/
825
- // `select` with no `mapsTo` — with the same per-control checks the site's own
826
- // questions get. The accepted entry's directory profile publishes them, minus
827
- // the owner's `privateFields` and any id the site itself declares.
828
- describe("a captured draft answers its listicle's Application fields (#7521 AC-2)", () => {
829
- setupLeadFileEnv("project-listing-listicle-answers");
830
- let app: FastifyInstance;
831
- let flows: Row[];
832
-
833
- const en = (value: string) => ({ en: value });
834
- function listicle(overrides: Row = {}, listing?: Record<string, unknown>): Row {
835
- return {
836
- id: "flow_partners",
837
- siteId: SITE_ID,
838
- workspaceId: SITE_ID,
839
- slug: "partners-apply",
840
- kind: "listicle",
841
- status: "live",
842
- landingPage: "/partners",
843
- createdAt: new Date("2026-09-01T00:00:00.000Z"),
844
- config: {
845
- slug: "partners-apply",
846
- defaultLocale: "en",
847
- steps: [
848
- {
849
- key: "application",
850
- type: "field",
851
- copy: { title: en("Apply") },
852
- fields: [
853
- { name: "full_name", inputType: "text", mapsTo: "name", copy: { label: en("Name") } },
854
- { name: "email", inputType: "email", mapsTo: "email", copy: { label: en("Email") } },
855
- { name: "phone", inputType: "tel", mapsTo: "phone", copy: { label: en("Phone") } },
856
- { name: "website", inputType: "url", copy: { label: en("Website") } },
857
- {
858
- name: "stage",
859
- inputType: "select",
860
- options: [
861
- { value: "idea", label: en("Idea") },
862
- { value: "live", label: en("Live") },
863
- ],
864
- copy: { label: en("Stage") },
865
- },
866
- { name: "about", inputType: "textarea", copy: { label: en("About") } },
867
- ],
868
- },
869
- ],
870
- ...(listing !== undefined && { listing }),
871
- },
872
- ...overrides,
873
- };
874
- }
875
-
876
- beforeEach(async () => {
877
- jest.clearAllMocks();
878
- installStore();
879
- flows = [listicle()];
880
- (prisma.flowDefinition.findFirst as jest.Mock).mockImplementation(
881
- ({ where }: { where: Record<string, unknown> }) =>
882
- Promise.resolve(flows.find((f) => matches(f, where)) ?? null),
883
- );
884
- (prisma.flowDefinition.findMany as jest.Mock).mockImplementation(
885
- ({ where }: { where: Record<string, unknown> }) =>
886
- Promise.resolve(flows.filter((f) => matches(f, where))),
887
- );
888
- (prisma.waitlistScore.groupBy as jest.Mock).mockResolvedValue([]);
889
- app = Fastify();
890
- await app.register(startProjectListingRoute);
891
- await app.register(patchProjectListingDraftRoute);
892
- await app.register(publicProjectListingsRoute);
893
- await app.register(projectListingDirectoryRoute);
894
- await app.ready();
895
- });
896
-
897
- afterEach(async () => {
898
- await app.close();
899
- });
900
-
901
- async function startCaptured(): Promise<{ id: string; resumeToken: string }> {
902
- const res = await app.inject({
903
- method: "POST",
904
- url: "/v1/project-listings",
905
- payload: { ...START_PAYLOAD, capturedByFlowSlug: "partners-apply" },
906
- });
907
- expect(res.statusCode).toBe(201);
908
- expect(rows[0]!["capturedByFlowId"]).toBe("flow_partners");
909
- return (res.json() as { data: { id: string; resumeToken: string } }).data;
910
- }
911
-
912
- /** Accept the one row, so it reaches the directory. */
913
- function accept(): void {
914
- Object.assign(rows[0]!, {
915
- slug: "acme",
916
- name: "Acme",
917
- status: "active",
918
- decidedAt: new Date(),
919
- windowEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
920
- });
921
- }
922
-
923
- async function profile(): Promise<Array<Record<string, string>>> {
924
- const res = await app.inject({
925
- method: "GET",
926
- url: `/v1/project-listings/directory?siteKey=${SITE_KEY}&landingPage=/partners`,
927
- });
928
- expect(res.statusCode).toBe(200);
929
- const data = (res.json() as { data: { entries: Array<{ profile: Array<Record<string, string>> }> } }).data;
930
- return data.entries[0]!.profile;
931
- }
932
-
933
- it("saves answers to the listicle's eligible fields on a captured draft", async () => {
934
- const draft = await startCaptured();
935
-
936
- const res = await patch(app, draft, { siteAnswers: { website: "https://x.test", stage: "live" } });
937
-
938
- expect(res.statusCode).toBe(200);
939
- expect(rows[0]!["siteAnswers"]).toEqual({ website: "https://x.test", stage: "live" });
940
- });
941
-
942
- it("refuses a contact field and a non-http link, writing nothing", async () => {
943
- const draft = await startCaptured();
944
-
945
- const email = await patch(app, draft, { siteAnswers: { email: "a@b.test" } });
946
- const script = await patch(app, draft, { siteAnswers: { website: "javascript:alert(1)" } });
947
-
948
- expect(email.statusCode).toBe(400);
949
- expect(script.statusCode).toBe(400);
950
- expect(rows[0]!["siteAnswers"]).toBeNull();
951
- });
952
-
953
- it("refuses the same answers on an uncaptured draft", async () => {
954
- const draft = await startDraft(app);
955
-
956
- const res = await patch(app, draft, { siteAnswers: { website: "https://x.test" } });
957
-
958
- expect(res.statusCode).toBe(400);
959
- expect(rows[0]!["siteAnswers"]).toBeNull();
960
- });
961
-
962
- it("refuses answers on a draft a DRAFT-status listicle captured", async () => {
963
- const draft = await startCaptured();
964
- flows[0]!["status"] = "draft";
965
-
966
- const res = await patch(app, draft, { siteAnswers: { website: "https://x.test" } });
967
-
968
- expect(res.statusCode).toBe(400);
969
- expect(rows[0]!["siteAnswers"]).toBeNull();
970
- });
971
-
972
- it("refuses a 501-character url answer", async () => {
973
- const draft = await startCaptured();
974
-
975
- const res = await patch(app, draft, {
976
- siteAnswers: { website: `https://x.test/${"a".repeat(486)}` },
977
- });
978
-
979
- expect(res.statusCode).toBe(400);
980
- expect(rows[0]!["siteAnswers"]).toBeNull();
981
- });
982
-
983
- it("a site-declared operator-only `about` keeps its visibility: off /public and off the profile", async () => {
984
- configData = {
985
- siteAnswers: { questions: [{ id: "about", control: "textarea", visibility: "operator-only" }] },
986
- };
987
- const draft = await startCaptured();
988
-
989
- const res = await patch(app, draft, {
990
- siteAnswers: { website: "https://x.test", about: "for the operator" },
991
- });
992
- expect(res.statusCode).toBe(200);
993
- accept();
994
-
995
- expect((await profile()).map((row) => row["name"])).toEqual(["website"]);
996
- const publicRead = await app.inject({
997
- method: "GET",
998
- url: `/v1/project-listings/public?siteKey=${SITE_KEY}`,
999
- });
1000
- expect(publicRead.body).not.toContain("for the operator");
1001
- });
1002
-
1003
- it("listing.privateFields keeps `stage` off the profile while the answer stays stored", async () => {
1004
- flows = [listicle({}, { privateFields: ["stage"] })];
1005
- const draft = await startCaptured();
1006
- await patch(app, draft, { siteAnswers: { website: "https://x.test", stage: "live" } });
1007
- accept();
1008
-
1009
- expect((await profile()).map((row) => row["name"])).toEqual(["website"]);
1010
- expect(rows[0]!["siteAnswers"]).toEqual({ website: "https://x.test", stage: "live" });
1011
- });
1012
-
1013
- it("a `javascript:` value stored while the field was `text` comes back as kind text", async () => {
1014
- await startCaptured();
1015
- accept();
1016
- rows[0]!["siteAnswers"] = { website: "javascript:x" };
1017
-
1018
- expect(await profile()).toEqual([
1019
- { name: "website", label: "Website", value: "javascript:x", kind: "text" },
1020
- ]);
1021
- });
1022
- });