create-ailk 0.22.0 → 0.24.0

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 (287) hide show
  1. package/README.md +27 -14
  2. package/component-catalog.json +149 -5
  3. package/component-catalog.md +7 -6
  4. package/dist/add-module.js +17 -10
  5. package/dist/add-route.d.ts +55 -1
  6. package/dist/add-route.js +301 -13
  7. package/dist/cli.js +27 -10
  8. package/dist/derive-architecture.d.ts +7 -5
  9. package/dist/derive-architecture.js +26 -32
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +0 -4
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +36 -2
  15. package/dist/lib/scaffold-render-sweep.js +50 -8
  16. package/dist/module-architecture.js +115 -10
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/seed-flows.d.ts +4 -1
  20. package/dist/seed-flows.js +50 -4
  21. package/dist/spoke-checkout.d.ts +38 -35
  22. package/dist/spoke-checkout.js +66 -229
  23. package/dist/starter-grid.d.ts +0 -3
  24. package/dist/starter-grid.js +24 -14
  25. package/dist/surfaces.js +33 -12
  26. package/dist/sync-routes.d.ts +68 -5
  27. package/dist/sync-routes.js +455 -29
  28. package/dist/template-record.d.ts +1 -1
  29. package/dist/template-record.js +3 -2
  30. package/dist/update.js +10 -0
  31. package/package.json +3 -3
  32. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  33. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  35. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  38. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  40. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  41. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  43. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  44. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  46. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  47. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  48. package/templates/.env.example +13 -7
  49. package/templates/.github/workflows/ci.yml +13 -0
  50. package/templates/CLAUDE.md +4 -2
  51. package/templates/README.md +1 -0
  52. package/templates/_gitignore +4 -0
  53. package/templates/apps/api/.env.example +34 -0
  54. package/templates/apps/api/CLAUDE.md +94 -19
  55. package/templates/apps/api/package.json +1 -0
  56. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  57. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  58. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  59. package/templates/apps/api/src/config/index.ts +109 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
  61. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  62. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  63. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  64. package/templates/apps/api/src/lib/site.ts +4 -1
  65. package/templates/apps/api/src/lib/stripe.ts +77 -4
  66. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  67. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  68. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  69. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  70. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  71. package/templates/apps/api/src/openapi/spec.ts +343 -7
  72. package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
  73. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  74. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
  75. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  76. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  77. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  78. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  79. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  80. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  81. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  82. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  83. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  84. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  85. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  87. package/templates/apps/api/src/routes/content/index.ts +16 -2
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  89. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  90. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  91. package/templates/apps/api/src/routes/content/search.ts +16 -45
  92. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  93. package/templates/apps/api/src/routes/leads/index.ts +377 -308
  94. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  95. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  96. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  97. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  98. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  99. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  100. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  101. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
  102. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  103. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  104. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  105. package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
  106. package/templates/apps/api/src/server.ts +165 -51
  107. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  108. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  109. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
  110. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  111. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  112. package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
  113. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
  114. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  115. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  116. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  117. package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
  118. package/templates/apps/api/src/services/entitlements.ts +157 -1
  119. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  120. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
  121. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  122. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
  123. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  124. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
  125. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
  126. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  127. package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
  128. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
  129. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  130. package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
  131. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  132. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  133. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  134. package/templates/apps/api/src/services/person.ts +147 -9
  135. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  136. package/templates/apps/api/src/services/project-listings.ts +43 -31
  137. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  138. package/templates/apps/mcp/CLAUDE.md +32 -18
  139. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  140. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  141. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  142. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  143. package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
  144. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  145. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  146. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  147. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  148. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  149. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  150. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  151. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  152. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  153. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  154. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  155. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  156. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  157. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  158. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  159. package/templates/apps/mcp/package.json +1 -0
  160. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  161. package/templates/apps/mcp/src/http-transport.ts +47 -9
  162. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  163. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  164. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  165. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  166. package/templates/apps/mcp/src/server.ts +5 -0
  167. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  168. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  169. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  170. package/templates/apps/mcp/src/tools/index.ts +22 -21
  171. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  172. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  173. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  174. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  175. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  176. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  177. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  178. package/templates/apps/web/.env.example +10 -31
  179. package/templates/apps/web/CLAUDE.md +324 -42
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  183. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  184. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  185. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  186. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  187. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  188. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  189. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  190. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  191. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  192. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  193. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  194. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  195. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  196. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  197. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
  198. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  199. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  200. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  201. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  202. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  203. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  204. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  205. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  206. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
  207. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  208. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  209. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  210. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  211. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  212. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  213. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  214. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  215. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  216. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  217. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  218. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  219. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  220. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  221. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  222. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  223. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  224. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  225. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  226. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  227. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
  228. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  229. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  230. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  231. package/templates/apps/web/lib/auth-routes.ts +36 -2
  232. package/templates/apps/web/lib/checkout-config.ts +9 -20
  233. package/templates/apps/web/lib/deck-page.ts +1 -0
  234. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  235. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  236. package/templates/apps/web/lib/magnet-routes.ts +44 -0
  237. package/templates/apps/web/lib/modules.generated.ts +9 -0
  238. package/templates/apps/web/lib/newsletter.actions.ts +205 -0
  239. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  240. package/templates/apps/web/lib/posting-title.ts +17 -0
  241. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  242. package/templates/apps/web/messages/en.json +49 -15
  243. package/templates/apps/web/middleware.ts +12 -2
  244. package/templates/apps/web/security-headers.config.ts +17 -0
  245. package/templates/content/en/BlogList/blog.mdx +4 -71
  246. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  247. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  248. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  249. package/templates/content/en/BookCall/start-project.mdx +1 -17
  250. package/templates/content/en/ContactPage/contact.mdx +6 -63
  251. package/templates/content/en/HomePage/home.mdx +3 -21
  252. package/templates/database/CHANGELOG.md +51 -0
  253. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  254. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  255. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  256. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  257. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  258. package/templates/database/__tests__/seed.test.ts +63 -1
  259. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  260. package/templates/database/auth/schema.prisma +4 -0
  261. package/templates/database/config/schema.prisma +91 -0
  262. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  263. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  264. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  265. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  266. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  267. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  268. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  269. package/templates/database/package.json +1 -1
  270. package/templates/database/seed.ts +57 -2
  271. package/templates/tools/lint-architecture.ts +205 -1
  272. package/versions.json +35 -35
  273. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  274. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  275. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  276. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  277. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  278. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  279. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  280. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  281. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  282. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  283. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  284. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  285. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  286. package/templates/apps/web/lib/checkout.ts +0 -93
  287. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -20,22 +20,6 @@ sections:
