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
|
@@ -57,6 +57,48 @@
|
|
|
57
57
|
* file leaves nothing to find, and its retry writes again. `subscribe` and
|
|
58
58
|
* `callback` never read the header. The key is never logged or echoed.
|
|
59
59
|
*
|
|
60
|
+
* CONTACT TERMS ACCEPTANCE (#8730). A `submit` whose `source` is a
|
|
61
|
+
* consent-bearing source (`isConsentLeadSource` in @working-theory/validation:
|
|
62
|
+
* the contact sources `contact-form`, `conversational-intake`, `flow:contact`,
|
|
63
|
+
* and since #9130 the Free apply page's `flow:job-application`) must carry a
|
|
64
|
+
* top-level `consent: true` — the boolean, nothing else — or it is
|
|
65
|
+
* `400 consent_required` before the tenant read or any write. An accepted one
|
|
66
|
+
* gets `payload.consent = { statement, acceptedAt }` written HERE, replacing
|
|
67
|
+
* any `payload.consent` the client sent: `statement` is the platform's
|
|
68
|
+
* contact terms statement (`CONTACT_FORM_TERMS_STATEMENT`) and `acceptedAt`
|
|
69
|
+
* this request's receive time. The `consent` flag itself is never stored. A
|
|
70
|
+
* lead with any other source, `subscribe` and `callback` are unchanged. A
|
|
71
|
+
* direct caller can still send `consent: true`; the gate makes acceptance an
|
|
72
|
+
* explicit, recorded assertion, not proof that a person read the terms.
|
|
73
|
+
*
|
|
74
|
+
* API↔MCP PARITY for `consent` (#8730): no guard entry, for the reason the
|
|
75
|
+
* Idempotency-Key paragraph below gives — this route parses no validation
|
|
76
|
+
* export, so it binds as `api-args-unbound`. `capture_lead` stamps `mcp`, a
|
|
77
|
+
* source this check never reads, so the tool is unchanged.
|
|
78
|
+
*
|
|
79
|
+
* A MAGNET'S COMPLETION (#8757 D6). After a `flow:<slug>` submit is written
|
|
80
|
+
* to the database, `services/magnet-completion.ts` reads that LIVE magnet's
|
|
81
|
+
* completion setting. On `invite` the 201 gains `next: { completion:
|
|
82
|
+
* "invite" }` and — on a first write, never a replay — the submitter is sent
|
|
83
|
+
* one confirmation email whose link starts sign-in with their address
|
|
84
|
+
* prefilled (capped per recipient and per magnet, counted in the database);
|
|
85
|
+
* on `checkout`, re-checked against the `commerce` claim and the published
|
|
86
|
+
* offerings, it gains `next: { completion: "checkout", checkoutOffering }`.
|
|
87
|
+
* `lead`, a draft or missing magnet, or a caller with nowhere to sign in
|
|
88
|
+
* leaves the 201 as it was. A failed read or send never changes the 201,
|
|
89
|
+
* and the completion read has a time budget (`MAGNET_COMPLETION_BUDGET_MS`).
|
|
90
|
+
* `next` is reachable only through a `flow:` source: `capture_lead` posts
|
|
91
|
+
* `source: "mcp"`, so the tool's arguments get the same answer as before and
|
|
92
|
+
* no parity guard row changes (NFR5).
|
|
93
|
+
*
|
|
94
|
+
* A CHECKOUT'S ACCOUNT LINK (#8757 D14). A checkout fulfilment's submit
|
|
95
|
+
* (`source: "web"`, `payload.page: "checkout"`) carries the caller's account
|
|
96
|
+
* origin into `routeLead` as `accountOrigin`, resolved exactly as the invite
|
|
97
|
+
* link's (`accountTargetFor`): the spoke's enabled sign-in client's callback
|
|
98
|
+
* origin, or `FRONTEND_URL`'s on a keyless build with an account area. With
|
|
99
|
+
* none, or a failed read, it is left out and the buyer's purchase email goes
|
|
100
|
+
* without the link. No other submit makes this read, and the 201 is unchanged.
|
|
101
|
+
*
|
|
60
102
|
* API↔MCP PARITY for the header (#7869 D9, roadmap §12 Q5): NO guard entry.
|
|
61
103
|
* The guard binds a route's arguments to the validation export its handler
|
|
62
104
|
* `.safeParse`s on `request.params|query|body`; the header check below is a
|
|
@@ -67,6 +109,11 @@
|
|
|
67
109
|
*/
|
|
68
110
|
|
|
69
111
|
import { track } from "@working-theory/analytics/server";
|
|
112
|
+
import {
|
|
113
|
+
CONTACT_FORM_TERMS_STATEMENT,
|
|
114
|
+
isConsentLeadSource,
|
|
115
|
+
type LeadSubmitNext,
|
|
116
|
+
} from "@working-theory/validation";
|
|
70
117
|
import type { FastifyPluginAsync } from "fastify";
|
|
71
118
|
|
|
72
119
|
import { acceptsKind, resolveLeadDelivery } from "../../lib/lead-delivery.js";
|
|
@@ -81,6 +128,11 @@ import {
|
|
|
81
128
|
routeLead,
|
|
82
129
|
type SourceMeta,
|
|
83
130
|
} from "../../services/lead-routing.js";
|
|
131
|
+
import {
|
|
132
|
+
accountTargetFor,
|
|
133
|
+
magnetCompletionWithinBudget,
|
|
134
|
+
sendInviteConfirmation,
|
|
135
|
+
} from "../../services/magnet-completion.js";
|
|
84
136
|
import { resolveCallerSite } from "../../services/public-site-resolver.js";
|
|
85
137
|
import { sendRequestConfirmLink } from "../../services/request-confirm.js";
|
|
86
138
|
|
|
@@ -105,6 +157,29 @@ function liftString(
|
|
|
105
157
|
return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
|
|
106
158
|
}
|
|
107
159
|
|
|
160
|
+
/** #8730, #9130 — the refusal a consent-bearing lead without terms acceptance gets. */
|
|
161
|
+
const CONSENT_REQUIRED_BODY = {
|
|
162
|
+
error: "consent_required",
|
|
163
|
+
message: "Accept the terms to send this form.",
|
|
164
|
+
} as const;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* #8730 D3, #9130 — the accepted terms, recorded by the server on a
|
|
168
|
+
* consent-bearing lead's payload, replacing any `consent` the client wrote
|
|
169
|
+
* there. Only a plain object payload is stamped: anything else is left for
|
|
170
|
+
* `routeLead`'s schema to refuse, never spread into an object that would then
|
|
171
|
+
* pass it.
|
|
172
|
+
*/
|
|
173
|
+
function withConsentRecord(payload: unknown, acceptedAt: string): unknown {
|
|
174
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
...(payload as Record<string, unknown>),
|
|
179
|
+
consent: { statement: CONTACT_FORM_TERMS_STATEMENT, acceptedAt },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
108
183
|
/**
|
|
109
184
|
* Build a typed RouteLeadInput from the raw request body for a given kind.
|
|
110
185
|
* Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
|
|
@@ -122,6 +197,7 @@ function buildInput(
|
|
|
122
197
|
kind: LeadKind,
|
|
123
198
|
body: Record<string, unknown>,
|
|
124
199
|
workspaceId: string,
|
|
200
|
+
receivedAt: string,
|
|
125
201
|
): RouteLeadInput {
|
|
126
202
|
const payload = (body["payload"] ?? {}) as Record<string, unknown>;
|
|
127
203
|
|
|
@@ -131,7 +207,9 @@ function buildInput(
|
|
|
131
207
|
siteId: workspaceId,
|
|
132
208
|
source: body["source"] as string,
|
|
133
209
|
email: liftString(body, payload, "email"),
|
|
134
|
-
payload
|
|
210
|
+
payload: isConsentLeadSource(body["source"])
|
|
211
|
+
? (withConsentRecord(payload, receivedAt) as Record<string, unknown>)
|
|
212
|
+
: payload,
|
|
135
213
|
};
|
|
136
214
|
}
|
|
137
215
|
if (kind === "subscribe") {
|
|
@@ -154,10 +232,32 @@ function buildInput(
|
|
|
154
232
|
};
|
|
155
233
|
}
|
|
156
234
|
|
|
235
|
+
/** A spoke checkout fulfilment's submit (#8757 D14): `source: "web"`, `payload.page: "checkout"`. */
|
|
236
|
+
function isCheckoutSubmit(body: Record<string, unknown>): boolean {
|
|
237
|
+
const payload = body["payload"];
|
|
238
|
+
return (
|
|
239
|
+
body["source"] === "web" &&
|
|
240
|
+
typeof payload === "object" &&
|
|
241
|
+
payload !== null &&
|
|
242
|
+
(payload as Record<string, unknown>)["page"] === "checkout"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
157
246
|
function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
158
247
|
return async (fastify) => {
|
|
159
248
|
fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
|
|
160
249
|
const body = request.body as Record<string, unknown>;
|
|
250
|
+
|
|
251
|
+
// #8730 D1, #9130 — a contact lead or a job application carries the
|
|
252
|
+
// visitor's terms acceptance, or it is refused before the tenant read
|
|
253
|
+
// and any write.
|
|
254
|
+
if (
|
|
255
|
+
kind === "submit" &&
|
|
256
|
+
isConsentLeadSource(body?.["source"]) &&
|
|
257
|
+
body?.["consent"] !== true
|
|
258
|
+
) {
|
|
259
|
+
return reply.status(400).send(CONSENT_REQUIRED_BODY);
|
|
260
|
+
}
|
|
161
261
|
// request.ip became attacker-influenceable when trustProxy: true went
|
|
162
262
|
// instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
|
|
163
263
|
// itself touched by that PR. Traced every consumer of this value
|
|
@@ -264,10 +364,20 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
264
364
|
sourceMeta.consentVisitorId = consentVisitorId;
|
|
265
365
|
}
|
|
266
366
|
|
|
367
|
+
// #8757 D14 — where a buyer's account lives, for the purchase email.
|
|
368
|
+
let accountOrigin: string | undefined;
|
|
369
|
+
if (kind === "submit" && isCheckoutSubmit(body)) {
|
|
370
|
+
const target = await accountTargetFor(caller).catch(() => null);
|
|
371
|
+
accountOrigin = target?.origin;
|
|
372
|
+
}
|
|
373
|
+
|
|
267
374
|
const result = await routeLead(
|
|
268
|
-
buildInput(kind, body, workspaceId),
|
|
375
|
+
buildInput(kind, body, workspaceId, sourceMeta.receivedAt),
|
|
269
376
|
sourceMeta,
|
|
270
|
-
|
|
377
|
+
{
|
|
378
|
+
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
379
|
+
...(accountOrigin !== undefined && { accountOrigin }),
|
|
380
|
+
},
|
|
271
381
|
);
|
|
272
382
|
|
|
273
383
|
if (!result.ok) {
|
|
@@ -357,10 +467,12 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
357
467
|
result.persisted === "db" &&
|
|
358
468
|
caller.kind !== "spoke"
|
|
359
469
|
) {
|
|
360
|
-
const callbackInput = buildInput(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
470
|
+
const callbackInput = buildInput(
|
|
471
|
+
kind,
|
|
472
|
+
body,
|
|
473
|
+
workspaceId,
|
|
474
|
+
sourceMeta.receivedAt,
|
|
475
|
+
) as Extract<RouteLeadInput, { kind: "callback" }>;
|
|
364
476
|
sendRequestConfirmLink(result.id, {
|
|
365
477
|
name: callbackInput.name,
|
|
366
478
|
phone: callbackInput.phone,
|
|
@@ -370,6 +482,50 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
370
482
|
});
|
|
371
483
|
}
|
|
372
484
|
|
|
485
|
+
// #8757 D6 — a written `flow:<slug>` submit answers its magnet's
|
|
486
|
+
// completion. A read that fails, or outlasts its budget, leaves the 201
|
|
487
|
+
// as it was: the lead is saved.
|
|
488
|
+
let next: LeadSubmitNext | undefined;
|
|
489
|
+
const source = body["source"];
|
|
490
|
+
if (
|
|
491
|
+
kind === "submit" &&
|
|
492
|
+
result.persisted === "db" &&
|
|
493
|
+
typeof source === "string" &&
|
|
494
|
+
source.startsWith("flow:")
|
|
495
|
+
) {
|
|
496
|
+
const slug = source.slice("flow:".length);
|
|
497
|
+
try {
|
|
498
|
+
const completion = await magnetCompletionWithinBudget(workspaceId, slug, () =>
|
|
499
|
+
accountTargetFor(caller),
|
|
500
|
+
);
|
|
501
|
+
if (completion !== null) {
|
|
502
|
+
next = completion.next;
|
|
503
|
+
if (completion.next.completion === "invite" && !result.replayed) {
|
|
504
|
+
const submitInput = buildInput(
|
|
505
|
+
kind,
|
|
506
|
+
body,
|
|
507
|
+
workspaceId,
|
|
508
|
+
sourceMeta.receivedAt,
|
|
509
|
+
) as Extract<RouteLeadInput, { kind: "submit" }>;
|
|
510
|
+
const locale = submitInput.payload["locale"];
|
|
511
|
+
void sendInviteConfirmation({
|
|
512
|
+
workspaceId,
|
|
513
|
+
slug,
|
|
514
|
+
email: submitInput.email,
|
|
515
|
+
locale: typeof locale === "string" ? locale : undefined,
|
|
516
|
+
completion,
|
|
517
|
+
now: new Date(),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
request.log.error(
|
|
523
|
+
{ event: "magnet-completion", error: error instanceof Error ? error.name : "unknown" },
|
|
524
|
+
"lead magnet completion read failed",
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
373
529
|
// Fire-and-forget: track() never throws; no need to hold the response.
|
|
374
530
|
// #7869 D6 — a replay records nothing new: the first write counted it.
|
|
375
531
|
if (!result.replayed) {
|
|
@@ -385,6 +541,7 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
|
|
|
385
541
|
id: result.id,
|
|
386
542
|
persisted: result.persisted,
|
|
387
543
|
email: result.email,
|
|
544
|
+
...(next === undefined ? {} : { next }),
|
|
388
545
|
});
|
|
389
546
|
});
|
|
390
547
|
};
|
|
@@ -199,6 +199,72 @@ through `metadata.site`.
|
|
|
199
199
|
site, which silences the hub's dunning record for that subscription — the
|
|
200
200
|
same key can already cancel or re-price it outright.
|
|
201
201
|
|
|
202
|
+
## Per-Organization endpoint (#8932)
|
|
203
|
+
|
|
204
|
+
`POST /v1/webhooks/stripe/:connectionToken` (`stripe-offerings.ts`) is the
|
|
205
|
+
receiver for one Organization's own Stripe connection — the Offerings money
|
|
206
|
+
path (spec `docs/specs/8932-the-per-organization-stripe-webhook-spec.md`,
|
|
207
|
+
contract `docs/specs/8879-the-offerings-security-contract-key-spec.md` W1,
|
|
208
|
+
W3–W11, G5, T8). The 43-character token selects one `StripeConnection` row
|
|
209
|
+
(`findStripeConnectionByWebhookToken`, #8907); that row's decrypted secret
|
|
210
|
+
verifies the signature through `verifyWebhookSignatureKeyless` (no env key),
|
|
211
|
+
and its `organizationId` is the only Organization the event can act for.
|
|
212
|
+
Registered beside the legacy route on every build, with its own 100/min
|
|
213
|
+
per-IP bucket; the commerce claim is read at call time.
|
|
214
|
+
|
|
215
|
+
**The ladder** — the first step that answers ends the request; every `200`
|
|
216
|
+
body is `{ received: true }` and the outcome is the log line's only:
|
|
217
|
+
|
|
218
|
+
| # | Step | Answer | Seen |
|
|
219
|
+
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- |
|
|
220
|
+
| 1 | the token resolves to no row (or is malformed) | `404 not-found`, logs nothing | no |
|
|
221
|
+
| 1a | the token lookup rejects | `503 webhook-unavailable` | no |
|
|
222
|
+
| 2 | the row has no usable secret | `503 webhook-not-configured` | no |
|
|
223
|
+
| 3–6 | signature settled, then the claim (as the legacy route): `undetermined` → `503` + `Retry-After: 300`; `unclaimed` → signed `200 dropped_no_commerce_claim`, unsigned the module-disabled `404`; then the held `400` | as stated | no |
|
|
224
|
+
| 7 | `event.livemode` is not the row's mode | `200 livemode_mismatch` | no |
|
|
225
|
+
| 8 | not `checkout.session.completed` / `.async_payment_succeeded` on a session object | `200 ignored_type` | no |
|
|
226
|
+
| 9–11 | the hub's session record (#8930): rejects / unreadable / none | `503 record-unavailable` / `500 record-unreadable` / `200 unrecorded` | no |
|
|
227
|
+
| 12 | another Organization's record, or `metadata.site` is not the record's workspace | `200 foreign_organization` | no |
|
|
228
|
+
| 13 | the record's mode is not the row's | `200 livemode_mismatch` | no |
|
|
229
|
+
| 14 | already seen for this Organization | `200 duplicate` | — |
|
|
230
|
+
| 15 | a `completed` session neither `paid` nor `no_payment_required` | `200 unpaid` | yes |
|
|
231
|
+
| 16 | the connection's client or its `listLineItems` call fails | `503 stripe-unavailable` (never the env key) | no |
|
|
232
|
+
| 17 | not exactly one line item whose Price id is the record's | `200 offering_mismatch` | yes |
|
|
233
|
+
| 18 | `routeLead` (below) | `200 fulfilled` / `already_fulfilled` / `unfulfillable`, else `503 fulfilment-unavailable` | yes / no |
|
|
234
|
+
|
|
235
|
+
**The lead.** One per session: `routeLead` with the idempotency key
|
|
236
|
+
`"stripe-offerings " + sessionId` — a space, which no `Idempotency-Key`
|
|
237
|
+
header can carry, so no anonymous lead can take the key first — and
|
|
238
|
+
`offeringsCheckout: true`. Its offering is the record's FULL lookup key,
|
|
239
|
+
never `metadata.ailk_offering`. A live session's lead is marked
|
|
240
|
+
`spokeCheckout`, the hold #8793's `releaseHeldCheckoutPlan` releases at the
|
|
241
|
+
buyer's verified first link; no path here writes `Person.planOffering`. A
|
|
242
|
+
test-mode session's lead is written unmarked: no hold, purchase message,
|
|
243
|
+
email or consent grant. A replay or a conflict on that key is
|
|
244
|
+
`already_fulfilled`; a lead the database did not take is a `503`, so Stripe
|
|
245
|
+
retries.
|
|
246
|
+
|
|
247
|
+
**The legacy endpoint steps aside (W8).** `POST /v1/webhooks/stripe` acks
|
|
248
|
+
any event carrying an `ailk_offering` key — in a session's metadata or any
|
|
249
|
+
site-stamp source above — with `200 { received: true }` and
|
|
250
|
+
`outcome: "offerings_path"`, **before** `markSeen`, and writes nothing.
|
|
251
|
+
Every event without the key takes the legacy path unchanged.
|
|
252
|
+
|
|
253
|
+
**Two dedup stores.** The legacy route keeps `hasSeen` / `markSeen`; the
|
|
254
|
+
per-Organization route uses its own `hasSeenForOrganization` /
|
|
255
|
+
`markSeenForOrganization`, keyed `<organizationId>:<eventId>`. Neither
|
|
256
|
+
endpoint's store can swallow the other's event. Both are in-memory per
|
|
257
|
+
instance; the per-Organization route's correctness rests on the lead's
|
|
258
|
+
database key.
|
|
259
|
+
|
|
260
|
+
**Logs.** Each line carries `organizationId`, `eventType`, an 8-character
|
|
261
|
+
`eventIdPrefix` and `outcome` (fewer before an event is verified) — never
|
|
262
|
+
the token, the secret, a full Stripe id, an email, a lookup key, a metadata
|
|
263
|
+
value or Stripe product text. Request and error lines log the path with
|
|
264
|
+
the token replaced whole by `:connectionToken` (`redactLogPath`).
|
|
265
|
+
|
|
266
|
+
**W11.** No billing identity is bound from a connection's event.
|
|
267
|
+
|
|
202
268
|
## Dunning (#6034)
|
|
203
269
|
|
|
204
270
|
Two arms record a per-subscriber lifecycle state so a failed renewal is no
|