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
@@ -49,6 +49,22 @@ export class SeedFlowsRefusedError extends Error {
49
49
  this.name = "SeedFlowsRefusedError";
50
50
  }
51
51
  }
52
+ /** A starter placeholder string: one `{…}` brace pair and nothing else. */
53
+ const PLACEHOLDER_STRING = /^\{[^{}]+\}$/;
54
+ /**
55
+ * Whether a flow config carries the starter's placeholder copy (#8850 D6):
56
+ * some string value anywhere in it is a whole `{brace}` placeholder.
57
+ */
58
+ function isPlaceholderFlow(config) {
59
+ if (typeof config === "string")
60
+ return PLACEHOLDER_STRING.test(config);
61
+ if (Array.isArray(config))
62
+ return config.some(isPlaceholderFlow);
63
+ if (config !== null && typeof config === "object") {
64
+ return Object.values(config).some(isPlaceholderFlow);
65
+ }
66
+ return false;
67
+ }
52
68
  function readDeclaredFlows(flowsDir) {
53
69
  if (!existsSync(flowsDir))
54
70
  return [];
@@ -78,7 +94,12 @@ function readDeclaredFlows(flowsDir) {
78
94
  if (slug !== expected) {
79
95
  throw new SeedFlowsRefusedError(`seed-flows: flows/${file} declares slug "${slug}"; a declared flow's file is named <slug>.json. Nothing was sent.`);
80
96
  }
81
- return { slug, file, config: config };
97
+ return {
98
+ slug,
99
+ file,
100
+ config: config,
101
+ placeholder: isPlaceholderFlow(config),
102
+ };
82
103
  });
83
104
  }
84
105
  // ── The hub origin ──────────────────────────────────────────────────────────
@@ -158,6 +179,7 @@ async function sendFlow(fetchImpl, origin, key, flow) {
158
179
  ok: true,
159
180
  result,
160
181
  status: safeCode(envelope.data?.status),
182
+ ...(flow.placeholder && { placeholder: true }),
161
183
  };
162
184
  }
163
185
  return { slug: flow.slug, ok: false, failure: "200 unknown" };
@@ -175,12 +197,15 @@ function hubLine(hub) {
175
197
  : `hub: ${hub.origin} (from ${hub.source})`;
176
198
  }
177
199
  function flowLine(outcome) {
178
- if (outcome.ok === "dry-run")
179
- return `${outcome.slug}: would send`;
180
200
  if (outcome.ok === false)
181
201
  return `${outcome.slug}: failed — ${outcome.failure}`;
202
+ const placeholder = outcome.placeholder === true
203
+ ? ` — placeholder copy: replace every {brace} in flows/${outcome.slug}.json`
204
+ : "";
205
+ if (outcome.ok === "dry-run")
206
+ return `${outcome.slug}: would send${placeholder}`;
182
207
  const draft = outcome.status === "draft" ? " (status draft — not shown to visitors)" : "";
183
- return `${outcome.slug}: ${outcome.result}${draft}`;
208
+ return `${outcome.slug}: ${outcome.result}${draft}${placeholder}`;
184
209
  }
185
210
  /** The whole report, one line per row — what `seedFlows` printed as it went. */