20
20
  "primaryCta": { "label": "{Get in touch}", "href": "/contact" },
21
21
  },
22
22
  },
23
- {
24
- "id": "add-scheduling",
25
- "type": "cta",
26
- "data":
27
- {
28
- "variant": "box",
29
- "headline": "{What booking an onsite estimate would do for your visitors}",
30
- "description": "{One or two sentences on what live scheduling adds once it's on}",
31
- "primaryCta":
32
- {
33
- "label": "Add scheduling with Pro",
34
- "href": "https://ailaunchkit.ai/pricing",
35
- "external": true,
36
- },
37
- },
38
- },
39
23
  {
40
24
  "id": "back-to-meet",
41
25
  "type": "content-block",
@@ -53,4 +37,4 @@ sections:
53
37
  ]
54
38
  ---
55
39
 
56
- {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A template that carries the scheduling module overrides this file with the live booker. Placeholder copy — replace every {brace} with your own words before you launch. */}
40
+ {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A site that carries the scheduling module renders the hub's meeting page instead, reading this seed only for its URL, its JSON-LD and its fallback title (#8843 C5). Placeholder copy — replace every {brace} with your own words before you launch. */}
@@ -20,22 +20,6 @@ sections:
20
20
  "primaryCta": { "label": "{Get in touch}", "href": "/contact" },
21
21
  },
22
22
  },
23
- {
24
- "id": "add-scheduling",
25
- "type": "cta",
26
- "data":
27
- {
28
- "variant": "box",
29
- "headline": "{What booking an onsite service call would do for your visitors}",
30
- "description": "{One or two sentences on what live scheduling adds once it's on}",
31
- "primaryCta":
32
- {
33
- "label": "Add scheduling with Pro",
34
- "href": "https://ailaunchkit.ai/pricing",
35
- "external": true,
36
- },
37
- },
38
- },
39
23
  {
40
24
  "id": "back-to-meet",
41
25
  "type": "content-block",
@@ -53,4 +37,4 @@ sections:
53
37
  ]
