create-ailk 0.21.0 → 0.23.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.
- package/component-catalog.json +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +87 -17
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- package/templates/apps/mcp/CLAUDE.md +22 -14
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
package/dist/spoke-checkout.js
CHANGED
|
@@ -1,78 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The spoke checkout slice (#7838) — what a spoke carrying `commerce`
|
|
3
|
-
* `apps/web
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* its hub also gets the billing action, the
|
|
7
|
-
*
|
|
2
|
+
* The spoke checkout slice (#7838, #8748) — what a spoke carrying `commerce`
|
|
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).
|
|
8
16
|
*
|
|
9
17
|
* Every guarantee lives in `@working-theory/spoke-checkout` and arrives by a
|
|
10
|
-
* package bump;
|
|
11
|
-
*
|
|
12
|
-
*
|
|
18
|
+
* package bump; the copied files are `apps/web` source, tested in the
|
|
19
|
+
* monorepo and kept current by `create-ailk update`, and apart from the
|
|
20
|
+
* config's one stamp line they are byte-identical across spokes at one
|
|
21
|
+
* create-ailk version (roadmap D3, AC-SPOKE-02). A hub never gets them.
|
|
13
22
|
*
|
|
14
|
-
* Spec: docs/specs/7838-spoke-web-checkout-slice-spec.md (D3–D7, D9)
|
|
23
|
+
* Spec: docs/specs/7838-spoke-web-checkout-slice-spec.md (D3–D7, D9);
|
|
24
|
+
* docs/specs/8748-a-standard-spoke-checkout-route-spec.md (D2, D3).
|
|
15
25
|
*
|
|
16
|
-
* Pure module: no `import.meta.url`, so jest imports it directly
|
|
17
|
-
* package specifier in a rendered file is
|
|
18
|
-
* monorepo's own architecture lint never
|
|
19
|
-
* create-ailk makes (the
|
|
26
|
+
* Pure module: no `import.meta.url`, so jest imports it directly — the caller
|
|
27
|
+
* passes `templatesDir`. Every package specifier in a rendered file is
|
|
28
|
+
* interpolated from a constant, so the monorepo's own architecture lint never
|
|
29
|
+
* reads a rendered line as an import create-ailk makes (the
|
|
30
|
+
* `CONTENT_ADAPTERS_PACKAGE` idiom in topology.ts).
|
|
20
31
|
*/
|
|
21
32
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
-
import { dirname, join } from "node:path";
|
|
24
|
-
import {
|
|
33
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
|
|
34
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
35
|
+
import { buildScaffoldMarkers, renderTemplateFile, templateDestination } from "./copy.js";
|
|
36
|
+
import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
|
|
25
37
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
26
38
|
import { diffTwoStrings, } from "./lib/extract-module-bundle.js";
|
|
27
39
|
import { substituteWorkspaceVersions, } from "./substitute.js";
|
|
28
40
|
import { TopologyError, WEB_SEAM_PATH, renderWebSeam, } from "./topology.js";
|
|
29
|
-
/** The checkout package a spoke's
|
|
41
|
+
/** The checkout package a spoke's billing action and thank-you page import. */
|
|
30
42
|
export const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
|
|
31
43
|
const API_CLIENT_PACKAGE = "@working-theory/api-client";
|
|
32
|
-
|
|
33
|
-
const VALIDATION_PACKAGE = "@working-theory/validation";
|
|
34
|
-
/** The site's checkout data — written once, the site's from then on. */
|
|
44
|
+
/** The site's checkout stamp — written once, the site's from then on (#8748 D2). */
|
|
35
45
|
export const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
|
|
36
|
-
/** The two return pages
|
|
46
|
+
/** The two return pages a commerce spoke still carries (#8748 D5). */
|
|
37
47
|
export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
|
|
38
48
|
"apps/web/app/[locale]/checkout/thank-you/page.tsx",
|
|
39
49
|
"apps/web/app/[locale]/checkout/unavailable/page.tsx",
|
|
40
50
|
]);
|
|
41
51
|
/**
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
56
|
*/
|
|
47
|
-
export const
|
|
48
|
-
|
|
57
|
+
export const SPOKE_CHECKOUT_TEMPLATE_PATHS = Object.freeze([
|
|
58
|
+
"apps/web/app/[locale]/checkout",
|
|
59
|
+
SPOKE_CHECKOUT_CONFIG_PATH,
|
|
60
|
+
]);
|
|
61
|
+
/** The variables a commerce spoke depends on (D9): the package reads both. */
|
|
49
62
|
export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
|
|
50
63
|
STRIPE_SECRET_KEY: {
|
|
51
|
-
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.`,
|
|
52
|
-
secret: true,
|
|
53
|
-
},
|
|
54
|
-
STRIPE_WEBHOOK_SECRET: {
|
|
55
|
-
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.`,
|
|
56
65
|
secret: true,
|
|
57
66
|
},
|
|
58
67
|
STRIPE_MODE: {
|
|
59
68
|
description: `Read by ${SPOKE_CHECKOUT_PACKAGE}: test (the default) or live. Live is the operator's switch, per site.`,
|
|
60
69
|
secret: false,
|
|
61
70
|
},
|
|
62
|
-
AILK_SPOKE_KEY: {
|
|
63
|
-
description: "The hub-issued key this site sends as x-ailk-spoke-key; the checkout fulfilment refuses to run without it. Never committed.",
|
|
64
|
-
secret: true,
|
|
65
|
-
},
|
|
66
71
|
});
|
|
67
72
|
/** The names a commerce spoke declares although only the package reads them (D9). */
|
|
68
73
|
export const SPOKE_CHECKOUT_DECLARED_ENV = Object.freeze([
|
|
69
74
|
"STRIPE_SECRET_KEY",
|
|
70
|
-
"STRIPE_WEBHOOK_SECRET",
|
|
71
75
|
"STRIPE_MODE",
|
|
72
76
|
]);
|
|
73
77
|
/**
|
|
74
78
|
* The catalog (and declared names) a spoke's `appendEnvRows` call passes: a
|
|
75
|
-
* commerce spoke adds the
|
|
79
|
+
* commerce spoke adds the package's two rows (D9). `ENV_CATALOG` itself is
|
|
76
80
|
* unchanged — its test fails on an entry the monorepo's apps never read.
|
|
77
81
|
*/
|
|
78
82
|
export function spokeEnvRowOptions(commerce) {
|
|
@@ -83,7 +87,7 @@ export function spokeEnvRowOptions(commerce) {
|
|
|
83
87
|
}
|
|
84
88
|
: { catalog: ENV_CATALOG };
|
|
85
89
|
}
|
|
86
|
-
/** `metadata.site`
|
|
90
|
+
/** `metadata.site` — the package's own pattern. */
|
|
87
91
|
const SITE_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
88
92
|
/** A stamp a renderer may interpolate: the package's pattern, never `default` (D6). */
|
|
89
93
|
function isValidSite(site) {
|
|
@@ -112,42 +116,17 @@ export function spokeCheckoutSite(projectName) {
|
|
|
112
116
|
const digest = createHash("sha256").update(projectName).digest("hex");
|
|
113
117
|
return `site-${digest.slice(0, 8)}`;
|
|
114
118
|
}
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
return [
|
|
118
|
-
`// ${opts.title} — GENERATED by create-ailk (#7838). Do not edit: every`,
|
|
119
|
-
"// guarantee lives in the spoke-checkout package and arrives by a package",
|
|
120
|
-
"// bump. This site's data is lib/checkout-config.ts.",
|
|
121
|
-
`import { createSpokeCheckout } from "${SPOKE_CHECKOUT_PACKAGE}";`,
|
|
122
|
-
"",
|
|
123
|
-
`import { checkoutConfig } from "../../../../lib/checkout-config";`,
|
|
124
|
-
"",
|
|
125
|
-
`export const runtime = "nodejs";`,
|
|
126
|
-
`export const dynamic = "force-dynamic";`,
|
|
127
|
-
"",
|
|
128
|
-
`// platform-decisions-waiver: api-versioning — ${opts.waiver} (spec 7838 §7)`,
|
|
129
|
-
opts.exportLine,
|
|
130
|
-
"",
|
|
131
|
-
].join("\n");
|
|
132
|
-
}
|
|
133
|
-
const BUY_ROUTE = renderRoute({
|
|
134
|
-
title: "The buy route",
|
|
135
|
-
waiver: "a browser buy link that redirects to Stripe Checkout, not a versioned API contract",
|
|
136
|
-
exportLine: "export const { GET, POST } = createSpokeCheckout(checkoutConfig).buy;",
|
|
137
|
-
});
|
|
138
|
-
const WEBHOOK_ROUTE = renderRoute({
|
|
139
|
-
title: "The Stripe webhook route",
|
|
140
|
-
waiver: "the endpoint URL registered in Stripe, not a versioned API contract",
|
|
141
|
-
exportLine: "export const { POST } = createSpokeCheckout(checkoutConfig).webhook;",
|
|
142
|
-
});
|
|
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:";
|
|
143
121
|
/**
|
|
144
122
|
* The spoke billing action (#8217 D6): wiring only, byte-identical across
|
|
145
123
|
* sites, so every guarantee arrives by a package bump. A plain server module
|
|
146
124
|
* (C2): its two exports take no argument, and the hub's read is its only
|
|
147
|
-
* 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`).
|
|
148
127
|
*/
|
|
149
|
-
const BILLING_ACTION = [
|
|
150
|
-
|
|
128
|
+
export const BILLING_ACTION = [
|
|
129
|
+
BILLING_ACTION_MARKER,
|
|
151
130
|
"// every guarantee lives in the spoke-checkout package and arrives by a",
|
|
152
131
|
"// package bump. A plain server module, not a Server Action file: neither",
|
|
153
132
|
"// export takes an argument, and the hub's GET /v1/member/billing is the only",
|
|
@@ -158,366 +137,74 @@ const BILLING_ACTION = [
|
|
|
158
137
|
"",
|
|
159
138
|
`import { checkoutConfig } from "../../../../../lib/checkout-config";`,
|
|
160
139
|
`import { forwardedCookieInit } from "../../../../../lib/server-api";`,
|
|
140
|
+
`import { resolveBaseUrl } from "../../../../../src/lib/routes";`,
|
|
161
141
|
"",
|
|
162
|
-
"const billing = createSpokeCheckout(
|
|
163
|
-
"
|
|
164
|
-
")
|
|
142
|
+
"const billing = createSpokeCheckout({",
|
|
143
|
+
" site: checkoutConfig.site,",
|
|
144
|
+
" siteBaseUrl: resolveBaseUrl(),",
|
|
145
|
+
"}).billing.forMember(async () => member.billing(await forwardedCookieInit()));",
|
|
165
146
|
"",
|
|
166
147
|
"export const readSpokeBilling = billing.read;",
|
|
167
148
|
"export const spokeBillingPortalUrl = billing.portalUrl;",
|
|
168
149
|
"",
|
|
169
150
|
].join("\n");
|
|
170
|
-
/** The site's checkout data (D5). Only the `const SITE` line varies by site. */
|
|
171
|
-
function renderConfig(site) {
|
|
172
|
-
return [
|
|
173
|
-
"// checkout-config.ts — this spoke's checkout, as data (#7838).",
|
|
174
|
-
"//",
|
|
175
|
-
"// Written once by create-ailk and yours from then on: create-ailk never",
|
|
176
|
-
"// rewrites it. The buy route and the Stripe webhook route build their",
|
|
177
|
-
"// handlers from it, and so does the billing action on a spoke that signs",
|
|
178
|
-
"// in through its hub. Only those files may import the checkout package",
|
|
179
|
-
"// (architecture.yaml), so this file never imports it, stripe-client or the",
|
|
180
|
-
"// stripe SDK. Keep it to data and the fulfil callback.",
|
|
181
|
-
"//",
|
|
182
|
-
"// site Stamped on every Checkout Session as metadata.site. This site's",
|
|
183
|
-
"// webhook acts only on its own stamp, and the hub ignores any stamp",
|
|
184
|
-
"// that is not its own, so the stamp must be unique on the Stripe",
|
|
185
|
-
'// account this site shares. Never "default". Change it only before',
|
|
186
|
-
"// any price exists: every lookup key starts with it.",
|
|
187
|
-
"// offerings slug -> Stripe lookup_key. The buy link is /api/checkout/<slug>.",
|
|
188
|
-
"// scaffold-commerce replaces the placeholder below.",
|
|
189
|
-
"// fulfil Runs for each paid session, at least once. It posts one lead to",
|
|
190
|
-
"// the hub with the session id as its Idempotency-Key, so a repeat",
|
|
191
|
-
"// writes nothing new. The hub dedupes only an identical body: change",
|
|
192
|
-
"// the payload's shape only when no webhook is failing.",
|
|
193
|
-
`import { leads } from "${API_CLIENT_PACKAGE}";`,
|
|
194
|
-
"",
|
|
195
|
-
`import { resolveBaseUrl } from "../src/lib/routes";`,
|
|
196
|
-
"",
|
|
197
|
-
`const SITE = "${site}";`,
|
|
198
|
-
"",
|
|
199
|
-
"/** What this site's fulfil callback reads from a completed checkout. */",
|
|
200
|
-
"interface CompletedCheckout {",
|
|
201
|
-
" idempotencyKey: string;",
|
|
202
|
-
" sessionId: string;",
|
|
203
|
-
" offering: string;",
|
|
204
|
-
' kind: "fixed" | "quoted";',
|
|
205
|
-
' mode: "test" | "live";',
|
|
206
|
-
" amountTotal: number | null;",
|
|
207
|
-
" currency: string | null;",
|
|
208
|
-
" email: string | null;",
|
|
209
|
-
" customerId: string | null;",
|
|
210
|
-
"}",
|
|
211
|
-
"",
|
|
212
|
-
"export const checkoutConfig = {",
|
|
213
|
-
" site: SITE,",
|
|
214
|
-
" siteBaseUrl: resolveBaseUrl(),",
|
|
215
|
-
" offerings: {",
|
|
216
|
-
" // A placeholder until scaffold-commerce creates this site's prices.",
|
|
217
|
-
' example: { kind: "fixed", lookupKey: `${SITE}_example`, cancelPath: "/" },',
|
|
218
|
-
" },",
|
|
219
|
-
" async fulfil(checkout: CompletedCheckout): Promise<void> {",
|
|
220
|
-
" if (checkout.email === null) return;",
|
|
221
|
-
" // No spoke key: the hub cannot place the lead in this site's workspace.",
|
|
222
|
-
" // Fail, so Stripe retries and the dashboard shows a failed webhook.",
|
|
223
|
-
" if (!process.env.AILK_SPOKE_KEY)",
|
|
224
|
-
' throw new Error("AILK_SPOKE_KEY is not set");',
|
|
225
|
-
" const result = await leads.submit(",
|
|
226
|
-
" {",
|
|
227
|
-
" siteId: SITE,",
|
|
228
|
-
' source: "web",',
|
|
229
|
-
" email: checkout.email,",
|
|
230
|
-
" payload: {",
|
|
231
|
-
" site: SITE,",
|
|
232
|
-
' page: "checkout",',
|
|
233
|
-
" offering: checkout.offering,",
|
|
234
|
-
" checkoutKind: checkout.kind,",
|
|
235
|
-
" mode: checkout.mode,",
|
|
236
|
-
" amountTotal: checkout.amountTotal,",
|
|
237
|
-
" currency: checkout.currency,",
|
|
238
|
-
" stripeCustomerId: checkout.customerId,",
|
|
239
|
-
" checkoutSession: checkout.sessionId,",
|
|
240
|
-
" },",
|
|
241
|
-
" },",
|
|
242
|
-
' { headers: { "Idempotency-Key": checkout.idempotencyKey } },',
|
|
243
|
-
" );",
|
|
244
|
-
" // A replay under the same key answers 201 with the first lead; any",
|
|
245
|
-
" // failure (422 idempotency_key_reused included) makes Stripe retry.",
|
|
246
|
-
" if (!result.ok) throw new Error(`hub lead write answered ${result.status}`);",
|
|
247
|
-
" },",
|
|
248
|
-
"};",
|
|
249
|
-
"",
|
|
250
|
-
].join("\n");
|
|
251
|
-
}
|
|
252
|
-
/** One return page (D7): a server component that reads nothing but its copy. */
|
|
253
|
-
function renderPage(opts) {
|
|
254
|
-
return [
|
|
255
|
-
`// ${opts.title} — ${SPOKE_CHECKOUT_PAGE_MARKER}.`,
|
|
256
|
-
...opts.about.map((line) => `// ${line}`),
|
|
257
|
-
`import { SectionRenderer } from "${UI_SECTIONS_PACKAGE}";`,
|
|
258
|
-
`import type { PageSection } from "${VALIDATION_PACKAGE}";`,
|
|
259
|
-
`import type { Metadata } from "next";`,
|
|
260
|
-
`import { getTranslations } from "next-intl/server";`,
|
|
261
|
-
`import type { ReactElement } from "react";`,
|
|
262
|
-
"",
|
|
263
|
-
"interface Props {",
|
|
264
|
-
" params: Promise<{ locale: string }>;",
|
|
265
|
-
"}",
|
|
266
|
-
"",
|
|
267
|
-
"export async function generateMetadata({ params }: Props): Promise<Metadata> {",
|
|
268
|
-
" const { locale } = await params;",
|
|
269
|
-
` const t = await getTranslations({ locale, namespace: "${opts.namespace}" });`,
|
|
270
|
-
` return { title: t("title"), robots: { index: false, follow: false } };`,
|
|
271
|
-
"}",
|
|
272
|
-
"",
|
|
273
|
-
"export default async function Page({ params }: Props): Promise<ReactElement> {",
|
|
274
|
-
" const { locale } = await params;",
|
|
275
|
-
` const t = await getTranslations({ locale, namespace: "${opts.namespace}" });`,
|
|
276
|
-
" const sections: PageSection[] = [",
|
|
277
|
-
...opts.sections.map((line) => ` ${line}`),
|
|
278
|
-
" ];",
|
|
279
|
-
" return (",
|
|
280
|
-
' <main id="main">',
|
|
281
|
-
" <SectionRenderer sections={sections} locale={locale} leadingSectionOwnsTitle />",
|
|
282
|
-
" </main>",
|
|
283
|
-
" );",
|
|
284
|
-
"}",
|
|
285
|
-
"",
|
|
286
|
-
].join("\n");
|
|
287
|
-
}
|
|
288
|
-
const THANK_YOU_PAGE = renderPage({
|
|
289
|
-
title: "The checkout thank-you page",
|
|
290
|
-
about: [
|
|
291
|
-
"Stripe returns a buyer here after paying. It reads no search parameter and",
|
|
292
|
-
"calls neither Stripe nor the hub: the webhook is what fulfils the order.",
|
|
293
|
-
],
|
|
294
|
-
namespace: "checkoutThankYou",
|
|
295
|
-
sections: [
|
|
296
|
-
"{",
|
|
297
|
-
' id: "checkout-thank-you",',
|
|
298
|
-
' type: "content-block",',
|
|
299
|
-
' data: { headline: t("title"), body: t("body") },',
|
|
300
|
-
"},",
|
|
301
|
-
],
|
|
302
|
-
});
|
|
303
|
-
const UNAVAILABLE_PAGE = renderPage({
|
|
304
|
-
title: "The checkout unavailable page",
|
|
305
|
-
about: [
|
|
306
|
-
"Every refused buy lands here. It never says why: a reason is a probe's",
|
|
307
|
-
"answer, and the buyer needs only that nothing was charged.",
|
|
308
|
-
],
|
|
309
|
-
namespace: "checkoutUnavailable",
|
|
310
|
-
sections: [
|
|
311
|
-
"{",
|
|
312
|
-
' id: "checkout-unavailable",',
|
|
313
|
-
' type: "content-block",',
|
|
314
|
-
' data: { headline: t("title"), body: t("body") },',
|
|
315
|
-
"},",
|
|
316
|
-
"{",
|
|
317
|
-
' id: "checkout-unavailable-cta",',
|
|
318
|
-
' type: "cta",',
|
|
319
|
-
" data: {",
|
|
320
|
-
' headline: t("ctaHeadline"),',
|
|
321
|
-
' primaryCta: { label: t("ctaLabel"), href: "/contact" },',
|
|
322
|
-
" },",
|
|
323
|
-
"},",
|
|
324
|
-
],
|
|
325
|
-
});
|
|
326
|
-
// ── #7839 D8 — the checkout e2e workflow ─────────────────────────────────────
|
|
327
|
-
/** Where a commerce spoke's end-to-end checkout workflow lives. */
|
|
328
|
-
export const SPOKE_CHECKOUT_E2E_WORKFLOW_PATH = ".github/workflows/checkout-e2e.yml";
|
|
329
151
|
/**
|
|
330
|
-
* The
|
|
331
|
-
*
|
|
332
|
-
*
|
|
152
|
+
* The rendered part of the slice for `site` (#8748 D3): with `signIn: "hub"`
|
|
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).
|
|
333
157
|
*/
|
|
334
|
-
export
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
export const E2E_PLAYWRIGHT_VERSION = "1.61.1";
|
|
338
|
-
/**
|
|
339
|
-
* Every action the workflow uses, pinned to the commit its `v4` tag named
|
|
340
|
-
* when this was written (SEC-7839-2): `gh api repos/<action>/commits/v4`.
|
|
341
|
-
*/
|
|
342
|
-
export const E2E_ACTION_PINS = Object.freeze({
|
|
343
|
-
"actions/checkout": "11d5960a326750d5838078e36cf38b85af677262",
|
|
344
|
-
"pnpm/action-setup": "b906affcce14559ad1aafd4ab0e942779e9f58b1",
|
|
345
|
-
"actions/setup-node": "49933ea5288caeca8642d1e84afbd3f7d6820020",
|
|
346
|
-
"actions/upload-artifact": "ea165f8d65b6e75b540449e92b4886f43607fa02",
|
|
347
|
-
});
|
|
348
|
-
function uses(action) {
|
|
349
|
-
return `uses: ${action}@${E2E_ACTION_PINS[action]} # v4`;
|
|
158
|
+
export function renderSpokeCheckoutFiles(site, opts = {}) {
|
|
159
|
+
assertSite(site);
|
|
160
|
+
return opts.signIn === "hub" ? [[SPOKE_BILLING_ACTION_PATH, BILLING_ACTION]] : [];
|
|
350
161
|
}
|
|
162
|
+
/** The one line of the template's config the writer stamps (#8748 D2). */
|
|
163
|
+
const SITE_LINE = /^const SITE = "[a-z0-9-]+";$/gm;
|
|
351
164
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
export const CHECKOUT_E2E_WORKFLOW = `# Checkout e2e — GENERATED by create-ailk (#7839). Do not edit: it changes
|
|
356
|
-
# with the ${SPOKE_CHECKOUT_PACKAGE} command it runs.
|
|
357
|
-
#
|
|
358
|
-
# Pays each named offering once through Stripe's hosted Checkout in TEST
|
|
359
|
-
# mode, then checks Stripe's webhook was answered 2xx and this site's
|
|
360
|
-
# fulfilment ran. Start it by hand (Actions > Checkout e2e > Run workflow)
|
|
361
|
-
# after a checkout change and before any live flip. It never runs on a push
|
|
362
|
-
# or a pull request: it needs the network, the Stripe CLI and test-mode state.
|
|
363
|
-
#
|
|
364
|
-
# Setup, once: create the GitHub Environment "checkout-e2e", limit its
|
|
365
|
-
# deployment branches to the default branch, and add to it the secrets
|
|
366
|
-
# STRIPE_TEST_SECRET_KEY (an sk_test_ or rk_test_ key) and AILK_SPOKE_KEY,
|
|
367
|
-
# and the variable AILK_API_URL (https). Each paid session posts one
|
|
368
|
-
# test-mode lead to the hub. Test subscriptions it creates are cancelled.
|
|
369
|
-
name: Checkout e2e
|
|
370
|
-
|
|
371
|
-
on:
|
|
372
|
-
workflow_dispatch:
|
|
373
|
-
inputs:
|
|
374
|
-
offerings:
|
|
375
|
-
description: Comma-separated fixed offering slugs from apps/web/lib/checkout-config.ts
|
|
376
|
-
required: true
|
|
377
|
-
type: string
|
|
378
|
-
|
|
379
|
-
permissions:
|
|
380
|
-
contents: read
|
|
381
|
-
|
|
382
|
-
concurrency:
|
|
383
|
-
group: checkout-e2e
|
|
384
|
-
cancel-in-progress: false
|
|
385
|
-
|
|
386
|
-
jobs:
|
|
387
|
-
checkout-e2e:
|
|
388
|
-
runs-on: ubuntu-latest
|
|
389
|
-
timeout-minutes: 30
|
|
390
|
-
environment: checkout-e2e
|
|
391
|
-
env:
|
|
392
|
-
NEXT_PUBLIC_BASE_URL: http://localhost:3000
|
|
393
|
-
STRIPE_MODE: test
|
|
394
|
-
steps:
|
|
395
|
-
- name: Refuse a live key or a missing setting
|
|
396
|
-
env:
|
|
397
|
-
STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
|
|
398
|
-
AILK_SPOKE_KEY: \${{ secrets.AILK_SPOKE_KEY }}
|
|
399
|
-
AILK_API_URL: \${{ vars.AILK_API_URL }}
|
|
400
|
-
run: |
|
|
401
|
-
case "$STRIPE_SECRET_KEY" in
|
|
402
|
-
sk_test_*|rk_test_*) ;;
|
|
403
|
-
*) echo "::error::STRIPE_TEST_SECRET_KEY must be an sk_test_ or rk_test_ key"; exit 2 ;;
|
|
404
|
-
esac
|
|
405
|
-
if [ -z "$AILK_SPOKE_KEY" ]; then
|
|
406
|
-
echo "::error::set the AILK_SPOKE_KEY secret in the checkout-e2e environment"; exit 2
|
|
407
|
-
fi
|
|
408
|
-
case "$AILK_API_URL" in
|
|
409
|
-
https://*) ;;
|
|
410
|
-
*) echo "::error::AILK_API_URL must be the hub's https origin"; exit 2 ;;
|
|
411
|
-
esac
|
|
412
|
-
|
|
413
|
-
- ${uses("actions/checkout")}
|
|
414
|
-
|
|
415
|
-
- ${uses("pnpm/action-setup")}
|
|
416
|
-
|
|
417
|
-
- run: test -f pnpm-lock.yaml || touch pnpm-lock.yaml
|
|
418
|
-
|
|
419
|
-
- ${uses("actions/setup-node")}
|
|
420
|
-
with:
|
|
421
|
-
node-version: 20
|
|
422
|
-
cache: pnpm
|
|
423
|
-
cache-dependency-path: pnpm-lock.yaml
|
|
424
|
-
|
|
425
|
-
- name: Install
|
|
426
|
-
run: |
|
|
427
|
-
if [ -s pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi
|
|
428
|
-
|
|
429
|
-
- name: Install the Stripe CLI (pinned, checksum-checked)
|
|
430
|
-
run: |
|
|
431
|
-
curl -fsSL -o "$RUNNER_TEMP/stripe.tar.gz" \\
|
|
432
|
-
https://github.com/stripe/stripe-cli/releases/download/v${STRIPE_CLI_VERSION}/stripe_${STRIPE_CLI_VERSION}_linux_x86_64.tar.gz
|
|
433
|
-
echo "${STRIPE_CLI_LINUX_X64_SHA256} $RUNNER_TEMP/stripe.tar.gz" | sha256sum -c -
|
|
434
|
-
mkdir -p "$RUNNER_TEMP/stripe-cli"
|
|
435
|
-
tar -xzf "$RUNNER_TEMP/stripe.tar.gz" -C "$RUNNER_TEMP/stripe-cli" stripe
|
|
436
|
-
|
|
437
|
-
- name: Install Playwright (pinned) and Chromium
|
|
438
|
-
run: |
|
|
439
|
-
npm install --no-save --ignore-scripts --prefix "$RUNNER_TEMP/e2e-deps" playwright@${E2E_PLAYWRIGHT_VERSION}
|
|
440
|
-
"$RUNNER_TEMP/e2e-deps/node_modules/.bin/playwright" install --with-deps chromium
|
|
441
|
-
|
|
442
|
-
- name: Fetch the Stripe CLI's signing secret
|
|
443
|
-
env:
|
|
444
|
-
STRIPE_API_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
|
|
445
|
-
run: |
|
|
446
|
-
umask 077
|
|
447
|
-
secret="$("$RUNNER_TEMP/stripe-cli/stripe" listen --print-secret)"
|
|
448
|
-
echo "::add-mask::$secret"
|
|
449
|
-
printf '%s' "$secret" > "$RUNNER_TEMP/stripe-whsec"
|
|
450
|
-
|
|
451
|
-
- name: Build the site
|
|
452
|
-
run: pnpm --filter ./apps/web build
|
|
453
|
-
|
|
454
|
-
- name: Start the site
|
|
455
|
-
env:
|
|
456
|
-
STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
|
|
457
|
-
AILK_SPOKE_KEY: \${{ secrets.AILK_SPOKE_KEY }}
|
|
458
|
-
AILK_API_URL: \${{ vars.AILK_API_URL }}
|
|
459
|
-
run: |
|
|
460
|
-
STRIPE_WEBHOOK_SECRET="$(cat "$RUNNER_TEMP/stripe-whsec")"
|
|
461
|
-
export STRIPE_WEBHOOK_SECRET
|
|
462
|
-
cd apps/web
|
|
463
|
-
nohup pnpm exec next start -p 3000 -H localhost > "$RUNNER_TEMP/next.log" 2>&1 &
|
|
464
|
-
echo $! > "$RUNNER_TEMP/next.pid"
|
|
465
|
-
for _ in $(seq 1 60); do
|
|
466
|
-
curl -fsS -o /dev/null http://localhost:3000/checkout/thank-you && exit 0
|
|
467
|
-
sleep 2
|
|
468
|
-
done
|
|
469
|
-
echo "::error::the site did not start on localhost:3000; run it locally with the same settings to see why"
|
|
470
|
-
exit 1
|
|
471
|
-
|
|
472
|
-
- name: Pay each offering
|
|
473
|
-
working-directory: apps/web
|
|
474
|
-
env:
|
|
475
|
-
STRIPE_SECRET_KEY: \${{ secrets.STRIPE_TEST_SECRET_KEY }}
|
|
476
|
-
OFFERINGS: \${{ inputs.offerings }}
|
|
477
|
-
SPOKE_CHECKOUT_E2E_DEPS: \${{ runner.temp }}/e2e-deps
|
|
478
|
-
STRIPE_CLI_PATH: \${{ runner.temp }}/stripe-cli/stripe
|
|
479
|
-
run: |
|
|
480
|
-
STRIPE_WEBHOOK_SECRET="$(cat "$RUNNER_TEMP/stripe-whsec")"
|
|
481
|
-
export STRIPE_WEBHOOK_SECRET
|
|
482
|
-
pnpm exec spoke-checkout-e2e --base-url http://localhost:3000 --offerings "$OFFERINGS" --out "$RUNNER_TEMP/checkout-e2e"
|
|
483
|
-
|
|
484
|
-
- name: Stop the site
|
|
485
|
-
if: always()
|
|
486
|
-
run: |
|
|
487
|
-
if [ -f "$RUNNER_TEMP/next.pid" ]; then pkill -P "$(cat "$RUNNER_TEMP/next.pid")" || true; kill "$(cat "$RUNNER_TEMP/next.pid")" || true; fi
|
|
488
|
-
rm -f "$RUNNER_TEMP/stripe-whsec"
|
|
489
|
-
|
|
490
|
-
- name: Keep the report
|
|
491
|
-
if: always()
|
|
492
|
-
${uses("actions/upload-artifact")}
|
|
493
|
-
with:
|
|
494
|
-
name: checkout-e2e
|
|
495
|
-
path: \${{ runner.temp }}/checkout-e2e
|
|
496
|
-
if-no-files-found: ignore
|
|
497
|
-
retention-days: 7
|
|
498
|
-
`;
|
|
499
|
-
/**
|
|
500
|
-
* The slice for `site`, in order: the two route files, the config, the two
|
|
501
|
-
* pages (D3), then the checkout e2e workflow (#7839 D8), which does not
|
|
502
|
-
* depend on `site`. With `signIn: "hub"` the billing action is the third
|
|
503
|
-
* pair, so the first three paths are `SPOKE_CHECKOUT_ROUTE_FILES` (#8217 D8).
|
|
504
|
-
* Throws `TopologyError` for an invalid stamp (SEC-7838-10).
|
|
165
|
+
* Every template file under `SPOKE_CHECKOUT_TEMPLATE_PATHS`, sorted, as its
|
|
166
|
+
* destination path and its source. A symlink is skipped, never followed, as
|
|
167
|
+
* `copyTemplate` skips one; a `.gitkeep` has no destination.
|
|
505
168
|
*/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
169
|
+
function templateCheckoutFiles(templatesDir) {
|
|
170
|
+
const out = [];
|
|
171
|
+
const visit = (rel) => {
|
|
172
|
+
const abs = join(templatesDir, ...rel.split("/"));
|
|
173
|
+
if (!existsSync(abs))
|
|
174
|
+
return;
|
|
175
|
+
const stat = lstatSync(abs);
|
|
176
|
+
if (stat.isSymbolicLink())
|
|
177
|
+
return;
|
|
178
|
+
if (stat.isDirectory()) {
|
|
179
|
+
for (const name of readdirSync(abs))
|
|
180
|
+
visit(`${rel}/${name}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const dest = templateDestination(rel);
|
|
184
|
+
if (dest !== undefined)
|
|
185
|
+
out.push([dest, abs]);
|
|
186
|
+
};
|
|
187
|
+
for (const entry of SPOKE_CHECKOUT_TEMPLATE_PATHS)
|
|
188
|
+
visit(entry);
|
|
189
|
+
return out.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
190
|
+
}
|
|
191
|
+
/** Whether anything — a dangling symlink included — sits at `abs`. */
|
|
192
|
+
function lstatExists(abs) {
|
|
193
|
+
try {
|
|
194
|
+
lstatSync(abs);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Whether a path, or (when absent) its nearest existing ancestor, resolves inside `targetReal`. */
|
|
202
|
+
function resolvesInside(targetReal, abs) {
|
|
203
|
+
let probe = abs;
|
|
204
|
+
while (!existsSync(probe) && dirname(probe) !== probe)
|
|
205
|
+
probe = dirname(probe);
|
|
206
|
+
const real = realpathSync(probe);
|
|
207
|
+
return real === targetReal || real.startsWith(targetReal + sep);
|
|
521
208
|
}
|
|
522
209
|
const WEB_PACKAGE_JSON = "apps/web/package.json";
|
|
523
210
|
/** `deps` with `name: version` added in sorted position, every other key kept in place. */
|
|
@@ -556,28 +243,79 @@ function isOlderPin(current, wanted) {
|
|
|
556
243
|
return false;
|
|
557
244
|
}
|
|
558
245
|
/**
|
|
559
|
-
* Write the slice into `targetDir` (D3).
|
|
560
|
-
*
|
|
246
|
+
* Write the slice into `targetDir` (#7838 D3, #8748 D3). The standard's
|
|
247
|
+
* template files (`SPOKE_CHECKOUT_TEMPLATE_PATHS`) are copied from
|
|
248
|
+
* `templatesDir`, each rendered with `copyTemplate`'s own renderer and
|
|
249
|
+
* markers, so a recorded hash equals what `create-ailk update` renders; the
|
|
250
|
+
* config's one `const SITE` line is stamped with `site`. `onWrite` is called
|
|
251
|
+
* once per template file written, as `copyTemplate` calls it. Then come the
|
|
252
|
+
* rendered files (`renderSpokeCheckoutFiles`). A file that already exists is
|
|
253
|
+
* left byte-for-byte and reported `skipped` — the config is the site's once
|
|
561
254
|
* written. With `signIn: "hub"` (#8217 D8) the slice carries the billing
|
|
562
255
|
* action, and the web seam — generated, so always rewritten — becomes the
|
|
563
|
-
* billing render: reported `patched` when it changed, not at all when equal.
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
256
|
+
* billing render: reported `patched` when it changed, not at all when equal.
|
|
257
|
+
* Adds the package to `apps/web/package.json` when absent, and raises a plain
|
|
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.
|
|
567
262
|
* Every refusal (an invalid stamp, a version map without the package, a
|
|
568
|
-
* missing or malformed `apps/web/package.json
|
|
263
|
+
* missing or malformed `apps/web/package.json`, a template missing any
|
|
264
|
+
* `SPOKE_CHECKOUT_TEMPLATE_PATHS` entry, a template config without exactly
|
|
265
|
+
* one `const SITE` line, a destination to write that resolves outside
|
|
266
|
+
* `targetDir`) throws before the first write.
|
|
569
267
|
*/
|
|
570
268
|
export function writeSpokeCheckoutSlice(targetDir, opts) {
|
|
571
|
-
const
|
|
269
|
+
const rendered = renderSpokeCheckoutFiles(opts.site, { signIn: opts.signIn });
|
|
572
270
|
const range = JSON.parse(substituteWorkspaceVersions(`{"${SPOKE_CHECKOUT_PACKAGE}": "workspace:*"}`, opts.versions));
|
|
573
271
|
const version = range[SPOKE_CHECKOUT_PACKAGE];
|
|
574
272
|
const pkgPath = join(targetDir, WEB_PACKAGE_JSON);
|
|
575
273
|
const before = readFileSync(pkgPath, "utf8");
|
|
576
274
|
const pkg = JSON.parse(before);
|
|
275
|
+
const markers = buildScaffoldMarkers(opts.projectName);
|
|
276
|
+
const copied = [];
|
|
277
|
+
for (const [path, source] of templateCheckoutFiles(opts.templatesDir)) {
|
|
278
|
+
let bytes = Buffer.from(renderTemplateFile(readFileSync(source), path, opts.versions, markers));
|
|
279
|
+
if (path === SPOKE_CHECKOUT_CONFIG_PATH) {
|
|
280
|
+
const text = bytes.toString("utf8");
|
|
281
|
+
const lines = text.match(SITE_LINE) ?? [];
|
|
282
|
+
if (lines.length !== 1) {
|
|
283
|
+
throw new TopologyError(`spoke checkout: the template's ${SPOKE_CHECKOUT_CONFIG_PATH} must hold exactly one const SITE line, found ${lines.length}`);
|
|
284
|
+
}
|
|
285
|
+
bytes = Buffer.from(text.replace(SITE_LINE, `const SITE = "${opts.site}";`), "utf8");
|
|
286
|
+
}
|
|
287
|
+
copied.push([path, bytes]);
|
|
288
|
+
}
|
|
289
|
+
const missing = SPOKE_CHECKOUT_TEMPLATE_PATHS.filter((entry) => !copied.some(([path]) => path === entry || path.startsWith(`${entry}/`)));
|
|
290
|
+
if (missing.length > 0) {
|
|
291
|
+
throw new TopologyError(`spoke checkout: the template has no ${missing.join(", ")}`);
|
|
292
|
+
}
|
|
293
|
+
// A file already there is skipped below, never written, so only a path
|
|
294
|
+
// that will be written must resolve inside the site.
|
|
295
|
+
const targetReal = realpathSync(targetDir);
|
|
296
|
+
for (const [path] of [...copied, ...rendered]) {
|
|
297
|
+
const abs = resolve(targetDir, path);
|
|
298
|
+
if (lstatExists(abs))
|
|
299
|
+
continue;
|
|
300
|
+
if (!resolvesInside(targetReal, abs)) {
|
|
301
|
+
throw new TopologyError(`spoke checkout: refusing to write outside the site: ${path}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
577
304
|
const rows = [];
|
|
578
|
-
for (const [path,
|
|
305
|
+
for (const [path, bytes] of copied) {
|
|
579
306
|
const abs = join(targetDir, path);
|
|
580
|
-
if (
|
|
307
|
+
if (lstatExists(abs)) {
|
|
308
|
+
rows.push({ path, action: "skipped", wanted: bytes.toString("utf8") });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
312
|
+
writeFileSync(abs, bytes);
|
|
313
|
+
opts.onWrite?.(path, bytes);
|
|
314
|
+
rows.push({ path, action: "written" });
|
|
315
|
+
}
|
|
316
|
+
for (const [path, text] of rendered) {
|
|
317
|
+
const abs = join(targetDir, path);
|
|
318
|
+
if (lstatExists(abs)) {
|
|
581
319
|
rows.push({ path, action: "skipped", wanted: text });
|
|
582
320
|
continue;
|
|
583
321
|
}
|
|
@@ -617,3 +355,43 @@ export function writeSpokeCheckoutSlice(targetDir, opts) {
|
|
|
617
355
|
}
|
|
618
356
|
return rows;
|
|
619
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Drop the checkout package from a scaffold that does not get the slice — a
|
|
360
|
+
* hub, or a spoke without `commerce` (#8748 D3). The template's
|
|
361
|
+
* `apps/web/package.json` depends on it because the monorepo's `apps/web`
|
|
362
|
+
* carries the standard. Every other key is kept in place; a site without the
|
|
363
|
+
* dependency, or without `apps/web/package.json`, is left as it is.
|
|
364
|
+
*/
|
|
365
|
+
export function dropSpokeCheckoutDependency(targetDir) {
|
|
366
|
+
const pkgPath = join(targetDir, WEB_PACKAGE_JSON);
|
|
367
|
+
if (!existsSync(pkgPath))
|
|
368
|
+
return;
|
|
369
|
+
const before = readFileSync(pkgPath, "utf8");
|
|
370
|
+
const pkg = JSON.parse(before);
|
|
371
|
+
const deps = pkg.dependencies;
|
|
372
|
+
if (deps === undefined ||
|
|
373
|
+
!Object.prototype.hasOwnProperty.call(deps, SPOKE_CHECKOUT_PACKAGE)) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
delete deps[SPOKE_CHECKOUT_PACKAGE];
|
|
377
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${before.endsWith("\n") ? "\n" : ""}`);
|
|
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";
|