186
211
  export function formatSeedFlowsReport(result) {
@@ -224,6 +249,7 @@ export async function seedFlows(opts) {
224
249
  const outcomes = flows.map((flow) => ({
225
250
  slug: flow.slug,
226
251
  ok: "dry-run",
252
+ ...(flow.placeholder && { placeholder: true }),
227
253
  }));
228
254
  onLine(hubLine(hub));
229
255
  outcomes.forEach((outcome) => onLine(flowLine(outcome)));
@@ -282,6 +308,26 @@ The hub's operator must first turn seeding on (\`AILK_SPOKE_FLOW_SEED=true\`
282
308
  on the hub). Until it is on and this command has run, links to these flows
283
309
  (\`/flows/<slug>\`) reach no flow.
284
310
 
311
+ ## The starter's placeholder flows
312
+
313
+ The \`signal-collectors\` module ships two declared flows here:
314
+ \`flows/diagnostic.json\` (an email step, then two questions) and
315
+ \`flows/join-waitlist.json\` (an email and an optional name). Every visible
316
+ string in both is a \`{brace}\` placeholder: replace each one with your own
317
+ copy. Both go live on the hub with the first seed, so each line this command
318
+ prints for them says they still carry placeholder copy.
319
+
320
+ A flow is reached at \`/flows/<slug>\`. To give one its own page pair, run:
321
+
322
+ npx create-ailk add-route magnet --path /diagnostic --flow diagnostic
323
+ npx create-ailk add-route magnet --path /waitlist --flow join-waitlist
324
+
325
+ A job application comes with the \`careers\` surface. Contact, newsletter and
326
+ job application are the hub's own standard magnets: the hub seeds them in
327
+ every workspace and refuses a spoke's copy of each, so they need no file
328
+ here. If the hub's Admin already owns a magnet under one of these two slugs,
329
+ the hub refuses the seed (\`slug_owned\`); rename the file and its \`slug\`.
330
+
285
331
  ## Who owns what
286
332
 
287
333
  The next seed that differs overwrites the hub's copy of a flow's config, so
@@ -2,26 +2,26 @@ import type { EnvCatalogEntry, PlanEnvRowsOptions } from "./lib/declared-env.js"
2
2
  import { type InstallReportRow } from "./lib/extract-module-bundle.js";
3
3
  import { type WorkspaceVersionMap } from "./substitute.js";
4
4
  import { type SignInMode } from "./topology.js";
5
- /** The checkout package a spoke's two route files import. */
5
+ /** The checkout package a spoke's billing action and thank-you page import. */
6
6
  export declare const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
7
- /** The site's checkout data — written once, the site's from then on (#8748 D2). */
7
+ /** The site's checkout stamp — written once, the site's from then on (#8748 D2). */
8
8
  export declare const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
9
- /** The two return pages: Stripe's success URL and every refused buy's target. */
9
+ /** The two return pages a commerce spoke still carries (#8748 D5). */
10
10
  export declare const SPOKE_CHECKOUT_PAGE_FILES: readonly string[];
11
11
  /**
12
- * The standard spoke checkout's template paths, scaffold-relative (#8748 D3).
13
- * A directory entry covers every file under it, so the tests that sit beside
14
- * the routes and pages travel with them. `copyTemplate` skips all of them for
15
- * every scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
12
+ * The spoke checkout's template paths, scaffold-relative (#8748 D3). A
13
+ * directory entry covers every file under it, so the tests that sit beside
14
+ * the pages travel with them. `copyTemplate` skips all of them for every
15
+ * scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
16
16
  */
17
17
  export declare const SPOKE_CHECKOUT_TEMPLATE_PATHS: readonly string[];
18
- /** The variables a commerce spoke depends on (D9). The package reads three of them. */
18
+ /** The variables a commerce spoke depends on (D9): the package reads both. */
19
19
  export declare const SPOKE_CHECKOUT_ENV_CATALOG: Readonly<Record<string, EnvCatalogEntry>>;
20
20
  /** The names a commerce spoke declares although only the package reads them (D9). */
21
21
  export declare const SPOKE_CHECKOUT_DECLARED_ENV: readonly string[];
22
22
  /**
23
23
  * The catalog (and declared names) a spoke's `appendEnvRows` call passes: a
24
- * commerce spoke adds the checkout's four rows (D9). `ENV_CATALOG` itself is
24
+ * commerce spoke adds the package's two rows (D9). `ENV_CATALOG` itself is
25
25
  * unchanged — its test fails on an entry the monorepo's apps never read.
26
26
  */
27
27
  export declare function spokeEnvRowOptions(commerce: boolean): Pick<PlanEnvRowsOptions, "catalog" | "declare">;
@@ -32,33 +32,22 @@ export declare function spokeEnvRowOptions(commerce: boolean): Pick<PlanEnvRowsO
32
32
  * `site-` and the first 8 hex characters of its SHA-256. Never `AILK_SITE_ID`.
33
33
  */
34
34
  export declare function spokeCheckoutSite(projectName: string): string;
35
- /** Where a commerce spoke's end-to-end checkout workflow lives. */
36
- export declare const SPOKE_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
35
+ /** The billing action's first line, which marks the file as create-ailk's to rewrite. */
36
+ export declare const BILLING_ACTION_MARKER = "// The spoke billing action \u2014 GENERATED by create-ailk (#8217). Do not edit:";
37
37
  /**
38
- * The Stripe CLI release the workflow installs, and the SHA-256 of its
39
- * `stripe_<v>_linux_x86_64.tar.gz` from that release's own
40
- * `stripe-linux-checksums.txt`. Never bump one without re-reading the other.
38
+ * The spoke billing action (#8217 D6): wiring only, byte-identical across
39
+ * sites, so every guarantee arrives by a package bump. A plain server module
40
+ * (C2): its two exports take no argument, and the hub's read is its only
41
+ * source of customer ids. It builds its config from the site's stamp and the
42
+ * site's own origin (#9142: no longer from `lib/checkout.ts`).
41
43
  */
42
- export declare const STRIPE_CLI_VERSION = "1.51.1";
43
- export declare const STRIPE_CLI_LINUX_X64_SHA256 = "38c8de73a8cab810a4aab8a4015cd4e3398b7b87aa42b6944d09d89ed524ac53";
44
- /** The Playwright the workflow installs — the monorepo's own pinned version. */
45
- export declare const E2E_PLAYWRIGHT_VERSION = "1.61.1";
46
- /**
47
- * Every action the workflow uses, pinned to the commit its `v4` tag named
48
- * when this was written (SEC-7839-2): `gh api repos/<action>/commits/v4`.
49
- */
50
- export declare const E2E_ACTION_PINS: Readonly<Record<string, string>>;
51
- /**
52
- * The workflow text (D8). Identical for every spoke at one create-ailk
53
- * version. Every `${{ … }}` sits in an env-style value, never in a script.
54
- */
55
- export declare const CHECKOUT_E2E_WORKFLOW: string;
44
+ export declare const BILLING_ACTION: string;
56
45
  /**
57
46
  * The rendered part of the slice for `site` (#8748 D3): with `signIn: "hub"`
58
- * the billing action (#8217 D8), then the checkout e2e workflow (#7839 D8).
59
- * Neither depends on `site`. The rest of the slice is template source,
60
- * copied by `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid
61
- * stamp (SEC-7838-10).
47
+ * the billing action (#8217 D8), else nothing. It does not depend on `site`.
48
+ * The rest of the slice is template source, copied by
49
+ * `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid stamp
50
+ * (SEC-7838-10).
62
51
  */
63
52
  export declare function renderSpokeCheckoutFiles(site: string, opts?: {
64
53
  signIn?: SignInMode | undefined;
@@ -76,9 +65,10 @@ export declare function renderSpokeCheckoutFiles(site: string, opts?: {
76
65
  * action, and the web seam — generated, so always rewritten — becomes the
77
66
  * billing render: reported `patched` when it changed, not at all when equal.
78
67
  * Adds the package to `apps/web/package.json` when absent, and raises a plain
79
- * pin older than the one it writes (#7839): the generated checkout e2e
80
- * workflow runs a command a `^0.2.0` pin never reaches. Any other range —
81
- * newer, `workspace:`, a tag — is the site's and is kept.
68
+ * pin older than the one it writes (#7839): the rendered billing action
69
+ * calls the package's current `createSpokeCheckout` shape, which an older
70
+ * pin does not take (#9142). Any other range — newer, `workspace:`, a tag —
71
+ * is the site's and is kept.
82
72
  * Every refusal (an invalid stamp, a version map without the package, a
83
73
  * missing or malformed `apps/web/package.json`, a template missing any
84
74
  * `SPOKE_CHECKOUT_TEMPLATE_PATHS` entry, a template config without exactly
@@ -101,3 +91,16 @@ export declare function writeSpokeCheckoutSlice(targetDir: string, opts: {
101
91
  * dependency, or without `apps/web/package.json`, is left as it is.
102
92
  */
103
93
  export declare function dropSpokeCheckoutDependency(targetDir: string): void;
94
+ /**
95
+ * What the legacy env-key checkout left in a spoke, scaffold-relative: the
96
+ * buy route and the Stripe webhook route (a directory entry covers the tests
97
+ * beside each), `lib/checkout.ts` (the fulfilment) and its test. Each was a
98
+ * template copy create-ailk recorded in `.ailk-template-files.json`;
99
+ * `create-ailk sync-routes --apply` removes the ones still byte-identical to
100
+ * that record.
101
+ */
102
+ export declare const RETIRED_SPOKE_CHECKOUT_PATHS: readonly string[];
103
+ /** The retired checkout e2e workflow — rendered, never recorded (#7839 D8). */
104
+ export declare const RETIRED_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
105
+ /** The workflow's first line, which marks it as create-ailk's to remove. */
106
+ export declare const RETIRED_CHECKOUT_E2E_MARKER = "# Checkout e2e \u2014 GENERATED by create-ailk (#7839). Do not edit: it changes";
@@ -1,21 +1,24 @@
1
1
  /**
2
2
  * The spoke checkout slice (#7838, #8748) — what a spoke carrying `commerce`
3
- * gets in `apps/web`: the standard spoke checkout, copied from the template
4
- * tree. That is the buy route and the Stripe webhook route (allowed to import
5
- * the checkout package, #7873), `lib/checkout.ts` (the standard fulfilment),
6
- * the site's own `lib/checkout-config.ts`, the thank-you and unavailable
7
- * return pages and their tests (`SPOKE_CHECKOUT_TEMPLATE_PATHS`). A spoke that
8
- * signs in through its hub also gets the billing action, the third file
9
- * allowed to import the package, and the web seam render that reaches it
10
- * (#8217 D6–D8); that action and the checkout e2e workflow are still rendered
11
- * here.
3
+ * gets in `apps/web`, copied from the template tree: the site's own
4
+ * `lib/checkout-config.ts` (its stamp) and the thank-you and unavailable
5
+ * return pages with their tests (`SPOKE_CHECKOUT_TEMPLATE_PATHS`). A spoke
6
+ * that signs in through its hub also gets the billing action, the one file
7
+ * allowed to import the package besides the thank-you page, and the web seam
8
+ * render that reaches it (#8217 D6–D8); that action is rendered here.
9
+ *
10
+ * A spoke sells through its hub's Offerings buy route
11
+ * (`apps/web/app/api/offerings/checkout/[lookupKey]/route.ts`), never its own
12
+ * key. The legacy env-key buy route, its Stripe webhook route, `lib/checkout.ts`
13
+ * (the fulfilment) and the checkout e2e workflow are retired (#9142):
14
+ * `create-ailk sync-routes --apply` removes them from an existing spoke
15
+ * (`RETIRED_SPOKE_CHECKOUT_PATHS`, sync-routes.ts).
12
16
  *
13
17
  * Every guarantee lives in `@working-theory/spoke-checkout` and arrives by a
14
18
  * package bump; the copied files are `apps/web` source, tested in the
15
19
  * monorepo and kept current by `create-ailk update`, and apart from the
16
20
  * config's one stamp line they are byte-identical across spokes at one
17
- * create-ailk version (roadmap D3, AC-SPOKE-02). A hub never gets them: its
18
- * checkout is `apps/api`'s.
21
+ * create-ailk version (roadmap D3, AC-SPOKE-02). A hub never gets them.
19
22
  *
20
23
  * Spec: docs/specs/7838-spoke-web-checkout-slice-spec.md (D3–D7, D9);
21
24
  * docs/specs/8748-a-standard-spoke-checkout-route-spec.md (D2, D3).
@@ -30,62 +33,50 @@ import { createHash } from "node:crypto";
30
33
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
31
34
  import { dirname, join, resolve, sep } from "node:path";
32
35
  import { buildScaffoldMarkers, renderTemplateFile, templateDestination } from "./copy.js";
33
- import { SPOKE_CHECKOUT_ROUTE_FILES } from "./derive-architecture.js";
36
+ import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
34
37
  import { ENV_CATALOG } from "./lib/env-catalog.js";
35
38
  import { diffTwoStrings, } from "./lib/extract-module-bundle.js";
36
39
  import { substituteWorkspaceVersions, } from "./substitute.js";
37
40
  import { TopologyError, WEB_SEAM_PATH, renderWebSeam, } from "./topology.js";
38
- /** The checkout package a spoke's two route files import. */
41
+ /** The checkout package a spoke's billing action and thank-you page import. */
39
42
  export const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
40
43
  const API_CLIENT_PACKAGE = "@working-theory/api-client";
41
- /** The site's checkout data — written once, the site's from then on (#8748 D2). */
44
+ /** The site's checkout stamp — written once, the site's from then on (#8748 D2). */
42
45
  export const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
43
- /** The two return pages: Stripe's success URL and every refused buy's target. */
46
+ /** The two return pages a commerce spoke still carries (#8748 D5). */
44
47
  export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
45
48
  "apps/web/app/[locale]/checkout/thank-you/page.tsx",
46
49
  "apps/web/app/[locale]/checkout/unavailable/page.tsx",
47
50
  ]);
48
51
  /**
49
- * The standard spoke checkout's template paths, scaffold-relative (#8748 D3).
50
- * A directory entry covers every file under it, so the tests that sit beside
51
- * the routes and pages travel with them. `copyTemplate` skips all of them for
52
- * every scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
52
+ * The spoke checkout's template paths, scaffold-relative (#8748 D3). A
53
+ * directory entry covers every file under it, so the tests that sit beside
54
+ * the pages travel with them. `copyTemplate` skips all of them for every
55
+ * scaffold; `writeSpokeCheckoutSlice` copies them into a commerce spoke.
53
56
  */
54
57
  export const SPOKE_CHECKOUT_TEMPLATE_PATHS = Object.freeze([
55
- "apps/web/app/api/checkout",
56
- "apps/web/app/api/webhooks/stripe",
57
58
  "apps/web/app/[locale]/checkout",
58
- "apps/web/lib/checkout.ts",
59
59
  SPOKE_CHECKOUT_CONFIG_PATH,
60
- "apps/web/lib/__tests__/checkout.test.ts",
61
60
  ]);
62
- /** The variables a commerce spoke depends on (D9). The package reads three of them. */
61
+ /** The variables a commerce spoke depends on (D9): the package reads both. */
63
62
  export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
64
63
  STRIPE_SECRET_KEY: {
65
- description: `Read by ${SPOKE_CHECKOUT_PACKAGE}: a test key (sk_test_ or rk_test_), or a restricted live key the operator sets with STRIPE_MODE=live.`,
66
- secret: true,
67
- },
68
- STRIPE_WEBHOOK_SECRET: {
69
- description: `Read by ${SPOKE_CHECKOUT_PACKAGE}: the signing secret of this site's Stripe webhook endpoint.`,
64
+ description: `Read by ${SPOKE_CHECKOUT_PACKAGE} for the thank-you page's paid-session read and the billing reads, never to sell: a test key (sk_test_ or rk_test_), or a restricted live key the operator sets with STRIPE_MODE=live.`,
70
65
  secret: true,
71
66
  },
72
67
  STRIPE_MODE: {
73
68
  description: `Read by ${SPOKE_CHECKOUT_PACKAGE}: test (the default) or live. Live is the operator's switch, per site.`,
74
69
  secret: false,
75
70
  },
76
- // Read by the standard spoke checkout's fulfilment, so the base catalog
77
- // carries it too (#8748).
78
- AILK_SPOKE_KEY: ENV_CATALOG["AILK_SPOKE_KEY"],
79
71
  });