54
38
  ---
55
39
 
56
- {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A template that carries the scheduling module overrides this file with the live booker. Placeholder copy — replace every {brace} with your own words before you launch. */}
40
+ {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A site that carries the scheduling module renders the hub's meeting page instead, reading this seed only for its URL, its JSON-LD and its fallback title (#8843 C5). Placeholder copy — replace every {brace} with your own words before you launch. */}
@@ -20,22 +20,6 @@ sections:
20
20
  "primaryCta": { "label": "{Get in touch}", "href": "/contact" },
21
21
  },
22
22
  },
23
- {
24
- "id": "add-scheduling",
25
- "type": "cta",
26
- "data":
27
- {
28
- "variant": "box",
29
- "headline": "{What booking a project kickoff would do for your visitors}",
30
- "description": "{One or two sentences on what live scheduling adds once it's on}",
31
- "primaryCta":
32
- {
33
- "label": "Add scheduling with Pro",
34
- "href": "https://ailaunchkit.ai/pricing",
35
- "external": true,
36
- },
37
- },
38
- },
39
23
  {
40
24
  "id": "back-to-meet",
41
25
  "type": "content-block",
@@ -53,4 +37,4 @@ sections:
53
37
  ]
54
38
  ---
55
39
 
56
- {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A template that carries the scheduling module overrides this file with the live booker. Placeholder copy — replace every {brace} with your own words before you launch. */}
40
+ {/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A site that carries the scheduling module renders the hub's meeting page instead, reading this seed only for its URL, its JSON-LD and its fallback title (#8843 C5). Placeholder copy — replace every {brace} with your own words before you launch. */}
@@ -1,73 +1,16 @@
1
1
  ---
2
2
  type: "ContactPage"
3
3
  slug: "contact"
4
- title: "Contact {{COMPANY_NAME}}"
5
- description: "Or upgrade to Pro to save every message as a lead in your own marketing database."
4
+ title: "Get in touch"
5
+ description: "{One sentence on who should get in touch and how soon you reply}"
6
6
  sections:
7
7
  [
8
8
  {
9
- "id": "hero",
10
- "type": "hero",
11
- "data":
12
- {
13
- "variant": "split",
14
- "eyebrow": "Contact",
15
- "headline": "Get every message in your email, free",
16
- "subheadline": "Or upgrade to Pro to save every message as a lead in your own marketing database.",
17
- "checklist":
18
- [
19
- "Every message arrives in your email",
20
- "No database or setup to start",
21
- "Upgrade any time to keep a lead inbox",
22
- ],
23
- "secondaryCta":
24
- {
25
- "label": "Save my leads",
26
- "href": "https://ailaunchkit.ai/pricing",
27
- "variant": "outline",
28
- },
29
- },
30
- },
31
- {
32
- "id": "meeting-types",
33
- "type": "items",
34
- "data":
35
- {
36
- "headline": "{Add appointment scheduling to your site}",
37
- "variant": "tiles",
38
- "columns": 2,
39
- "items":
40
- [
41
- {
42
- "title": "Book a Call",
43
- "description": "{A quick intro call.}",
44
- "link": { "label": "Book a Call", "href": "/meet/book-a-call" },
45
- },
46
- {
47
- "title": "Onsite Estimate",
48
- "description": "{We come to you and quote the work.}",
49
- "link":
50
- {
51
- "label": "Onsite Estimate",
52
- "href": "/meet/onsite-estimate",
53
- },
54
- },
55
- {
56
- "title": "Start Project",
57
- "description": "{Kick off a project you're ready to start.}",
58
- "link":
59
- { "label": "Start Project", "href": "/meet/start-project" },
60
- },
61
- {
62
- "title": "Onsite Service",
63
- "description": "{Book a service visit at your location.}",
64
- "link":
65
- { "label": "Onsite Service", "href": "/meet/onsite-service" },
66
- },
67
- ],
68
- },
9
+ "id": "contact-form",
10
+ "type": "contact-form",
11
+ "data": { "variant": "column", "fields": [] },
69
12
  },
70
13
  ]
71
14
  ---
72
15
 
73
- {/* Rendered from frontmatter `sections[]`. The page's one contact surface — the plain contact form — is mounted by the /contact route itself, in the split hero's right column, and posts to the site's own API (`/v1/leads/submit`). Placeholder copy — replace before you launch. */}
16
+ {/* Rendered from frontmatter `sections[]`. The page's one contact surface is the workspace's Contact form magnet: its heading, sentence, fields, consent, button and ending all come from the magnet, mounted by the /contact route in this section's slot. To change the form, change the magnet in Admin → Magnets. The `description` is the page's meta description — replace it before you launch. */}
@@ -39,27 +39,9 @@ sections:
39
39
  "headlineStyle": "label",
40
40
  "logos":
41
41
  [
42
- {
43
- "name": "SalesSmyth",
44
- "src": "/logos/salessmyth.svg",
45
- "darkSrc": "/logos/salessmyth-light.svg",
46
- "width": 1118,
47
- "height": 147,
48
- },
49
- {
50
- "name": "AI Launch Kit",
51
- "src": "/logos/ai-launch-kit.svg",
52
- "darkSrc": "/logos/ai-launch-kit-light.svg",
53
- "width": 1248,
54
- "height": 144,
55
- },
56
- {
57
- "name": "Atomic Agent",
58
- "src": "/logos/atomic-agent.svg",
59
- "darkSrc": "/logos/atomic-agent-light.svg",
60
- "width": 1320,
61
- "height": 148,
62
- },
42
+ { "name": "Placeholder logo", "src": "/logomark.svg" },
43
+ { "name": "Placeholder logo", "src": "/logomark.svg" },
44
+ { "name": "Placeholder logo", "src": "/logomark.svg" },
63
45
  ],
64
46
  "layout": "inline",
65
47
  "treatment": "monochrome",
@@ -1,5 +1,56 @@
1
1
  # @working-theory/database
2
2
 
3
+ ## 0.45.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#9309](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/9309) [`42b37e2`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/42b37e237d0f424573346b158290f0ecc4ae77a4) Thanks [@samuelhenry](https://github.com/samuelhenry)! - Every workspace now holds a standard trial magnet at the fixed slug `trial` ([#9301](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/9301)).
8
+
9
+ - **`@working-theory/validation`**: new exports `TRIAL_FLOW_SLUG`, `TRIAL_FLOW_TITLE`, `TRIAL_FLOW_LANDING_PAGE`, `TRIAL_TERMS_STATEMENT` and `trialFlowConfig` — one step asking for first name, last name, work email and the terms box, ending on "Thanks, {name}.", with a landing whose button reads "Request a free trial". `RESERVED_FLOW_SLUGS` gains `trial`, so no flow-engine write, spoke seed or new listing can take the slug (a new listing named "Trial" now derives `trial-2`).
10
+ - **`@working-theory/database`**: the default workspace's seed writes the trial magnet (`diagnostic`, `live`, completion `lead`, landing page `/trial`), and the `20261025120000_trial_magnet` migration backfills it into every existing workspace, insert-only — a row already at `trial` is never changed.
11
+ - **`create-ailk`**: the API seeds the trial magnet when it creates a workspace and from `seed-presets`, and the magnet PATCH refuses to rename the trial magnet or rename another magnet to `trial` (`409`). A completed walk is a `flow:trial` Inbox submission whose Person carries the given name, family name, email and, when a site adds a `company`-tagged field, the company.
12
+
13
+ - [#9330](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/9330) [`569b348`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/569b348a95aa7b1b414ed62151d810e41877f238) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The trial magnet gets its page pair: `/trial` and `/trial/request` ([#9302](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/9302)).
14
+
15
+ - **`create-ailk`**: a site with the `signal-collectors` module carries `/trial`, the indexable landing inside the site's Nav and Footer — the trial magnet's landing copy, one button ("Request a free trial") to `/trial/request`, and the `Assessment` JSON-LD — and `/trial/request`, the full-screen, `noindex, follow` walk of the trial magnet with a back arrow to `/trial`. When the trial flow is absent or the hub cannot answer, the walk shows the site's contact email under its own "Free trial" heading instead of a 404. The magnet landing renderer takes an optional `startPath` for a walk not at `<path>/start`.
16
+ - **`@working-theory/validation`**: `trialFlowConfig.landing` gains its proof line, `meta`: "A person on our team reviews every request." It is true for every site, because only an operator starts a trial, from the Inbox.
17
+ - **`@working-theory/database`**: the `20261026120000_trial_magnet_proof_line` migration gives that line to every workspace's trial magnet that still holds [#9301](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/9301)'s exact default. A trial magnet an owner has edited is never touched.
18
+ - **`@working-theory/templates`**: `ROUTES` gains the `/trial` and `/trial/request` rows (module `signal-collectors`).
19
+
20
+ - Updated dependencies [[`1d9b675`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1d9b6755b3fbffebfcd7c57a93ea7cbc9941bba0), [`875714a`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/875714ad871e2b9b3778383ed106e8a1586fe3a6), [`42b37e2`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/42b37e237d0f424573346b158290f0ecc4ae77a4), [`569b348`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/569b348a95aa7b1b414ed62151d810e41877f238), [`d648b1a`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d648b1a98a84dfee56f6847e7ec5f9fd0421a163), [`5008d1d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5008d1d13283fe5ed04504dcfb17122d18892a18), [`a5c5d1c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/a5c5d1cc3443394d0da9507c7b11954b54e8c0da), [`67e75c8`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/67e75c87519235435f578ee22c2cf8fa2f74f17d), [`16c889b`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/16c889b7434ad49c3b18f900c42779c72e78f233)]:
21
+ - @working-theory/validation@0.45.0
22
+
23
+ ## 0.44.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [#8955](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8955) [`1a84162`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1a841626a9e5664d926a2f186425f35e8757a6b4) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The Offerings data model and its typed read services ([#8876](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8876)).
28
+
29
+ - **`@working-theory/database`** — three config tables, in migration `20261020120000_offerings_data_model`: `StripeConnection` (an Organization's own Stripe account by a restricted key — mode, last four, the key and webhook secret as ciphertext the database refuses in clear, a webhook token), `OrganizationOfferings` (the Products an Organization has switched on) and `WorkspaceOfferings` (what a workspace shows, its Recommended plan and add-on, and the last-good Stripe snapshot). Deploy the migration before the API. No existing table is touched.
30
+ - **`@working-theory/ids`** — registers the `stripecon`, `orgoffer` and `wsoffer` prefixes for the new models.
31
+ - **`@working-theory/validation`** — the Offerings data contract: `parseLookupKey` (the one lookup-key parser, `<plan>_<monthly|yearly|once>`), `periodOfStripePrice`, the text bounds, and the typed shapes `offeringsProductSchema`, `offeringsSnapshotSchema`, `orgEnabledProductsSchema`, `workspaceShownSetSchema` and `stripeConnectionSummarySchema`.
32
+ - **`create-ailk`** — a scaffold's `apps/api` carries the Offerings read services (`getOrgEnabledProducts`, `getWorkspaceShownSet`, `getSellableProductIds`, the bounded snapshot refresh and the connection summary). No route reads them yet, so a site's behaviour is unchanged.
33
+
34
+ - [#8968](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8968) [`8da1163`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8da1163fd04f083fd8d408a4a7d762b756b23274) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The Offerings Checkout session record ([#8930](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8930)).
35
+
36
+ - **`@working-theory/database`** — one config table, in migration `20261021120000_offerings_checkout_session`: `OfferingsCheckoutSession`, one row per Checkout Session the hub created (its id, workspace, Organization, Price, lookup key and mode), with a CHECK on every column but `created_at`. Deploy the migration before the API. No existing table is touched.
37
+ - **`@working-theory/ids`** — registers the `ocsess` prefix for the new model.
38
+ - **`create-ailk`** — a scaffold's `apps/api` carries the record's one store (`recordCheckoutSession`, `readCheckoutSessionRecord`). No route calls it yet, so a site's behaviour is unchanged.
39
+
40
+ - [#9091](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/9091) [`23cf71c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/23cf71c316c9e44c6109c76439cb64281e6368d5) Thanks [@samuelhenry](https://github.com/samuelhenry)! - A workspace's Offerings shown set is chosen per Stripe Price, and the Admin's Offerings page reads like Magnets ([#9070](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/9070)).
41
+
42
+ - **`@working-theory/database`** — migration `20261023120000_offerings_shown_price_ids` adds `workspace_offerings.shown_price_ids TEXT[] NOT NULL DEFAULT '{}'` and two CHECKs (at most 300 non-null ids; Price ids only beside a shown Product). No row is rewritten: an empty array means every Price of each shown Product, so every workspace saved before this release shows exactly what it showed before. **Deploy the migration before the API, and the API before the Admin.** While the API and the Admin are on different versions the Offerings page cannot save (nothing is written and nothing new is sold).
43
+ - **`@working-theory/validation`** — new exports `OFFERINGS_PRICE_IDS_MAX`, `offeringsPriceIdsSchema`, `workspaceOfferingsShownEntrySchema` and `workspaceOfferingsShownSchema`. Changed shapes: `workspaceOfferingsSelectionInputSchema` is now one Price's switch, `{ productId, period, shown }`, so no body names a Recommended or a whole set; `workspaceOfferingsSelectionSchema` gains `shownPriceIds`, empty exactly when `shownProductIds` is; `workspaceOfferingsViewSchema`'s available branch answers `shown` (`{ productId, periods }` per Product) in place of `shownProductIds`.
44
+ - **`create-ailk`** — a scaffold's `PUT /v1/workspaces/:workspaceId/offerings` takes one Price per save and keeps or clears a stored Recommended but never sets one (`recommended_not_shown` is retired; a period the Product has no Price for is `400 price_not_offered`). Its audit row's `shown`, `added` and `removed` count Prices, not Products, from this release. `/pricing`, the agent pricing tools, the plan picker and checkout show and sell only the shown Prices, so a seller can show a yearly Price without its monthly sibling; a Price replaced in Stripe stays off until it is switched on. The Admin's Offerings page is a Title / Tags / Live table, one row per Price, with a New recommender button and no Recommended control.
45
+
46
+ **Rollback.** Roll the API back, then run `UPDATE workspace_offerings SET shown_price_ids = '{}'` so every row reads at the Product grain again and the old route's saves succeed. Tell each owner who saved per Price that every Price of their shown Products — on `/pricing` and at checkout — is on sale again. Never drop the column while a per-Price row exists without that notice.
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies [[`6ad87d6`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/6ad87d65d91013ac9c0945a62efbf28c627b1fce), [`d9c2cd3`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d9c2cd3c601d9a267d4d29cb9bcef77ec71cd518), [`407810f`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/407810fbd63a02d14773aef36a10c1f9f04dd5e3), [`5875c18`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5875c189f72f66ea2af7b7c79a47baba53a329d1), [`13c0b3d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/13c0b3dd64a105292962821f5b0050c5cabc2d45), [`82bc2f1`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/82bc2f1ecab1a12f23a45e326355841ba87d9f04), [`ea18e21`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/ea18e2181241ba9357b4dcfa3a1d2f4c3ffe4518), [`8754f86`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8754f866b49003965c4be28cbe19b1f040aec4af), [`1a84162`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1a841626a9e5664d926a2f186425f35e8757a6b4), [`74cb7b3`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/74cb7b37adea5d27adc1f3ec3ea58b9941167c4d), [`25b4b61`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/25b4b6199d8632167bd5501a4e12d4774eed432a), [`8da1163`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8da1163fd04f083fd8d408a4a7d762b756b23274), [`bd1c9b5`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/bd1c9b576ac337596e585145f09b08696e9f412e), [`d1dacc5`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d1dacc5d76a9cc91f6ad3b1883067e327b5e3d3c), [`fff5487`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/fff5487a6d14a3010bf22cb99e7554e6542449a0), [`23cf71c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/23cf71c316c9e44c6109c76439cb64281e6368d5), [`75ecc58`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/75ecc584f43793bb5234f69e0f80b15327a99236), [`85ae357`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/85ae357410ebd6c11b08b04355a9ede1d2088d2d), [`059c892`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/059c89249f7db3dc106f133ef37180e9eb1e318a), [`c563360`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/c563360db386758aa2d25309ab693a18a41e4139)]:
51
+ - @working-theory/validation@0.44.0
52
+ - @working-theory/ids@0.15.0
53
+
3
54
  ## 0.43.0
4
55
 
5
56
  ### Minor Changes
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file in-page-magnets-migration.test.ts
3
+ *
4
+ * DB-free: migrations/20261022120000_in_page_magnets writes two configs as
5
+ * JSONB literals, because SQL cannot import TS (#8847 spec
6
+ * docs/specs/8847-the-in-page-form-shape-spec.md D5; AC-3, AC-5). This test
7
+ * holds each literal to its one TS source so they can never drift, as
8
+ * `member-contract-migration.test.ts` does for the request-quote backfill:
9
+ *
10
+ * - `newsletter backfill` inserts `newsletterFlowConfig` into every
11
+ * workspace, insert-only on `(site_id, slug)`;
12
+ * - `contact-form upgrade` replaces a stored Contact form with
13
+ * `contactFormFlowConfig` ONLY where its config equals #8661's shipped
14
+ * default as JSONB, so an owner's edit (or a wizard-built Contact form) is
15
+ * never touched, and it sets nothing but `config` and `updated_at`.
16
+ */
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import {
21
+ CONTACT_FORM_FLOW_SLUG,
22
+ contactFormFlowConfig,
23
+ flowConfigSchema,
24
+ NEWSLETTER_FLOW_SLUG,
25
+ NEWSLETTER_FLOW_TITLE,
26
+ NEWSLETTER_LANDING_PAGE,
27
+ newsletterFlowConfig,
28
+ STANDARD_MAGNET_COMPLETIONS,
29
+ } from "@working-theory/validation";
30
+
31
+ const MIGRATION_SQL = readFileSync(
32
+ join(__dirname, "..", "migrations", "20261022120000_in_page_magnets", "migration.sql"),
33
+ "utf8",
34
+ );
35
+
36
+ /**
37
+ * #8661's shipped default Contact form — `contactFormFlowConfig` as it stood
38
+ * at commit e34e7468^ (`git show e34e7468^:packages/validation/src/schemas/flow.ts`),
39
+ * the only shape the #8661 seed ever wrote. Written out here, not imported:
40
+ * the constant has moved on, and this is the value the upgrade's guard must
41
+ * match exactly.
42
+ */
43
+ const CONTACT_FORM_8661_DEFAULT = {
44
+ slug: "contact",
45
+ defaultLocale: "en",
46
+ steps: [
47
+ {
48
+ key: "contact",
49
+ type: "field",
50
+ copy: { title: { en: "Contact form" }, cta: { en: "Send message" } },
51
+ fields: [
52
+ { name: "name", inputType: "text", required: true, mapsTo: "name", copy: { label: { en: "Name" } } },
53
+ { name: "email", inputType: "email", required: true, mapsTo: "email", copy: { label: { en: "Email" } } },
54
+ {
55
+ name: "subject",
56
+ inputType: "select",
57
+ required: false,
58
+ options: [
59
+ { value: "General question", label: { en: "General question" } },
60
+ { value: "Working together", label: { en: "Working together" } },
61
+ { value: "Support", label: { en: "Support" } },
62
+ { value: "Press and partnerships", label: { en: "Press and partnerships" } },
63
+ { value: "Other", label: { en: "Other" } },
64
+ ],
65
+ copy: { label: { en: "Subject" }, placeholder: { en: "Choose a subject" } },
66
+ },
67
+ { name: "message", inputType: "textarea", required: false, copy: { label: { en: "Message" } } },
68
+ {
69
+ name: "terms",
70
+ inputType: "consent",
71
+ required: true,
72
+ purposes: ["service_terms"],
73
+ copy: {
74
+ label: {
75
+ en: "I accept the [Terms](terms), [Privacy Policy](privacy), and [Working Agreement](working_agreement).",
76
+ },
77
+ },
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ };
83
+
84
+ /** The text between `-- BEGIN <name>` and `-- END <name>`. */
85
+ function block(name: string): string {
86
+ const match = new RegExp(`-- BEGIN ${name}\\n([\\s\\S]*?)-- END ${name}`).exec(MIGRATION_SQL);
87
+ if (!match?.[1]) throw new Error(`${name} markers not found`);
88
+ return match[1];
89
+ }
90
+
91
+ /** A SQL string literal's body, `''` unescaped, parsed as JSON. */
92
+ const parseLiteral = (body: string): unknown => JSON.parse(body.replace(/''/g, "'"));
93
+
94
+ /** The block with every string literal blanked, so only SQL structure is left. */
95
+ const withoutLiterals = (sql: string): string => sql.replace(/'(?:[^']|'')*'/g, "''");
96
+
97
+ describe("the newsletter backfill (#8847 D5, AC-5)", () => {
98
+ const sql = block("newsletter backfill");
99
+
100
+ it("carries exactly one JSONB config literal, and it deep-equals newsletterFlowConfig", () => {
101
+ const literals = [...sql.matchAll(/'(\{[\s\S]*?\})'::jsonb/g)];
102
+ expect(literals).toHaveLength(1);
103
+ const parsed = parseLiteral(literals[0]![1]!);
104
+ expect(parsed).toEqual(newsletterFlowConfig);
105
+ expect(flowConfigSchema.safeParse(parsed).success).toBe(true);
106
+ });
107
+
108
+ it("writes the constants' slug, title and landing page, kind newsletter, live, the lead completion, into every workspace", () => {
109
+ expect(sql).toContain(
110
+ `INSERT INTO "flow_definitions" ("site_id", "workspace_id", "slug", "config", "kind", "title", "status", "customers_enabled", "completion", "landing_page", "updated_at")`,
111
+ );
112
+ expect(sql).toContain(`SELECT w."id", w."id", '${NEWSLETTER_FLOW_SLUG}',`);
113
+ expect(STANDARD_MAGNET_COMPLETIONS[NEWSLETTER_FLOW_SLUG]).toBe("lead");
114
+ expect(sql).toContain(
115
+ `'newsletter', '${NEWSLETTER_FLOW_TITLE}', 'live', false, 'lead', '${NEWSLETTER_LANDING_PAGE}', CURRENT_TIMESTAMP`,
116
+ );
117
+ expect(sql).toContain(`FROM "workspaces" w`);
118
+ });
119
+
120
+ it("ends ON CONFLICT (\"site_id\", \"slug\") DO NOTHING, so an existing row is never overwritten", () => {
121
+ expect(sql.trimEnd().endsWith(`ON CONFLICT ("site_id", "slug") DO NOTHING;`)).toBe(true);
122
+ expect(sql).not.toMatch(/DO UPDATE/i);
123
+ });
124
+ });
125
+
126
+ describe("the contact-form upgrade (#8847 D5, C5, AC-3)", () => {
127
+ const sql = block("contact-form upgrade");
128
+ const statements = withoutLiterals(sql).split(";").filter((s) => s.trim().length > 0);
129
+
130
+ it("is one UPDATE of flow_definitions", () => {
131
+ expect(statements).toHaveLength(1);
132
+ expect(statements[0]!.trim()).toMatch(/^UPDATE "flow_definitions" SET /);
133
+ });
134
+
135
+ it("its SET literal deep-equals contactFormFlowConfig", () => {
136
+ const set = /SET "config" = '(\{[\s\S]*?\})'::jsonb/.exec(sql);
137
+ expect(set?.[1]).toBeDefined();
138
+ const parsed = parseLiteral(set![1]!);
139
+ expect(parsed).toEqual(contactFormFlowConfig);
140
+ expect(flowConfigSchema.safeParse(parsed).success).toBe(true);
141
+ });
142
+
143
+ it("its WHERE names the contact slug and compares config with = against a ::jsonb literal equal to #8661's default", () => {
144
+ const where = new RegExp(
145
+ `WHERE "slug" = '${CONTACT_FORM_FLOW_SLUG}' AND "config" = '(\\{[\\s\\S]*?\\})'::jsonb;`,
146
+ ).exec(sql);
147
+ expect(where?.[1]).toBeDefined();
148
+ expect(parseLiteral(where![1]!)).toEqual(CONTACT_FORM_8661_DEFAULT);
149
+ // The guard is a different value from what it writes, so a re-run matches nothing.
150
+ expect(CONTACT_FORM_8661_DEFAULT).not.toEqual(contactFormFlowConfig);
151
+ expect(flowConfigSchema.safeParse(CONTACT_FORM_8661_DEFAULT).success).toBe(true);
152
+ });
153
+
154
+ it("sets no column besides config and updated_at", () => {
155
+ const setClause = /SET ([\s\S]*?)\s+WHERE /.exec(withoutLiterals(sql))?.[1];
156
+ expect(setClause).toBeDefined();
157
+ const columns = [...setClause!.matchAll(/"([a-z_]+)"\s*=/g)].map((m) => m[1]);
158
+ expect(columns).toEqual(["config", "updated_at"]);
159
+ expect(setClause).toContain(`"updated_at" = CURRENT_TIMESTAMP`);
160
+ });
161
+ });