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
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
getPageSections,
|
|
47
47
|
pageToJsonLd,
|
|
48
|
+
siteOrigin,
|
|
48
49
|
type JsonLdObject,
|
|
49
50
|
} from "@working-theory/page-renderer";
|
|
50
51
|
import {
|
|
@@ -110,6 +111,13 @@ if (process.env.AILK_CONTENT_ROOT === undefined) {
|
|
|
110
111
|
process.env.AILK_CONTENT_ROOT = CONTENT_ROOT;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// A page that checks its own locale (`lib/locale-guard`, the meeting page)
|
|
115
|
+
// reads the enabled list next.config.mjs bakes in; pin it to the locale this
|
|
116
|
+
// file renders, as a built app would carry it.
|
|
117
|
+
if (process.env.NEXT_PUBLIC_ENABLED_LOCALES === undefined) {
|
|
118
|
+
process.env.NEXT_PUBLIC_ENABLED_LOCALES = DEFAULT_LOCALE;
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
// Mirrors the locale-directory rule in tools/content-compile and the adapter.
|
|
114
122
|
const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
115
123
|
|
|
@@ -194,11 +202,8 @@ function extractJsonLdBlocks(container: HTMLElement): JsonLdObject[] {
|
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
const SITE_IDENTITY = readSiteIdentity(CONTENT_ROOT);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
process.env.MARKETING_WEB_BASE_URL ??
|
|
200
|
-
"http://localhost:3000"
|
|
201
|
-
).replace(/\/+$/, "");
|
|
205
|
+
// The resolver's own origin chain (#8797 D5), so this check cannot drift from it.
|
|
206
|
+
const SITE_ORIGIN = siteOrigin(SITE_IDENTITY);
|
|
202
207
|
|
|
203
208
|
/**
|
|
204
209
|
* The sitewide identity trio every rendered page carries when the site's own
|
|
@@ -297,12 +302,24 @@ describe("structured data — every content page builds valid JSON-LD", () => {
|
|
|
297
302
|
* exempt route whose page.tsx does render through `renderPage` fails too,
|
|
298
303
|
* because the reason given for it is false.
|
|
299
304
|
*/
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
+
// The marker both standard spoke checkout pages carry in their header comment
|
|
306
|
+
// (apps/web/app/[locale]/checkout/{thank-you,unavailable}/page.tsx, #8748 D5).
|
|
307
|
+
const CHECKOUT_PAGE_MARKER = "Standard spoke checkout page (#8748)";
|
|
308
|
+
// The marker create-ailk rendered before #8748. A spoke scaffolded then keeps
|
|
309
|
+
// its rendered pages through `create-ailk update` (they are not in its
|
|
310
|
+
// record), so its updated copy of this test must still exempt them.
|
|
311
|
+
const LEGACY_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
|
|
305
312
|
const CHECKOUT_PAGE_NOINDEX_LINE = "robots: { index: false, follow: false }";
|
|
313
|
+
// #8840 D13 — the marker every standard magnet page carries in its header
|
|
314
|
+
// comment (`apps/web/app/[locale]/flows/[slug]/magnet-page.tsx`), and the
|
|
315
|
+
// renderer it must import. A magnet page renders no `renderPage` call this
|
|
316
|
+
// harness can read, and its landing's JSON-LD comes from the hub's record, so
|
|
317
|
+
// a page carrying both is exempt at ANY route: `create-ailk add-route magnet`
|
|
318
|
+
// writes a pair at a path the site picks. Keyed on the file, as #8513 is.
|
|
319
|
+
const MAGNET_PAGE_MARKER = "Standard magnet page (#8840)";
|
|
320
|
+
const MAGNET_RENDERER_IMPORT = "flows/[slug]/magnet-page";
|
|
321
|
+
const MAGNET_PAGE_REASON =
|
|
322
|
+
"standard magnet page (#8840) — rendered by flows/[slug]/magnet-page; its landing's JSON-LD comes from the hub's flow record";
|
|
306
323
|
|
|
307
324
|
const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
308
325
|
route: string;
|
|
@@ -313,21 +330,22 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
313
330
|
* entry naming no strings applies to its route unconditionally, as every
|
|
314
331
|
* entry did before. Keys the exemption on the file, not the topology seam:
|
|
315
332
|
* a spoke can carry a hand-written page at this route, and a site can
|
|
316
|
-
* edit the generated one — either is checked like any other route.
|
|
333
|
+
* edit the generated one — either is checked like any other route. A
|
|
334
|
+
* nested list is any one of its strings.
|
|
317
335
|
*/
|
|
318
|
-
requires?: readonly string[]
|
|
336
|
+
requires?: ReadonlyArray<string | readonly string[]>;
|
|
319
337
|
}> = [
|
|
320
338
|
// Fumadocs routes: JSON-LD is emitted by @working-theory/fumadocs-schema
|
|
321
339
|
// from the fumadocs `.source` collection, which is not built in this
|
|
322
340
|
// harness. Not checked here.
|
|
323
341
|
{
|
|
324
342
|
route: "blog",
|
|
325
|
-
reason: "fumadocs route —
|
|
343
|
+
reason: "fumadocs route — its .source collection is not built in this harness",
|
|
326
344
|
subtree: true,
|
|
327
345
|
},
|
|
328
346
|
{
|
|
329
347
|
route: "docs",
|
|
330
|
-
reason: "fumadocs route —
|
|
348
|
+
reason: "fumadocs route — its .source collection is not built in this harness",
|
|
331
349
|
subtree: true,
|
|
332
350
|
},
|
|
333
351
|
// Hub routes that enumerate a detail type with no list page type of their
|
|
@@ -337,6 +355,10 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
337
355
|
route: "how-to",
|
|
338
356
|
reason: "hand-composed listing; no HowTo list type exists",
|
|
339
357
|
},
|
|
358
|
+
{
|
|
359
|
+
route: "careers/[slug]/[action]",
|
|
360
|
+
reason: "the Free apply form for a posting; noindex, no JSON-LD (#8849 D7)",
|
|
361
|
+
},
|
|
340
362
|
{
|
|
341
363
|
route: "software",
|
|
342
364
|
reason: "hand-composed listing; no SoftwareProduct list type exists",
|
|
@@ -365,11 +387,23 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
365
387
|
reason: "interactive flow engine, API-backed",
|
|
366
388
|
subtree: true,
|
|
367
389
|
},
|
|
368
|
-
|
|
390
|
+
// #8843 D7 — the meeting page `meet/[slug]` composes its own body from a
|
|
391
|
+
// `BookCall` seed (`getMdxSource` + `pageToJsonLd`), so it is checked like
|
|
392
|
+
// any route; the two indexes and the confirmation page are not pages.
|
|
393
|
+
{ route: "meet", reason: "booking index, API-backed (#8843 D8)" },
|
|
394
|
+
{ route: "meet/confirmed", reason: "booking confirmation, API-backed" },
|
|
395
|
+
{ route: "private", reason: "private booking index, API-backed and noindex (#8843 D8)" },
|
|
369
396
|
{
|
|
370
397
|
route: "unsubscribe",
|
|
371
398
|
reason: "app-shaped landing, API-backed; no content page behind it",
|
|
372
399
|
},
|
|
400
|
+
// #8848 D12 — `/downloads` and `/downloads/<slug>` are permanent redirects
|
|
401
|
+
// into `/reports`; a redirect renders no page.
|
|
402
|
+
{
|
|
403
|
+
route: "downloads",
|
|
404
|
+
reason: "permanent redirect into /reports (#8848 D12); renders no page",
|
|
405
|
+
subtree: true,
|
|
406
|
+
},
|
|
373
407
|
// #6084 — both purchase return pages read a Checkout Session (or read
|
|
374
408
|
// nothing, for cancel) and render via SectionRenderer directly; neither
|
|
375
409
|
// is a content-adapter page, and the paid render's own schema.org node
|
|
@@ -388,22 +422,28 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
388
422
|
reason: "auth surfaces, not content pages",
|
|
389
423
|
subtree: true,
|
|
390
424
|
},
|
|
391
|
-
// #8513 —
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
425
|
+
// #8513 — the two standard spoke checkout return pages (#8748 D5):
|
|
426
|
+
// noindex, rendered through SectionRenderer directly, no JSON-LD. Exempt
|
|
427
|
+
// only when the page on disk still carries both the standard's marker and
|
|
428
|
+
// the noindex line (see `requires` above) — a hand-written or edited page
|
|
429
|
+
// at either route is checked like any other.
|
|
396
430
|
{
|
|
397
431
|
route: "checkout/thank-you",
|
|
398
432
|
reason:
|
|
399
|
-
"spoke checkout return page (
|
|
400
|
-
requires: [
|
|
433
|
+
"spoke checkout return page (#8748) — noindex, renders sections only, no JSON-LD",
|
|
434
|
+
requires: [
|
|
435
|
+
[CHECKOUT_PAGE_MARKER, LEGACY_CHECKOUT_PAGE_MARKER],
|
|
436
|
+
CHECKOUT_PAGE_NOINDEX_LINE,
|
|
437
|
+
],
|
|
401
438
|
},
|
|
402
439
|
{
|
|
403
440
|
route: "checkout/unavailable",
|
|
404
441
|
reason:
|
|
405
|
-
"spoke checkout return page (
|
|
406
|
-
requires: [
|
|
442
|
+
"spoke checkout return page (#8748) — noindex, renders sections only, no JSON-LD",
|
|
443
|
+
requires: [
|
|
444
|
+
[CHECKOUT_PAGE_MARKER, LEGACY_CHECKOUT_PAGE_MARKER],
|
|
445
|
+
CHECKOUT_PAGE_NOINDEX_LINE,
|
|
446
|
+
],
|
|
407
447
|
},
|
|
408
448
|
];
|
|
409
449
|
|
|
@@ -431,6 +471,9 @@ function walkRoutes(dir: string, out: RouteEntry[] = []): RouteEntry[] {
|
|
|
431
471
|
}
|
|
432
472
|
|
|
433
473
|
function exemption(route: string, src: string): string | undefined {
|
|
474
|
+
if (src.includes(MAGNET_PAGE_MARKER) && src.includes(MAGNET_RENDERER_IMPORT)) {
|
|
475
|
+
return MAGNET_PAGE_REASON;
|
|
476
|
+
}
|
|
434
477
|
const entry = NO_STRUCTURED_DATA_ROUTES.find(
|
|
435
478
|
(e) =>
|
|
436
479
|
route === e.route ||
|
|
@@ -439,7 +482,9 @@ function exemption(route: string, src: string): string | undefined {
|
|
|
439
482
|
if (entry === undefined) return undefined;
|
|
440
483
|
if (
|
|
441
484
|
entry.requires !== undefined &&
|
|
442
|
-
!entry.requires.every((s) =>
|
|
485
|
+
!entry.requires.every((s) =>
|
|
486
|
+
typeof s === "string" ? src.includes(s) : s.some((any) => src.includes(any)),
|
|
487
|
+
)
|
|
443
488
|
) {
|
|
444
489
|
return undefined;
|
|
445
490
|
}
|
|
@@ -645,6 +690,12 @@ describe("structured data — exemption(route, src) keys the generated checkout
|
|
|
645
690
|
expect(exemption("checkout/unavailable", NO_NOINDEX_SRC)).toBeUndefined();
|
|
646
691
|
});
|
|
647
692
|
|
|
693
|
+
it("still exempts a page create-ailk rendered before #8748, which update leaves in place", () => {
|
|
694
|
+
const legacy = `// The checkout thank-you page — ${LEGACY_CHECKOUT_PAGE_MARKER}.\nexport async function generateMetadata() {\n return { ${CHECKOUT_PAGE_NOINDEX_LINE} };\n}\n`;
|
|
695
|
+
expect(exemption("checkout/thank-you", legacy)).toContain("noindex");
|
|
696
|
+
expect(exemption("checkout/unavailable", legacy)).toContain("noindex");
|
|
697
|
+
});
|
|
698
|
+
|
|
648
699
|
it("does not exempt a neighboring checkout route or an unrelated route", () => {
|
|
649
700
|
expect(exemption("checkout/other", GENERATED_SRC)).toBeUndefined();
|
|
650
701
|
expect(exemption("about", GENERATED_SRC)).toBeUndefined();
|
|
@@ -661,6 +712,31 @@ describe("structured data — exemption(route, src) keys the generated checkout
|
|
|
661
712
|
});
|
|
662
713
|
});
|
|
663
714
|
|
|
715
|
+
// ── 2b. exemption(route, src) — the standard magnet pages (#8840 D13) ────
|
|
716
|
+
|
|
717
|
+
describe("structured data — exemption(route, src) keys a standard magnet page on the file (#8840)", () => {
|
|
718
|
+
// The import line is assembled, not written out, so this file's own text
|
|
719
|
+
// carries no import of a path a scaffold's copy of it may not ship.
|
|
720
|
+
const IMPORT_LINE = ["import { magnetLanding } from ", JSON.stringify(`../${MAGNET_RENDERER_IMPORT}`), ";"].join("");
|
|
721
|
+
const PAIR_SRC = `// Generated by create-ailk add-route magnet.\n// ${MAGNET_PAGE_MARKER} — the landing of the /diagnostic magnet pair.\n${IMPORT_LINE}\n`;
|
|
722
|
+
|
|
723
|
+
it("exempts a page carrying the marker and the renderer import, at any route", () => {
|
|
724
|
+
expect(exemption("diagnostic", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
|
|
725
|
+
expect(exemption("diagnostic/start", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
|
|
726
|
+
expect(exemption("careers/writer", PAIR_SRC)).toBe(MAGNET_PAGE_REASON);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("checks a page carrying the marker but no import of the renderer like any other", () => {
|
|
730
|
+
const noImport = `// ${MAGNET_PAGE_MARKER}\nexport default function Page() { return null; }\n`;
|
|
731
|
+
expect(exemption("diagnostic", noImport)).toBeUndefined();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("checks a page importing the renderer without the marker like any other", () => {
|
|
735
|
+
const noMarker = `${IMPORT_LINE}\n`;
|
|
736
|
+
expect(exemption("diagnostic", noMarker)).toBeUndefined();
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
664
740
|
// ── 3. The two defect shapes that shipped, pinned ───────────────────────────
|
|
665
741
|
//
|
|
666
742
|
// These prove the checks above reach the validator rule that rejects each
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* account-invite.ts — the /contact thank-you's account link (#8757 D8): the
|
|
3
|
+
* path that starts sign-in on this site with the submitter's email prefilled
|
|
4
|
+
* and lands on their account. It signs no one in; the email is only the
|
|
5
|
+
* sign-in form's starting value (NFR2).
|
|
6
|
+
*
|
|
7
|
+
* Built from the generated sign-in provider (`SIGN_IN_PROVIDER`):
|
|
8
|
+
* - `site` — this site's own `/sign-in`, locale-prefixed like every page
|
|
9
|
+
* (`localeHref`), with `next` = the locale's `/account` and `email`;
|
|
10
|
+
* - `hub` — a hub-sign-in spoke starts at `/api/hub-session/sign-in`
|
|
11
|
+
* (`accountInvitePath("hub", …)`), which hands the hint to the hub;
|
|
12
|
+
* - `none` — no sign-in, so no link.
|
|
13
|
+
* An email the hint schema refuses (`accountEmailHintSchema`) gives no link.
|
|
14
|
+
* The query is built with `URLSearchParams` (SEC-8757-L).
|
|
15
|
+
*/
|
|
16
|
+
import { localeHref } from "@working-theory/ui";
|
|
17
|
+
import { accountEmailHintSchema, accountInvitePath } from "@working-theory/validation";
|
|
18
|
+
|
|
19
|
+
import { SIGN_IN_PROVIDER } from "./topology.generated";
|
|
20
|
+
|
|
21
|
+
export function accountInviteHref(locale: string, email: string): string | null {
|
|
22
|
+
const hint = accountEmailHintSchema.safeParse(email);
|
|
23
|
+
if (!hint.success) return null;
|
|
24
|
+
switch (SIGN_IN_PROVIDER) {
|
|
25
|
+
case "site": {
|
|
26
|
+
const query = new URLSearchParams({
|
|
27
|
+
next: localeHref(locale, "/account"),
|
|
28
|
+
email: hint.data,
|
|
29
|
+
});
|
|
30
|
+
return `${localeHref(locale, "/sign-in")}?${query.toString()}`;
|
|
31
|
+
}
|
|
32
|
+
case "hub":
|
|
33
|
+
return accountInvitePath("hub", hint.data);
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* signed-in account area — `/account` and everything under it — which the
|
|
13
13
|
* layout also renders without the marketing chrome.
|
|
14
14
|
*
|
|
15
|
+
* `isBookingPath()` (#8843 D10) is the same predicate for the scheduling
|
|
16
|
+
* module's booking pages — `/meet`, `/meet/<slug>` and `/private`, never
|
|
17
|
+
* `/meet/confirmed` — which the layout renders chrome-free only where the
|
|
18
|
+
* module is installed (`lib/modules.generated.ts`). This module imports
|
|
19
|
+
* nothing, so edge code can import it too.
|
|
20
|
+
*
|
|
15
21
|
* No Next.js import — pure functions, asserted directly by their own suite.
|
|
16
22
|
*/
|
|
17
23
|
|
|
@@ -62,6 +68,21 @@ export function isAccountPath(
|
|
|
62
68
|
return firstSegment(pathname, locales) === "account";
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether `pathname` addresses a booking page: `/private`, `/meet`, or
|
|
73
|
+
* `/meet/<slug>` for any slug but `confirmed` (the booker's confirmation
|
|
74
|
+
* page, which keeps the marketing chrome). The same prefix rule and the same
|
|
75
|
+
* fail-safe direction as `isAuthPath()`.
|
|
76
|
+
*/
|
|
77
|
+
export function isBookingPath(
|
|
78
|
+
pathname: string | undefined | null,
|
|
79
|
+
locales: readonly string[],
|
|
80
|
+
): boolean {
|
|
81
|
+
const [first, second] = segments(pathname, locales);
|
|
82
|
+
if (first === "private") return true;
|
|
83
|
+
return first === "meet" && second !== "confirmed";
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* The first path segment after an enabled-locale prefix, or `undefined` for
|
|
67
88
|
* an absent or empty pathname.
|
|
@@ -70,7 +91,18 @@ function firstSegment(
|
|
|
70
91
|
pathname: string | undefined | null,
|
|
71
92
|
locales: readonly string[],
|
|
72
93
|
): string | undefined {
|
|
73
|
-
|
|
94
|
+
return segments(pathname, locales)[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The path's segments after an enabled-locale prefix (empty for an absent or
|
|
99
|
+
* empty pathname). A trailing slash adds no segment.
|
|
100
|
+
*/
|
|
101
|
+
function segments(
|
|
102
|
+
pathname: string | undefined | null,
|
|
103
|
+
locales: readonly string[],
|
|
104
|
+
): string[] {
|
|
105
|
+
if (!pathname) return [];
|
|
74
106
|
|
|
75
107
|
// Drop a query string / fragment — `x-pathname` carries the path, but a
|
|
76
108
|
// caller passing a full request path should get the same verdict.
|
|
@@ -80,5 +112,7 @@ function firstSegment(
|
|
|
80
112
|
? path.replace(new RegExp(`^/(${locales.join("|")})(?=/|$)`), "")
|
|
81
113
|
: path;
|
|
82
114
|
|
|
83
|
-
|
|
115
|
+
const parts = bare.split("/").slice(1);
|
|
116
|
+
if (parts.at(-1) === "") parts.pop();
|
|
117
|
+
return parts;
|
|
84
118
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// checkout-config.ts — this spoke's checkout stamp, as data (#7838, #8748).
|
|
2
|
+
//
|
|
3
|
+
// Written once by create-ailk and yours from then on: `create-ailk update`
|
|
4
|
+
// never rewrites it. Keep it to data: no import, and no code. A spoke sells
|
|
5
|
+
// through its hub's Offerings buy route, /api/offerings/checkout/<lookup_key>
|
|
6
|
+
// (#9142); the thank-you page and, on a spoke that signs in through its hub,
|
|
7
|
+
// the billing action read `site` from here.
|
|
8
|
+
//
|
|
9
|
+
// site The paid-session stamp the thank-you page checks
|
|
10
|
+
// (`metadata.site`) before it prefills "Open your account". Never
|
|
11
|
+
// "default". The create-ailk writer stamps the line below.
|
|
12
|
+
const SITE = "example-site";
|
|
13
|
+
|
|
14
|
+
export const checkoutConfig = {
|
|
15
|
+
site: SITE,
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consent-policies.ts — the one policy map an agreement surface links
|
|
3
|
+
* through (#8765 D9, spec docs/specs/8765-sign-up-refuses-without-the-spec.md).
|
|
4
|
+
*
|
|
5
|
+
* A consent statement names its links by key (`[Terms](terms)`); the keys
|
|
6
|
+
* resolve against the registry's `policies` map. This localizes that map, so
|
|
7
|
+
* the sign-up page, the account interstitial and the Privacy tab link Terms,
|
|
8
|
+
* Privacy Policy and Working Agreement to the same three pages `/contact`
|
|
9
|
+
* builds inline (#8761 decision, C6).
|
|
10
|
+
*/
|
|
11
|
+
import { localeHref } from "@working-theory/ui";
|
|
12
|
+
import { SITE_CONSENT_CONFIG_DEFAULTS } from "@working-theory/validation";
|
|
13
|
+
|
|
14
|
+
/** The registry's policy map with each path localized for `locale`. */
|
|
15
|
+
export function consentPoliciesFor(locale: string): Record<string, string> {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(SITE_CONSENT_CONFIG_DEFAULTS.policies).map(([key, path]) => [
|
|
18
|
+
key,
|
|
19
|
+
localeHref(locale, path),
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -37,6 +37,7 @@ import { siteChrome, type SiteChrome } from "./site-chrome";
|
|
|
37
37
|
export const DECK_ROUTES = [
|
|
38
38
|
"/about",
|
|
39
39
|
"/alternatives",
|
|
40
|
+
"/careers",
|
|
40
41
|
"/contact",
|
|
41
42
|
"/faq",
|
|
42
43
|
"/launch-service",
|
|
@@ -55,6 +56,8 @@ export const DECK_ROUTES = [
|
|
|
55
56
|
type Chrome = SiteChrome & {
|
|
56
57
|
deckGrounds?: DeckPageInputs["deckGrounds"];
|
|
57
58
|
deckPresentation?: true;
|
|
59
|
+
/** #8773 — present only when the design chooses bare slide anchors. */
|
|
60
|
+
deckAnchors?: "bare";
|
|
58
61
|
};
|
|
59
62
|
const chrome: Chrome = siteChrome;
|
|
60
63
|
|
|
@@ -180,6 +183,10 @@ async function inputsFor(ctx: DeckPageContext): Promise<DeckPageInputs | null> {
|
|
|
180
183
|
return {
|
|
181
184
|
...(chrome.deckGrounds !== undefined ? { deckGrounds: chrome.deckGrounds } : {}),
|
|
182
185
|
deckPresentation: chrome.deckPresentation === true,
|
|
186
|
+
// #8773 — a page that presents names its slides as the design chose.
|
|
187
|
+
...(chrome.deckPresentation === true && chrome.deckAnchors === "bare"
|
|
188
|
+
? { deckAnchorStyle: "bare" as const }
|
|
189
|
+
: {}),
|
|
183
190
|
};
|
|
184
191
|
}
|
|
185
192
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a deliverable grant id must match before anything spends a hub
|
|
3
|
+
* call on it (#9125). One definition, read by the report page
|
|
4
|
+
* (`app/[locale]/reports/[slug]/page.tsx`, `resolveGrantId`) and its file
|
|
5
|
+
* route (`app/[locale]/reports/[slug]/file/route.ts`), so the page and the
|
|
6
|
+
* bytes can never disagree on which ids are worth probing.
|
|
7
|
+
*
|
|
8
|
+
* A grant id is minted as `dgrant_<uuidv7>` (database/inbox/schema.prisma).
|
|
9
|
+
* This is deliberately a charset-and-length check, not a `dgrant_` prefix
|
|
10
|
+
* match: the API stays the authority on which ids are real, and a future id
|
|
11
|
+
* scheme should not need an edit here. 128 is a generous ceiling over today's
|
|
12
|
+
* `dgrant_<uuid>`.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately base-tree, not slice-owned: a keyed scaffold's
|
|
15
|
+
* signal-collectors bundle installs the page and route that import it, so it
|
|
16
|
+
* must already be in every scaffold.
|
|
17
|
+
*/
|
|
18
|
+
export const GRANT_ID_SHAPE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HubSignInGate (#7894 D10) as #8790 AC-1/AC-2 shape it: the site's inner-page
|
|
3
|
+
* header (back arrow + mark), a label-size heading, the one hub sign-in
|
|
4
|
+
* action, and a prospect line only when the site's messages carry it.
|
|
5
|
+
*/
|
|
6
|
+
import { render, screen } from "@testing-library/react";
|
|
7
|
+
import en from "../../../messages/en.json";
|
|
8
|
+
import { HubSignInGate } from "../sign-in-gate";
|
|
9
|
+
|
|
10
|
+
type Gate = Record<string, string>;
|
|
11
|
+
let gateMessages: Gate;
|
|
12
|
+
|
|
13
|
+
jest.mock("next-intl/server", () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
getTranslations: jest.fn(async () => {
|
|
16
|
+
const t = (key: string) => gateMessages[key] ?? `copy:${key}`;
|
|
17
|
+
t.has = (key: string) => key in gateMessages;
|
|
18
|
+
return t;
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const mockConfig = jest.fn();
|
|
23
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; hubSessionConfig() is a pure env read
|
|
24
|
+
jest.mock("../config", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
hubSessionConfig: () => mockConfig(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; siteLogo() reads the site's brand files
|
|
30
|
+
jest.mock("../../site-logo", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
siteLogo: () => (
|
|
33
|
+
<img src="/logomark.svg" alt="Site" data-testid="site-mark" />
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
async function renderGate(pathname?: string): Promise<void> {
|
|
38
|
+
const requestHeaders = new Headers();
|
|
39
|
+
if (pathname) requestHeaders.set("x-pathname", pathname);
|
|
40
|
+
render(await HubSignInGate({ locale: "en", requestHeaders }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
gateMessages = { ...(en.account.gate as Gate) };
|
|
45
|
+
mockConfig.mockReset().mockReturnValue({ clientId: "spoke_1" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("<HubSignInGate/> (#8790 AC-1)", () => {
|
|
49
|
+
it("wears the site's inner-page header: a back arrow to the home and the mark", async () => {
|
|
50
|
+
await renderGate("/account");
|
|
51
|
+
const header = screen.getByTestId("account-sign-in-gate-header");
|
|
52
|
+
const back = screen.getByTestId("nav-back-link");
|
|
53
|
+
expect(header).toContainElement(back);
|
|
54
|
+
expect(back).toHaveAttribute("href", "/");
|
|
55
|
+
expect(back).toHaveAccessibleName(en.account.gate.homeLink);
|
|
56
|
+
expect(screen.getByTestId("account-sign-in-gate-mark")).toContainElement(
|
|
57
|
+
screen.getByTestId("site-mark"),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("sets the heading at the label size and offers the one sign-in action", async () => {
|
|
62
|
+
await renderGate("/account/settings");
|
|
63
|
+
const heading = screen.getByRole("heading", { level: 1 });
|
|
64
|
+
expect(heading).toHaveTextContent(en.account.gate.hubHeadline);
|
|
65
|
+
expect(heading).toHaveClass("text-heading-2");
|
|
66
|
+
expect(screen.getByTestId("account-hub-sign-in")).toHaveAttribute(
|
|
67
|
+
"href",
|
|
68
|
+
"/api/hub-session/sign-in?next=%2Faccount%2Fsettings",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders no prospect line by default (the shipped messages carry no prospect keys)", async () => {
|
|
73
|
+
await renderGate();
|
|
74
|
+
expect(screen.queryByTestId("account-sign-in-gate-prospect")).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("<HubSignInGate/> prospect line (#8790 AC-2)", () => {
|
|
79
|
+
it("renders the line and its link when the site fills all three keys", async () => {
|
|
80
|
+
gateMessages.prospectPrompt = "New here?";
|
|
81
|
+
gateMessages.prospectLink = "See if we fit";
|
|
82
|
+
gateMessages.prospectHref = "/fit";
|
|
83
|
+
await renderGate();
|
|
84
|
+
const line = screen.getByTestId("account-sign-in-gate-prospect");
|
|
85
|
+
expect(line).toHaveTextContent("New here? See if we fit.");
|
|
86
|
+
expect(screen.getByRole("link", { name: "See if we fit" })).toHaveAttribute(
|
|
87
|
+
"href",
|
|
88
|
+
"/fit",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders nothing when any of the three keys is absent", async () => {
|
|
93
|
+
gateMessages.prospectPrompt = "New here?";
|
|
94
|
+
gateMessages.prospectLink = "See if we fit";
|
|
95
|
+
await renderGate();
|
|
96
|
+
expect(screen.queryByTestId("account-sign-in-gate-prospect")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps the not-configured state when the hub is not configured", async () => {
|
|
100
|
+
mockConfig.mockReturnValue(null);
|
|
101
|
+
await renderGate();
|
|
102
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
103
|
+
en.account.gate.emptyHeadline,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByTestId("account-sign-in-gate-home")).toHaveAttribute(
|
|
106
|
+
"href",
|
|
107
|
+
"/",
|
|
108
|
+
);
|
|
109
|
+
expect(screen.queryByTestId("account-hub-sign-in")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|