80
72
  /** The names a commerce spoke declares although only the package reads them (D9). */
81
73
  export const SPOKE_CHECKOUT_DECLARED_ENV = Object.freeze([
82
74
  "STRIPE_SECRET_KEY",
83
- "STRIPE_WEBHOOK_SECRET",
84
75
  "STRIPE_MODE",
85
76
  ]);
86
77
  /**
87
78
  * The catalog (and declared names) a spoke's `appendEnvRows` call passes: a
88
- * commerce spoke adds the checkout's four rows (D9). `ENV_CATALOG` itself is
79
+ * commerce spoke adds the package's two rows (D9). `ENV_CATALOG` itself is
89
80
  * unchanged — its test fails on an entry the monorepo's apps never read.
90
81
  */
91
82
  export function spokeEnvRowOptions(commerce) {
@@ -96,7 +87,7 @@ export function spokeEnvRowOptions(commerce) {
96
87
  }
97
88
  : { catalog: ENV_CATALOG };
98
89
  }
99
- /** `metadata.site` / an offering slug — the package's own pattern. */
90
+ /** `metadata.site` — the package's own pattern. */
100
91
  const SITE_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
101
92
  /** A stamp a renderer may interpolate: the package's pattern, never `default` (D6). */
102
93
  function isValidSite(site) {
@@ -125,14 +116,17 @@ export function spokeCheckoutSite(projectName) {
125
116
  const digest = createHash("sha256").update(projectName).digest("hex");
126
117
  return `site-${digest.slice(0, 8)}`;
127
118
  }
119
+ /** The billing action's first line, which marks the file as create-ailk's to rewrite. */
120
+ export const BILLING_ACTION_MARKER = "// The spoke billing action — GENERATED by create-ailk (#8217). Do not edit:";
128
121
  /**
129
122
  * The spoke billing action (#8217 D6): wiring only, byte-identical across
130
123
  * sites, so every guarantee arrives by a package bump. A plain server module
131
124
  * (C2): its two exports take no argument, and the hub's read is its only
132
- * source of customer ids.
125
+ * source of customer ids. It builds its config from the site's stamp and the
126
+ * site's own origin (#9142: no longer from `lib/checkout.ts`).
133
127
  */
134
- const BILLING_ACTION = [
135
- "// The spoke billing action — GENERATED by create-ailk (#8217). Do not edit:",
128
+ export const BILLING_ACTION = [
129
+ BILLING_ACTION_MARKER,
136
130
  "// every guarantee lives in the spoke-checkout package and arrives by a",
137
131
  "// package bump. A plain server module, not a Server Action file: neither",
138
132
  "// export takes an argument, and the hub's GET /v1/member/billing is the only",
@@ -141,206 +135,29 @@ const BILLING_ACTION = [
141
135
  `import { member } from "${API_CLIENT_PACKAGE}";`,
142
136
  `import { createSpokeCheckout } from "${SPOKE_CHECKOUT_PACKAGE}";`,
143
137
  "",
144
- `import { spokeCheckoutConfig } from "../../../../../lib/checkout";`,
138
+ `import { checkoutConfig } from "../../../../../lib/checkout-config";`,
145
139
  `import { forwardedCookieInit } from "../../../../../lib/server-api";`,
140
+ `import { resolveBaseUrl } from "../../../../../src/lib/routes";`,
146
141
  "",
147
- "const billing = createSpokeCheckout(spokeCheckoutConfig).billing.forMember(",
148
- " async () => member.billing(await forwardedCookieInit()),",
149
- ");",
142
+ "const billing = createSpokeCheckout({",
143
+ " site: checkoutConfig.site,",
144
+ " siteBaseUrl: resolveBaseUrl(),",
145
+ "}).billing.forMember(async () => member.billing(await forwardedCookieInit()));",
150
146
  "",
151
147
  "export const readSpokeBilling = billing.read;",
152
148
  "export const spokeBillingPortalUrl = billing.portalUrl;",
153
149
  "",
154
150
  ].join("\n");
155
- // ── #7839 D8 — the checkout e2e workflow ─────────────────────────────────────
156
- /** Where a commerce spoke's end-to-end checkout workflow lives. */
157
- export const SPOKE_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
158
- /**
159
- * The Stripe CLI release the workflow installs, and the SHA-256 of its
160
- * `stripe_<v>_linux_x86_64.tar.gz` from that release's own
161
- * `stripe-linux-checksums.txt`. Never bump one without re-reading the other.
162
- */
163
- export const STRIPE_CLI_VERSION = "1.51.1";
164
- export const STRIPE_CLI_LINUX_X64_SHA256 = "38c8de73a8cab810a4aab8a4015cd4e3398b7b87aa42b6944d09d89ed524ac53";
165
- /** The Playwright the workflow installs — the monorepo's own pinned version. */
166
- export const E2E_PLAYWRIGHT_VERSION = "1.61.1";
167
- /**
168
- * Every action the workflow uses, pinned to the commit its `v4` tag named
169
- * when this was written (SEC-7839-2): `gh api repos/<action>/commits/v4`.
170
- */
171
- export const E2E_ACTION_PINS = Object.freeze({
172
- "actions/checkout": "11d5960a326750d5838078e36cf38b85af677262",
173
- "pnpm/action-setup": "b906affcce14559ad1aafd4ab0e942779e9f58b1",
174
- "actions/setup-node": "49933ea5288caeca8642d1e84afbd3f7d6820020",
175
- "actions/upload-artifact": "ea165f8d65b6e75b540449e92b4886f43607fa02",
176
- });
177
- function uses(action) {
178
- return `uses: ${action}@${E2E_ACTION_PINS[action]} # v4`;
179
- }
180
- /**
181
- * The workflow text (D8). Identical for every spoke at one create-ailk
182
- * version. Every `${{ … }}` sits in an env-style value, never in a script.
183
- */
184
- export const CHECKOUT_E2E_WORKFLOW = `# Checkout e2e — GENERATED by create-ailk (#7839). Do not edit: it changes
185
- # with the ${SPOKE_CHECKOUT_PACKAGE} command it runs.
186
- #
187
- # Pays each named offering once through Stripe's hosted Checkout in TEST
188
- # mode, then checks Stripe's webhook was answered 2xx and this site's
189
- # fulfilment ran. Start it by hand (Actions > Checkout e2e > Run workflow)
190
- # after a checkout change and before any live flip. It never runs on a push
191
- # or a pull request: it needs the network, the Stripe CLI and test-mode state.
192
- #
193
- # Setup, once: create the GitHub Environment "checkout-e2e", limit its
194
- # deployment branches to the default branch, and add to it the secrets
195
- # STRIPE_TEST_SECRET_KEY (an sk_test_ or rk_test_ key) and AILK_SPOKE_KEY,
196
- # and the variable AILK_API_URL (https). Each paid session posts one
197
- # test-mode lead to the hub. Test subscriptions it creates are cancelled.
198
- name: Checkout e2e
199
-
200
- on:
201
- workflow_dispatch:
202
- inputs:
203
- offerings:
204
- description: Comma-separated fixed offering slugs from apps/web/lib/checkout-config.ts
205
- required: true
206
- type: string
207
-
208
- permissions:
209
- contents: read
210
-
211
- concurrency:
212
- group: checkout-e2e
213
- cancel-in-progress: false
214
-
215
- jobs:
216
- checkout-e2e:
217
- runs-on: ubuntu-latest
218
- timeout-minutes: 30
219
- environment: checkout-e2e
220
- env:
221
- NEXT_PUBLIC_BASE_URL: http://localhost:3000
222
- STRIPE_MODE: test
223
- steps:
224
- - name: Refuse a live key or a missing setting
225
- env:
226
- STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
227
- AILK_SPOKE_KEY: \${{ secrets.AILK_SPOKE_KEY }}
228
- AILK_API_URL: \${{ vars.AILK_API_URL }}
229
- run: |
230
- case "$STRIPE_SECRET_KEY" in
231
- sk_test_*|rk_test_*) ;;
232
- *) echo "::error::STRIPE_TEST_SECRET_KEY must be an sk_test_ or rk_test_ key"; exit 2 ;;
233
- esac
234
- if [ -z "$AILK_SPOKE_KEY" ]; then
235
- echo "::error::set the AILK_SPOKE_KEY secret in the checkout-e2e environment"; exit 2
236
- fi
237
- case "$AILK_API_URL" in
238
- https://*) ;;
239
- *) echo "::error::AILK_API_URL must be the hub's https origin"; exit 2 ;;
240
- esac
241
-
242
- - ${uses("actions/checkout")}
243
-
244
- - ${uses("pnpm/action-setup")}
245
-
246
- - run: test -f pnpm-lock.yaml || touch pnpm-lock.yaml
247
-
248
- - ${uses("actions/setup-node")}
249
- with:
250
- node-version: 20
251
- cache: pnpm
252
- cache-dependency-path: pnpm-lock.yaml
253
-
254
- - name: Install
255
- run: |
256
- if [ -s pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi
257
-
258
- - name: Install the Stripe CLI (pinned, checksum-checked)
259
- run: |
260
- curl -fsSL -o "$RUNNER_TEMP/stripe.tar.gz" \\
261
- https://github.com/stripe/stripe-cli/releases/download/v${STRIPE_CLI_VERSION}/stripe_${STRIPE_CLI_VERSION}_linux_x86_64.tar.gz
262
- echo "${STRIPE_CLI_LINUX_X64_SHA256} $RUNNER_TEMP/stripe.tar.gz" | sha256sum -c -
263
- mkdir -p "$RUNNER_TEMP/stripe-cli"
264
- tar -xzf "$RUNNER_TEMP/stripe.tar.gz" -C "$RUNNER_TEMP/stripe-cli" stripe
265
-
266
- - name: Install Playwright (pinned) and Chromium
267
- run: |
268
- npm install --no-save --ignore-scripts --prefix "$RUNNER_TEMP/e2e-deps" playwright@${E2E_PLAYWRIGHT_VERSION}
269
- "$RUNNER_TEMP/e2e-deps/node_modules/.bin/playwright" install --with-deps chromium
270
-
271
- - name: Fetch the Stripe CLI's signing secret
272
- env:
273
- STRIPE_API_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
274
- run: |
275
- umask 077
276
- secret="$("$RUNNER_TEMP/stripe-cli/stripe" listen --print-secret)"
277
- echo "::add-mask::$secret"
278
- printf '%s' "$secret" > "$RUNNER_TEMP/stripe-whsec"
279
-
280
- - name: Build the site
281
- run: pnpm --filter ./apps/web build
282
-
283
- - name: Start the site
284
- env:
285
- STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
286
- AILK_SPOKE_KEY: \${{ secrets.AILK_SPOKE_KEY }}
287
- AILK_API_URL: \${{ vars.AILK_API_URL }}
288
- run: |
289
- STRIPE_WEBHOOK_SECRET="$(cat "$RUNNER_TEMP/stripe-whsec")"
290
- export STRIPE_WEBHOOK_SECRET
291
- cd apps/web
292
- nohup pnpm exec next start -p 3000 -H localhost > "$RUNNER_TEMP/next.log" 2>&1 &
293
- echo $! > "$RUNNER_TEMP/next.pid"
294
- for _ in $(seq 1 60); do
295
- curl -fsS -o /dev/null http://localhost:3000/checkout/thank-you && exit 0
296
- sleep 2
297
- done
298
- echo "::error::the site did not start on localhost:3000; run it locally with the same settings to see why"
299
- exit 1
300
-
301
- - name: Pay each offering
302
- working-directory: apps/web
303
- env:
304
- STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
305
- OFFERINGS: \${{ inputs.offerings }}
306
- SPOKE_CHECKOUT_E2E_DEPS: \${{ runner.temp }}/e2e-deps
307
- STRIPE_CLI_PATH: \${{ runner.temp }}/stripe-cli/stripe
308
- run: |
309
- STRIPE_WEBHOOK_SECRET="$(cat "$RUNNER_TEMP/stripe-whsec")"
310
- export STRIPE_WEBHOOK_SECRET
311
- pnpm exec spoke-checkout-e2e --base-url http://localhost:3000 --offerings "$OFFERINGS" --out "$RUNNER_TEMP/checkout-e2e"
312
-
313
- - name: Stop the site
314
- if: always()
315
- run: |
316
- if [ -f "$RUNNER_TEMP/next.pid" ]; then pkill -P "$(cat "$RUNNER_TEMP/next.pid")" || true; kill "$(cat "$RUNNER_TEMP/next.pid")" || true; fi
317
- rm -f "$RUNNER_TEMP/stripe-whsec"
318
-
319
- - name: Keep the report
320
- if: always()
321
- ${uses("actions/upload-artifact")}
322
- with:
323
- name: checkout-e2e
324
- path: \${{ runner.temp }}/checkout-e2e
325
- if-no-files-found: ignore
326
- retention-days: 7
327
- `;
328
151
  /**
329
152
  * The rendered part of the slice for `site` (#8748 D3): with `signIn: "hub"`
330
- * the billing action (#8217 D8), then the checkout e2e workflow (#7839 D8).
331
- * Neither depends on `site`. The rest of the slice is template source,
332
- * copied by `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid
333
- * stamp (SEC-7838-10).
153
+ * the billing action (#8217 D8), else nothing. It does not depend on `site`.
154
+ * The rest of the slice is template source, copied by
155
+ * `writeSpokeCheckoutSlice`. Throws `TopologyError` for an invalid stamp
156
+ * (SEC-7838-10).
334
157
  */
335
158
  export function renderSpokeCheckoutFiles(site, opts = {}) {
336
159
  assertSite(site);
337
- const billingActionPath = SPOKE_CHECKOUT_ROUTE_FILES[2];
338
- return [
339
- ...(opts.signIn === "hub"
340
- ? [[billingActionPath, BILLING_ACTION]]
341
- : []),
342
- [SPOKE_CHECKOUT_E2E_WORKFLOW_PATH, CHECKOUT_E2E_WORKFLOW],
343
- ];
160
+ return opts.signIn === "hub" ? [[SPOKE_BILLING_ACTION_PATH, BILLING_ACTION]] : [];
344
161
  }
345
162
  /** The one line of the template's config the writer stamps (#8748 D2). */
346
163
  const SITE_LINE = /^const SITE = "[a-z0-9-]+";$/gm;
@@ -438,9 +255,10 @@ function isOlderPin(current, wanted) {
438
255
  * action, and the web seam — generated, so always rewritten — becomes the
439
256
  * billing render: reported `patched` when it changed, not at all when equal.
440
257
  * Adds the package to `apps/web/package.json` when absent, and raises a plain
441
- * pin older than the one it writes (#7839): the generated checkout e2e
442
- * workflow runs a command a `^0.2.0` pin never reaches. Any other range —
443
- * newer, `workspace:`, a tag — is the site's and is kept.
258
+ * pin older than the one it writes (#7839): the rendered billing action
259
+ * calls the package's current `createSpokeCheckout` shape, which an older
260
+ * pin does not take (#9142). Any other range — newer, `workspace:`, a tag —
261
+ * is the site's and is kept.
444
262
  * Every refusal (an invalid stamp, a version map without the package, a
445
263
  * missing or malformed `apps/web/package.json`, a template missing any
446
264
  * `SPOKE_CHECKOUT_TEMPLATE_PATHS` entry, a template config without exactly
@@ -558,3 +376,22 @@ export function dropSpokeCheckoutDependency(targetDir) {
558
376
  delete deps[SPOKE_CHECKOUT_PACKAGE];
559
377
  writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${before.endsWith("\n") ? "\n" : ""}`);
560
378
  }
379
+ // ── #9142 — the retired legacy env-key checkout ─────────────────────────────
380
+ /**
381
+ * What the legacy env-key checkout left in a spoke, scaffold-relative: the
382
+ * buy route and the Stripe webhook route (a directory entry covers the tests
383
+ * beside each), `lib/checkout.ts` (the fulfilment) and its test. Each was a
384
+ * template copy create-ailk recorded in `.ailk-template-files.json`;
385
+ * `create-ailk sync-routes --apply` removes the ones still byte-identical to
386
+ * that record.
387
+ */
388
+ export const RETIRED_SPOKE_CHECKOUT_PATHS = Object.freeze([
389
+ "apps/web/app/api/checkout",
390
+ "apps/web/app/api/webhooks/stripe",
391
+ "apps/web/lib/checkout.ts",
392
+ "apps/web/lib/__tests__/checkout.test.ts",
393
+ ]);
394
+ /** The retired checkout e2e workflow — rendered, never recorded (#7839 D8). */
395
+ export const RETIRED_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
396
+ /** The workflow's first line, which marks it as create-ailk's to remove. */
397
+ export const RETIRED_CHECKOUT_E2E_MARKER = "# Checkout e2e — GENERATED by create-ailk (#7839). Do not edit: it changes";
@@ -109,9 +109,6 @@ export declare const ALIASES: Readonly<{
109
109
  readonly design: "standard";
110
110
  readonly modules: readonly ["marketing"];
111
111
  readonly categories: readonly ["accounting", "legal", "technology"];
112
- readonly layout: {
113
- readonly announcementBanner: true;
114
- };
115
112
  };
116
113
  /**
117
114
  * The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line