create-ailk 0.4.0 → 0.6.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/README.md +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import cors from "@fastify/cors";
|
|
13
13
|
import rateLimit from "@fastify/rate-limit";
|
|
14
|
+
import { idRegex } from "@working-theory/ids";
|
|
14
15
|
import Fastify from "fastify";
|
|
15
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
FastifyPluginAsync,
|
|
18
|
+
FastifyPluginCallback,
|
|
19
|
+
FastifyRequest,
|
|
20
|
+
} from "fastify";
|
|
16
21
|
|
|
17
22
|
import { config } from "./config/index.js";
|
|
18
23
|
import { isModuleEnabled } from "./config/modules.js";
|
|
19
24
|
import { registerErrorSerializer } from "./lib/error-serializer.js";
|
|
20
25
|
import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
|
|
26
|
+
import { requireCommerceClaim } from "./lib/require-commerce-claim.js";
|
|
27
|
+
import { assertSiteAliasResolves } from "./lib/site.js";
|
|
21
28
|
import { loadSlice } from "./lib/slice-load.js";
|
|
22
29
|
import { authPreHandlerPlugin } from "./middleware/auth.js";
|
|
30
|
+
import { defaultTenantPreHandlerPlugin } from "./middleware/default-tenant.js";
|
|
23
31
|
import {
|
|
24
32
|
rateLimitMiddleware,
|
|
25
33
|
DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
@@ -36,24 +44,87 @@ import {
|
|
|
36
44
|
import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
37
45
|
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
38
46
|
import { tokenRoutes } from "./routes/auth/tokens.js";
|
|
47
|
+
import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
|
|
39
48
|
import { userRoutes } from "./routes/auth/user.js";
|
|
40
49
|
import { getPageRoute } from "./routes/content/get-page.js";
|
|
41
50
|
import { listPagesRoute } from "./routes/content/list-pages.js";
|
|
42
51
|
import { listTypesRoute } from "./routes/content/list-types.js";
|
|
43
52
|
import {
|
|
53
|
+
CONTENT_RATE_LIMIT_AUTH,
|
|
44
54
|
CONTENT_RATE_LIMIT_UNAUTH,
|
|
45
55
|
CONTENT_RATE_LIMIT_WINDOW,
|
|
46
56
|
} from "./routes/content/rate-limit.js";
|
|
57
|
+
import { searchContentRoute } from "./routes/content/search.js";
|
|
47
58
|
import {
|
|
48
59
|
leadCallbackRoute,
|
|
49
60
|
leadSubmitRoute,
|
|
50
61
|
leadSubscribeRoute,
|
|
51
62
|
} from "./routes/leads/index.js";
|
|
63
|
+
import { leadListRoute } from "./routes/leads/list.js";
|
|
64
|
+
import { requestRoutes } from "./routes/requests/index.js";
|
|
52
65
|
import {
|
|
53
66
|
registerTestAuthRoute,
|
|
54
67
|
shouldMountTestAuth,
|
|
55
68
|
} from "./routes/test/auth/session.js";
|
|
56
69
|
|
|
70
|
+
/**
|
|
71
|
+
* What a request looks like in a log line — Fastify's own default shape, minus
|
|
72
|
+
* the QUERY STRING (#5970 security review).
|
|
73
|
+
*
|
|
74
|
+
* Exported so the property can be asserted directly rather than inferred from
|
|
75
|
+
* a captured line, because the thing it protects is easy to regress silently:
|
|
76
|
+
* pino's stock `req` serializer logs `req.raw.url` verbatim, and a route may
|
|
77
|
+
* carry its credential there. `GET /v1/requests/:id/confirm?token=…` does,
|
|
78
|
+
* since a link in an email has nowhere else to put one, and that token is live
|
|
79
|
+
* for seven days and mints a chargeable Stripe artifact.
|
|
80
|
+
*
|
|
81
|
+
* Nothing that identifies a request is lost: a route parameter such as `:id`
|
|
82
|
+
* lives in the PATH and survives. Only the secret after `?` is dropped.
|
|
83
|
+
*
|
|
84
|
+
* #6084 D14 — `GET /v1/checkout/sessions/:id` puts its credential in the
|
|
85
|
+
* PATH, not the query string, so the strip above does not cover it: every
|
|
86
|
+
* request would otherwise write a live `cs_…` Checkout Session id into the
|
|
87
|
+
* log — contradicting D3 and `sessions.ts`'s own stated invariant that only
|
|
88
|
+
* the first 8 characters of a session id ever reach a log line. A path
|
|
89
|
+
* SEGMENT rule (never a substring replace) truncates any `cs_…`-shaped
|
|
90
|
+
* segment to its first 8 characters plus `…`, so it cannot corrupt an
|
|
91
|
+
* unrelated URL.
|
|
92
|
+
*
|
|
93
|
+
* #6216 D11/§ 9 A-3 — the SAME rule now carries a second arm for
|
|
94
|
+
* `lead_…`-shaped segments. Fastify's automatic "incoming request" line is
|
|
95
|
+
* emitted by its internal hook BEFORE any route hook can intervene, so
|
|
96
|
+
* every `GET`/`PATCH .../workspaces/:workspaceId/messages/:leadId` would
|
|
97
|
+
* otherwise write the lead id into the log beside `remoteAddress` —
|
|
98
|
+
* completing exactly the `leadId` ↔ `siteId`/`ip`/`user-agent` correlation
|
|
99
|
+
* `routes/leads/index.ts` and this file's own PII posture refuse. No
|
|
100
|
+
* route-level discipline can fix it; this is the only place that can.
|
|
101
|
+
*/
|
|
102
|
+
const SESSION_ID_LOG_SEGMENT = /^cs_[A-Za-z0-9_]+$/;
|
|
103
|
+
const LEAD_ID_LOG_SEGMENT = idRegex("lead");
|
|
104
|
+
|
|
105
|
+
function redactSensitiveIdSegments(url: string): string {
|
|
106
|
+
return url
|
|
107
|
+
.split("/")
|
|
108
|
+
.map((segment) =>
|
|
109
|
+
SESSION_ID_LOG_SEGMENT.test(segment) || LEAD_ID_LOG_SEGMENT.test(segment)
|
|
110
|
+
? `${segment.slice(0, 8)}…`
|
|
111
|
+
: segment,
|
|
112
|
+
)
|
|
113
|
+
.join("/");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function requestLogSerializer(request: FastifyRequest) {
|
|
117
|
+
const raw = request.url;
|
|
118
|
+
const queryAt = raw.indexOf("?");
|
|
119
|
+
const path = queryAt === -1 ? raw : raw.slice(0, queryAt);
|
|
120
|
+
return {
|
|
121
|
+
method: request.method,
|
|
122
|
+
url: redactSensitiveIdSegments(path),
|
|
123
|
+
host: request.hostname,
|
|
124
|
+
remoteAddress: request.ip,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
57
128
|
/**
|
|
58
129
|
* Paid-module SLICE entry points, keyed by the export each branch needs
|
|
59
130
|
* (#5182).
|
|
@@ -132,11 +203,21 @@ const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
|
|
|
132
203
|
const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
|
|
133
204
|
const WORKSPACES_GENERAL = "./routes/workspaces/general.js";
|
|
134
205
|
const WORKSPACES_MEMBERS = "./routes/workspaces/members.js";
|
|
206
|
+
// #6216 — a SIBLING specifier registered in the SAME workspace/multi-tenant
|
|
207
|
+
// scope as WORKSPACES_GENERAL/WORKSPACES_MEMBERS below, not the
|
|
208
|
+
// multi-tenant-only family WORKSPACES_MEETINGS/WORKSPACES_CONFIG sit in.
|
|
209
|
+
const WORKSPACES_MESSAGES = "./routes/workspaces/messages.js";
|
|
210
|
+
// #6183 S9b — a SEPARATE specifier from WORKSPACES_CONFIG: this route is
|
|
211
|
+
// registered under isModuleEnabled("scheduling") alone, never the
|
|
212
|
+
// workspace/multi-tenant scope config.js shares with general.js/members.js
|
|
213
|
+
// (see config.ts's own header for why).
|
|
214
|
+
const WORKSPACES_MEETINGS = "./routes/workspaces/meetings.js";
|
|
215
|
+
const WORKSPACES_CONTENT = "./routes/workspaces/content.js";
|
|
216
|
+
const ASSETS_UPLOAD = "./routes/assets/upload.js";
|
|
217
|
+
const ASSETS_READ = "./routes/assets/read.js";
|
|
218
|
+
const ASSETS_MANAGE = "./routes/assets/manage.js";
|
|
135
219
|
const BILLING_INDEX = "./routes/billing/index.js";
|
|
136
220
|
const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
|
|
137
|
-
const CONTENT_CREATE = "./routes/content/create.js";
|
|
138
|
-
const CONTENT_UPDATE = "./routes/content/update.js";
|
|
139
|
-
const CONTENT_DELETE = "./routes/content/delete.js";
|
|
140
221
|
const SCHEDULE_INDEX = "./routes/schedule/index.js";
|
|
141
222
|
const FLOWS_INDEX = "./routes/flows/index.js";
|
|
142
223
|
const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
|
|
@@ -146,7 +227,9 @@ const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
|
|
|
146
227
|
const CHECKOUT_INDEX = "./routes/checkout/index.js";
|
|
147
228
|
const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
|
|
148
229
|
const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
|
|
230
|
+
const SUBSCRIPTION_INDEX = "./routes/subscription/index.js";
|
|
149
231
|
const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
|
|
232
|
+
const MAGNETS_INDEX = "./routes/magnets/index.js";
|
|
150
233
|
|
|
151
234
|
/**
|
|
152
235
|
* Paid-module SLICE entry points, keyed by the export each branch needs.
|
|
@@ -181,6 +264,33 @@ const SLICE_PLUGINS = {
|
|
|
181
264
|
specifier: WORKSPACES_MEMBERS,
|
|
182
265
|
load: () => import(WORKSPACES_MEMBERS),
|
|
183
266
|
},
|
|
267
|
+
// #6216 — the Inbox message API, the SAME scope as the two above.
|
|
268
|
+
workspaceMessageRoutes: {
|
|
269
|
+
specifier: WORKSPACES_MESSAGES,
|
|
270
|
+
load: () => import(WORKSPACES_MESSAGES),
|
|
271
|
+
},
|
|
272
|
+
// #6183 S9b — registered in the scheduling scope, not the
|
|
273
|
+
// workspace/multi-tenant scope its siblings above share.
|
|
274
|
+
workspaceMeetingsRoutes: {
|
|
275
|
+
specifier: WORKSPACES_MEETINGS,
|
|
276
|
+
load: () => import(WORKSPACES_MEETINGS),
|
|
277
|
+
},
|
|
278
|
+
workspaceContentRoutes: {
|
|
279
|
+
specifier: WORKSPACES_CONTENT,
|
|
280
|
+
load: () => import(WORKSPACES_CONTENT),
|
|
281
|
+
},
|
|
282
|
+
workspaceAssetUploadRoute: {
|
|
283
|
+
specifier: ASSETS_UPLOAD,
|
|
284
|
+
load: () => import(ASSETS_UPLOAD),
|
|
285
|
+
},
|
|
286
|
+
publicAssetReadRoute: {
|
|
287
|
+
specifier: ASSETS_READ,
|
|
288
|
+
load: () => import(ASSETS_READ),
|
|
289
|
+
},
|
|
290
|
+
workspaceAssetManageRoutes: {
|
|
291
|
+
specifier: ASSETS_MANAGE,
|
|
292
|
+
load: () => import(ASSETS_MANAGE),
|
|
293
|
+
},
|
|
184
294
|
billingRoutes: {
|
|
185
295
|
specifier: BILLING_INDEX,
|
|
186
296
|
load: () => import(BILLING_INDEX),
|
|
@@ -189,18 +299,6 @@ const SLICE_PLUGINS = {
|
|
|
189
299
|
specifier: WAITLIST_EXPERIMENTS_INDEX,
|
|
190
300
|
load: () => import(WAITLIST_EXPERIMENTS_INDEX),
|
|
191
301
|
},
|
|
192
|
-
createPageRoute: {
|
|
193
|
-
specifier: CONTENT_CREATE,
|
|
194
|
-
load: () => import(CONTENT_CREATE),
|
|
195
|
-
},
|
|
196
|
-
updatePageRoute: {
|
|
197
|
-
specifier: CONTENT_UPDATE,
|
|
198
|
-
load: () => import(CONTENT_UPDATE),
|
|
199
|
-
},
|
|
200
|
-
deletePageRoute: {
|
|
201
|
-
specifier: CONTENT_DELETE,
|
|
202
|
-
load: () => import(CONTENT_DELETE),
|
|
203
|
-
},
|
|
204
302
|
listAvailabilityRoute: {
|
|
205
303
|
specifier: SCHEDULE_INDEX,
|
|
206
304
|
load: () => import(SCHEDULE_INDEX),
|
|
@@ -213,6 +311,20 @@ const SLICE_PLUGINS = {
|
|
|
213
311
|
specifier: SCHEDULE_INDEX,
|
|
214
312
|
load: () => import(SCHEDULE_INDEX),
|
|
215
313
|
},
|
|
314
|
+
// #6183 S9b D2/D12 — the enabled-meeting-slugs public read; same
|
|
315
|
+
// specifier as the three anonymous schedule routes above (§ D6's
|
|
316
|
+
// precedent, no new SLICE_PLUGINS specifier constant needed).
|
|
317
|
+
meetingTypesRoute: {
|
|
318
|
+
specifier: SCHEDULE_INDEX,
|
|
319
|
+
load: () => import(SCHEDULE_INDEX),
|
|
320
|
+
},
|
|
321
|
+
// #6085 — the tenant-guarded appointments read; same specifier as the
|
|
322
|
+
// three anonymous schedule routes above (§ D6, no new SLICE_PLUGINS
|
|
323
|
+
// specifier constant needed).
|
|
324
|
+
listAppointmentsRoute: {
|
|
325
|
+
specifier: SCHEDULE_INDEX,
|
|
326
|
+
load: () => import(SCHEDULE_INDEX),
|
|
327
|
+
},
|
|
216
328
|
getFlowRoute: {
|
|
217
329
|
specifier: FLOWS_INDEX,
|
|
218
330
|
load: () => import(FLOWS_INDEX),
|
|
@@ -225,6 +337,12 @@ const SLICE_PLUGINS = {
|
|
|
225
337
|
specifier: FLOWS_INDEX,
|
|
226
338
|
load: () => import(FLOWS_INDEX),
|
|
227
339
|
},
|
|
340
|
+
// #6157 — the tenant-scoped magnet Admin authoring surface. ONE plugin,
|
|
341
|
+
// ONE specifier (D2): all four routes register through this one entry.
|
|
342
|
+
magnetRoutes: {
|
|
343
|
+
specifier: MAGNETS_INDEX,
|
|
344
|
+
load: () => import(MAGNETS_INDEX),
|
|
345
|
+
},
|
|
228
346
|
deliverableDownloadRoute: {
|
|
229
347
|
specifier: DELIVERABLES_INDEX,
|
|
230
348
|
load: () => import(DELIVERABLES_INDEX),
|
|
@@ -263,9 +381,13 @@ const SLICE_PLUGINS = {
|
|
|
263
381
|
specifier: WAITLIST_SIGNUPS_INDEX,
|
|
264
382
|
load: () => import(WAITLIST_SIGNUPS_INDEX),
|
|
265
383
|
},
|
|
266
|
-
// Commerce (D-47, #5364)
|
|
267
|
-
//
|
|
268
|
-
//
|
|
384
|
+
// Commerce (D-47, #5364), re-cut by D-64 (#5796). `checkoutRoutes` and
|
|
385
|
+
// `webhooksRoutes` are the base-tree PRIMITIVES now: they register
|
|
386
|
+
// UNCONDITIONALLY and gate at call time on the `commerce` license claim
|
|
387
|
+
// (lib/require-commerce-claim.ts), so they are no longer slice-gated even
|
|
388
|
+
// though they still load through this table. `flowCheckoutRoutes` and the
|
|
389
|
+
// two recommender entries below stay module-conditional (isModuleEnabled
|
|
390
|
+
// "commerce") — they are commerce OVERLAY, not primitives.
|
|
269
391
|
checkoutRoutes: {
|
|
270
392
|
specifier: CHECKOUT_INDEX,
|
|
271
393
|
load: () => import(CHECKOUT_INDEX),
|
|
@@ -278,6 +400,13 @@ const SLICE_PLUGINS = {
|
|
|
278
400
|
specifier: FLOW_CHECKOUTS_INDEX,
|
|
279
401
|
load: () => import(FLOW_CHECKOUTS_INDEX),
|
|
280
402
|
},
|
|
403
|
+
// #6038 — the buyer-scoped subscription-status route pair (the
|
|
404
|
+
// update-your-card banner's read + portal handoff). Commerce OVERLAY,
|
|
405
|
+
// same posture as flowCheckoutRoutes immediately above.
|
|
406
|
+
subscriptionRoutes: {
|
|
407
|
+
specifier: SUBSCRIPTION_INDEX,
|
|
408
|
+
load: () => import(SUBSCRIPTION_INDEX),
|
|
409
|
+
},
|
|
281
410
|
saveRecommendationRoute: {
|
|
282
411
|
specifier: RECOMMENDER_INDEX,
|
|
283
412
|
load: () => import(RECOMMENDER_INDEX),
|
|
@@ -334,6 +463,27 @@ async function tenantPreHandlerSlicePlugin(): Promise<FastifyPluginCallback> {
|
|
|
334
463
|
return plugin;
|
|
335
464
|
}
|
|
336
465
|
|
|
466
|
+
/**
|
|
467
|
+
* The tenant guard THIS build runs (#5677). Exactly one of the two, never
|
|
468
|
+
* both: the multi-tenant module's `resolveActiveTenant` guard when the module
|
|
469
|
+
* is enabled, and the base-build default-workspace guard
|
|
470
|
+
* (`middleware/default-tenant.ts`) otherwise.
|
|
471
|
+
*
|
|
472
|
+
* Precedence, not parallelism. On a build WITH the module this returns the
|
|
473
|
+
* identical plugin every tenant scope registered before, so multi-tenant
|
|
474
|
+
* resolution and behavior are unchanged. On a build without it, the base guard
|
|
475
|
+
* resolves the seeded default Workspace (`database/tenancy.ts`) under the same
|
|
476
|
+
* default-deny posture — which is what makes the already-built review
|
|
477
|
+
* dashboard reachable at Pro instead of leaving its routes unregistered.
|
|
478
|
+
*
|
|
479
|
+
* The base guard is a STATIC import, not a slice load: it always ships.
|
|
480
|
+
*/
|
|
481
|
+
async function tenantPreHandlerForBuild(): Promise<FastifyPluginCallback> {
|
|
482
|
+
return isModuleEnabled("multi-tenant")
|
|
483
|
+
? await tenantPreHandlerSlicePlugin()
|
|
484
|
+
: defaultTenantPreHandlerPlugin;
|
|
485
|
+
}
|
|
486
|
+
|
|
337
487
|
/**
|
|
338
488
|
* The commerce boot-time sandbox guard — the one other slice entry that is
|
|
339
489
|
* not a route plugin, mirroring `tenantPreHandlerSlicePlugin()` above (D3).
|
|
@@ -363,12 +513,22 @@ export async function build() {
|
|
|
363
513
|
// Startup-time validation: fail fast if LEAD_CAPTURE_FILE points at a missing directory.
|
|
364
514
|
await validateLeadCaptureDir();
|
|
365
515
|
|
|
516
|
+
// #6155 D6 — the site-alias workspace assertion (ownership brief § Layer
|
|
517
|
+
// 2, AC-5): resolves + asserts once at startup that AILK_SITE_ID (or its
|
|
518
|
+
// "default" fallback) names a real workspace, throwing when it does not —
|
|
519
|
+
// the same catch-log-exit(1) path index.ts already runs on this throws
|
|
520
|
+
// loud rather than serving empty lists. Carve-out: with no DATABASE_URL
|
|
521
|
+
// configured, it logs a warning and returns instead of throwing (a
|
|
522
|
+
// database-less dev boot).
|
|
523
|
+
await assertSiteAliasResolves();
|
|
524
|
+
|
|
366
525
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
367
526
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
368
|
-
// assertSandboxKey is called again inside a route handler.
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
527
|
+
// assertSandboxKey is called again inside a route handler. UNCONDITIONAL
|
|
528
|
+
// since D-64 (#5796): the commerce primitives and lib/stripe.ts are base
|
|
529
|
+
// tree now, so the key's presence — not a module flag — is the whole
|
|
530
|
+
// question. A build with no STRIPE_SECRET_KEY has nothing to assert.
|
|
531
|
+
if (config.STRIPE_SECRET_KEY) {
|
|
372
532
|
await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
|
|
373
533
|
}
|
|
374
534
|
|
|
@@ -395,7 +555,27 @@ export async function build() {
|
|
|
395
555
|
// (server-action-forwarded traffic), which was previously broken for
|
|
396
556
|
// every caller.
|
|
397
557
|
const server = Fastify({
|
|
398
|
-
logger:
|
|
558
|
+
logger: {
|
|
559
|
+
// The request serializer drops the QUERY STRING (#5970 security review,
|
|
560
|
+
// finding 1). Fastify's default `req` serializer logs `req.raw.url`
|
|
561
|
+
// verbatim, so any route whose credential rides in the query string
|
|
562
|
+
// writes that credential into the API's own log on every single request
|
|
563
|
+
// — including the automatic "incoming request" line, which is emitted by
|
|
564
|
+
// Fastify's internal hook before any route-level hook can intervene.
|
|
565
|
+
//
|
|
566
|
+
// `GET /v1/requests/:id/confirm?token=…` is exactly such a route: the
|
|
567
|
+
// token IS the authorization, it is live for seven days, and holding it
|
|
568
|
+
// is enough to read a requester's name and phone and to mint a Payment
|
|
569
|
+
// Link on the site's own Stripe account. `Referrer-Policy: no-referrer`
|
|
570
|
+
// on that route exists to keep the token out of a THIRD PARTY's logs;
|
|
571
|
+
// this keeps it out of ours, which is the larger hole of the two.
|
|
572
|
+
//
|
|
573
|
+
// The shape is otherwise Fastify's own default, minus the query: a log
|
|
574
|
+
// consumer reading `method` + `url` + `remoteAddress` sees what it
|
|
575
|
+
// always saw. Nothing that identifies a REQUEST is lost — `:id` is in
|
|
576
|
+
// the path and survives; only the secret after `?` is dropped.
|
|
577
|
+
serializers: { req: requestLogSerializer },
|
|
578
|
+
},
|
|
399
579
|
trustProxy: true,
|
|
400
580
|
});
|
|
401
581
|
|
|
@@ -527,6 +707,18 @@ export async function build() {
|
|
|
527
707
|
);
|
|
528
708
|
}
|
|
529
709
|
|
|
710
|
+
// The leads INBOX read (#5678) — auth only, NOT tenant-guarded, for the
|
|
711
|
+
// same reason as the three reads below: the tenant scope is registered
|
|
712
|
+
// only when the multi-tenant module is present, and lead capture is a
|
|
713
|
+
// never-gate surface at EVERY level, so reading a lead back cannot be
|
|
714
|
+
// gated on a module either. Registered unconditionally — it is the one
|
|
715
|
+
// route in this scope with no `isModuleEnabled()` gate. Identity is
|
|
716
|
+
// `request.userId`; the scope is resolved from the caller's own
|
|
717
|
+
// memberships by `services/leads-inbox.ts` through
|
|
718
|
+
// `services/owner-workspaces.ts`'s `resolveOwnerWorkspaces`, never from a
|
|
719
|
+
// request parameter.
|
|
720
|
+
await authScope.register(leadListRoute);
|
|
721
|
+
|
|
530
722
|
// Respondent signup read (#4731) — auth only, NOT tenant-guarded: a
|
|
531
723
|
// respondent has no workspace membership and the tenant guard is
|
|
532
724
|
// default-deny. Identity is request.userId; the service re-checks
|
|
@@ -546,6 +738,17 @@ export async function build() {
|
|
|
546
738
|
await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
|
|
547
739
|
}
|
|
548
740
|
|
|
741
|
+
// The update-your-card banner's read + portal handoff (#6038) — the
|
|
742
|
+
// same posture as flow-checkouts immediately above: auth only, NOT
|
|
743
|
+
// tenant-guarded (a subscriber has no workspace membership). Identity is
|
|
744
|
+
// request.userId; GET is cookie-or-bearer, POST refuses bearer callers
|
|
745
|
+
// (D3.1). Module-conditional (commerce, D8): the account page's
|
|
746
|
+
// subscription-status read already degrades to "no banner" with no code
|
|
747
|
+
// change when this is absent (SKIPPED_STATUSES covers 404 — spec §4.2).
|
|
748
|
+
if (isModuleEnabled("commerce")) {
|
|
749
|
+
await authScope.register(await sliceRoutePlugin("subscriptionRoutes"));
|
|
750
|
+
}
|
|
751
|
+
|
|
549
752
|
// The founder's OWN listings read (#5295) — the same posture again: auth
|
|
550
753
|
// only, NOT tenant-guarded, because a founder is not a workspace member
|
|
551
754
|
// and the tenant guard is default-deny. Identity is request.userId; the
|
|
@@ -580,6 +783,35 @@ export async function build() {
|
|
|
580
783
|
await tenantScope.register(await sliceRoutePlugin("sessionRoutes"));
|
|
581
784
|
// Org-billing routes (#3701) — org-scoped read + Stripe customer-portal.
|
|
582
785
|
await tenantScope.register(await sliceRoutePlugin("billingRoutes"));
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// REVIEW-SURFACE routes — a sibling tenant scope, gated on EITHER module
|
|
790
|
+
// (#5677). The waitlist operator dashboard and the project-listings
|
|
791
|
+
// dashboard were registered inside the multi-tenant scope above, which was
|
|
792
|
+
// too narrow: `signal-collectors` (Pro) is the module that SELLS the
|
|
793
|
+
// instruments these routes read back — flows, deliverables, project
|
|
794
|
+
// listings, waitlist experiments — so a Pro customer could collect
|
|
795
|
+
// submissions and not read them. That was an accident of registration
|
|
796
|
+
// scope, not a pricing decision (#5677). Widening the gate is the
|
|
797
|
+
// correction.
|
|
798
|
+
//
|
|
799
|
+
// This is a REGISTRATION change, not a tenant-isolation change. The scope
|
|
800
|
+
// registers `tenantPreHandlerForBuild()`, which on a multi-tenant build is
|
|
801
|
+
// byte-for-byte the guard these two route plugins already ran behind, and
|
|
802
|
+
// on a build without the module is the base-build guard with the same
|
|
803
|
+
// default-deny posture over the seeded default Workspace. Each plugin
|
|
804
|
+
// registers exactly once — they live here and nowhere else, so the scopes
|
|
805
|
+
// cannot collide on a path — and every OTHER route in the scope above
|
|
806
|
+
// (workspaceSessionRoutes, sessionRoutes, billingRoutes) stays
|
|
807
|
+
// multi-tenant-only: those are Organization/Workspace administration, not
|
|
808
|
+
// a site's own submissions.
|
|
809
|
+
if (
|
|
810
|
+
isModuleEnabled("multi-tenant") ||
|
|
811
|
+
isModuleEnabled("signal-collectors")
|
|
812
|
+
) {
|
|
813
|
+
await authScope.register(async (tenantScope) => {
|
|
814
|
+
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
583
815
|
// Waitlist operator-dashboard read API (#4665 D7, B0) — tenant-scoped
|
|
584
816
|
// list/comparison/signups reads over A1's waitlist-experiment rows.
|
|
585
817
|
await tenantScope.register(
|
|
@@ -593,6 +825,51 @@ export async function build() {
|
|
|
593
825
|
});
|
|
594
826
|
}
|
|
595
827
|
|
|
828
|
+
// The magnet Admin authoring surface (#6157 S5c) — its OWN sibling
|
|
829
|
+
// tenant scope (D3), not a line inside the review-surface scope above:
|
|
830
|
+
// its module-exclusion row stays its own, and the scope can be read,
|
|
831
|
+
// moved or gated without disturbing `waitlistExperimentRoutes` /
|
|
832
|
+
// `projectListingTenantRoutes`. Gated `multi-tenant || signal-collectors`
|
|
833
|
+
// (D4), VERBATIM the review-surface expression above — not narrowed to
|
|
834
|
+
// `(multi-tenant || signal-collectors) && <tenancy module>`, which would
|
|
835
|
+
// put the magnet surface out of reach of `marketing-site` (the Pro
|
|
836
|
+
// flagship template, which takes `signal-collectors` WITHOUT
|
|
837
|
+
// `multi-tenant`). The delivery is brought up to the gate instead
|
|
838
|
+
// (`packages/create-ailk/src/module-architecture.ts` D14/D15/D16), the
|
|
839
|
+
// same correction #5677 already made once for this exact scope.
|
|
840
|
+
if (
|
|
841
|
+
isModuleEnabled("multi-tenant") ||
|
|
842
|
+
isModuleEnabled("signal-collectors")
|
|
843
|
+
) {
|
|
844
|
+
await authScope.register(async (tenantScope) => {
|
|
845
|
+
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
846
|
+
await tenantScope.register(await sliceRoutePlugin("magnetRoutes"));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// The appointments read (#6085, brief H7) — a sibling tenant scope,
|
|
851
|
+
// gated on `scheduling` alone (§ D6). NOT the anonymous schedule scope
|
|
852
|
+
// below (`isModuleEnabled("scheduling")`'s OTHER site, § 1.9/§ D6): a
|
|
853
|
+
// prospect booking a call has no account, so that scope carries no
|
|
854
|
+
// auth preHandler, and mounting this read there would publish every
|
|
855
|
+
// attendee's name and email to the internet. `tenantPreHandlerForBuild()`
|
|
856
|
+
// is the SAME default-deny guard the review-surface scope above uses —
|
|
857
|
+
// on a build with no `multi-tenant` module it fails closed unless
|
|
858
|
+
// `AILK_OPERATOR_EMAIL` is pinned (`middleware/default-tenant.ts`).
|
|
859
|
+
//
|
|
860
|
+
// The meetings config route (#6183 S9b) shares this exact scope and
|
|
861
|
+
// gate, for the same reason: its GET reaches
|
|
862
|
+
// apps/api/src/services/scheduling/ (D24's listEventTypes join), which
|
|
863
|
+
// is undeliverable to a scaffold without this module — see
|
|
864
|
+
// routes/workspaces/meetings.ts's own header.
|
|
865
|
+
if (isModuleEnabled("scheduling")) {
|
|
866
|
+
await authScope.register(async (tenantScope) => {
|
|
867
|
+
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
868
|
+
await tenantScope.register(await sliceRoutePlugin("listAppointmentsRoute"));
|
|
869
|
+
await tenantScope.register(await sliceRoutePlugin("workspaceMeetingsRoutes"));
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
596
873
|
// Workspace SETTINGS routes — a sibling tenant scope, gated on EITHER
|
|
597
874
|
// module (#5371 D2). The site-config routes were registered inside the
|
|
598
875
|
// multi-tenant scope above, which was too narrow: nothing in their logic
|
|
@@ -603,18 +880,30 @@ export async function build() {
|
|
|
603
880
|
// original mis-scoping twice.
|
|
604
881
|
//
|
|
605
882
|
// This is a REGISTRATION change, not a tenant-isolation change: the scope
|
|
606
|
-
// registers its own `
|
|
607
|
-
// guard the
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
883
|
+
// registers its own `tenantPreHandlerForBuild()`, the same default-deny
|
|
884
|
+
// guard the scopes above use. The remaining routes in the multi-tenant
|
|
885
|
+
// scope above (workspaceSessionRoutes, sessionRoutes, billingRoutes) stay
|
|
886
|
+
// multi-tenant-only.
|
|
887
|
+
//
|
|
888
|
+
// #5677's selector fixes ONE of this scope's two stripped-scaffold
|
|
889
|
+
// problems and not the other, so do not read it as making a
|
|
890
|
+
// `--modules workspace` scaffold work. It removes the guard's dependency
|
|
891
|
+
// on `./middleware/tenant.js` (a multi-tenant slice file, which threw
|
|
892
|
+
// ERR_MODULE_NOT_FOUND during construction). The three route plugins
|
|
893
|
+
// below are still reached through slice loads, and
|
|
894
|
+
// `routes/workspaces/config.ts` remains a multi-tenant `ownedFiles` entry
|
|
895
|
+
// — `general.ts` and `members.ts` are workspace-owned, `config.ts` is not
|
|
896
|
+
// — so that scaffold still has no `routes/workspaces/` directory and
|
|
897
|
+
// still throws here. Fixing it is the same one-line class of change #5677
|
|
898
|
+
// made for the review surface: co-own `config.ts` under `workspace` in
|
|
899
|
+
// MODULE_SLICES. Out of scope for #5677, which owns the review surface.
|
|
611
900
|
//
|
|
612
901
|
// A build with BOTH modules enabled registers each of the three route
|
|
613
902
|
// plugins exactly once: they live here and nowhere else, so the two
|
|
614
903
|
// scopes cannot collide on a path.
|
|
615
904
|
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
616
905
|
await authScope.register(async (tenantScope) => {
|
|
617
|
-
await tenantScope.register(await
|
|
906
|
+
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
618
907
|
await tenantScope.register(
|
|
619
908
|
await sliceRoutePlugin("workspaceConfigRoutes"),
|
|
620
909
|
);
|
|
@@ -626,28 +915,157 @@ export async function build() {
|
|
|
626
915
|
await tenantScope.register(
|
|
627
916
|
await sliceRoutePlugin("workspaceMembersRoutes"),
|
|
628
917
|
);
|
|
918
|
+
// The Inbox message API (#6216 S7e) — the SAME scope, the SAME
|
|
919
|
+
// default-deny guard: list, item read, and the handled stamp.
|
|
920
|
+
await tenantScope.register(
|
|
921
|
+
await sliceRoutePlugin("workspaceMessageRoutes"),
|
|
922
|
+
);
|
|
923
|
+
// The asset store's MANAGE routes — list, PATCH, DELETE (#6093 D16).
|
|
924
|
+
// Registered HERE, in the plain tenant scope, and NOT inside the
|
|
925
|
+
// upload's own nested strict-rate-limit scope below: a JSON
|
|
926
|
+
// list/patch/delete has no 20 MB body to bound, and inheriting that
|
|
927
|
+
// per-IP ceiling would throttle the Admin's own list against its
|
|
928
|
+
// siblings.
|
|
929
|
+
await tenantScope.register(
|
|
930
|
+
await sliceRoutePlugin("workspaceAssetManageRoutes"),
|
|
931
|
+
);
|
|
932
|
+
// The workspace asset store's upload half (#5854). It belongs in THIS
|
|
933
|
+
// scope and not beside the public routes below, because the two
|
|
934
|
+
// refusals its AC names — no granted tenant, and a `:workspaceId` that
|
|
935
|
+
// is not the session's — are `tenantPreHandlerForBuild()`'s to make,
|
|
936
|
+
// before any handler runs and so before any write. Its raw-body parser
|
|
937
|
+
// and its `onRequest` size/type hook are declared INSIDE that plugin,
|
|
938
|
+
// so they encapsulate to it and reach neither the public read below nor
|
|
939
|
+
// any sibling here.
|
|
940
|
+
//
|
|
941
|
+
// Its own rate-limit scope, at the strict per-IP ceiling the leads and
|
|
942
|
+
// scheduling scopes use (#5854 security review round 1, MEDIUM). Auth
|
|
943
|
+
// and tenant are `preHandler`s, so an UNAUTHENTICATED caller still
|
|
944
|
+
// reaches the parser and can make the server buffer up to 20 MB before
|
|
945
|
+
// the 401. `rateLimitMiddleware` is an `onRequest` hook, so scoping it
|
|
946
|
+
// here is what bounds how often that can be done — the cap bounds one
|
|
947
|
+
// body, this bounds the rate. Nested so the ceiling applies to the
|
|
948
|
+
// upload alone and not to the config/general/members siblings above.
|
|
949
|
+
await tenantScope.register(async (assetUploadScope) => {
|
|
950
|
+
assetUploadScope.register(rateLimitMiddleware, {
|
|
951
|
+
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
952
|
+
});
|
|
953
|
+
await assetUploadScope.register(
|
|
954
|
+
await sliceRoutePlugin("workspaceAssetUploadRoute"),
|
|
955
|
+
);
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// The editor's tenant-guarded content-write surface (#5853, roadmap D4).
|
|
961
|
+
// Its OWN sibling scope (D9) — not nested inside the workspace/multi-tenant
|
|
962
|
+
// scope above, so its module-exclusion row stays its own. The gate is
|
|
963
|
+
// `content-write` AND a tenancy module: the route file imports
|
|
964
|
+
// `lib/tenant-db.ts`, which only the `workspace` / `multi-tenant` slices
|
|
965
|
+
// deliver, so a scaffold enabling `content-write` without either would
|
|
966
|
+
// throw ERR_MODULE_NOT_FOUND at build() — the exact defect
|
|
967
|
+
// `tools/lint-slice-registration.ts` (required in CI) refuses. D9's
|
|
968
|
+
// "content-write alone" premise was falsified by that lint on PR #5921;
|
|
969
|
+
// the path itself is workspace-scoped (`/v1/workspaces/:workspaceId/…`),
|
|
970
|
+
// so requiring a tenancy module is coherent, not a narrowing.
|
|
971
|
+
// `tenantPreHandlerForBuild()` still selects the base-build
|
|
972
|
+
// default-workspace guard on a `workspace`-only build. The plugin is
|
|
973
|
+
// registered here and nowhere else, so no scope can collide on a path.
|
|
974
|
+
//
|
|
975
|
+
// The module flags are read ONCE, here, at build time (D10): on a build
|
|
976
|
+
// that excludes them the routes are never registered and Fastify's own 404
|
|
977
|
+
// answers — no auth, no tenant resolution, no handler-level
|
|
978
|
+
// `isModuleEnabled`.
|
|
979
|
+
if (
|
|
980
|
+
isModuleEnabled("content-write") &&
|
|
981
|
+
(isModuleEnabled("workspace") || isModuleEnabled("multi-tenant"))
|
|
982
|
+
) {
|
|
983
|
+
await authScope.register(async (tenantScope) => {
|
|
984
|
+
// D12: the authenticated ceiling (600/min), scope-local. The global
|
|
985
|
+
// limiter above is the unauth 60/min, which an autosaving editor would
|
|
986
|
+
// trip against its own user.
|
|
987
|
+
await tenantScope.register(rateLimit, {
|
|
988
|
+
max: CONTENT_RATE_LIMIT_AUTH,
|
|
989
|
+
timeWindow: CONTENT_RATE_LIMIT_WINDOW,
|
|
990
|
+
});
|
|
991
|
+
await tenantScope.register(await tenantPreHandlerForBuild());
|
|
992
|
+
await tenantScope.register(
|
|
993
|
+
await sliceRoutePlugin("workspaceContentRoutes"),
|
|
994
|
+
);
|
|
629
995
|
});
|
|
630
996
|
}
|
|
631
997
|
});
|
|
632
998
|
|
|
633
999
|
// Public routes — no auth required.
|
|
1000
|
+
//
|
|
1001
|
+
// The workspace asset store's READ half (#5854), under the SAME predicate as
|
|
1002
|
+
// its upload above rather than unconditionally: with no upload path in the
|
|
1003
|
+
// build it would be a permanent 404 advertised on both self-description
|
|
1004
|
+
// surfaces. It cannot join the block above — that block is nested in
|
|
1005
|
+
// `authScope`, and an `<img src>` carries no session.
|
|
1006
|
+
if (isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")) {
|
|
1007
|
+
await server.register(await sliceRoutePlugin("publicAssetReadRoute"));
|
|
1008
|
+
}
|
|
634
1009
|
await server.register(listTypesRoute);
|
|
1010
|
+
// Before `listPagesRoute`: `search` must never be read as a `:type`.
|
|
1011
|
+
await server.register(searchContentRoute);
|
|
635
1012
|
await server.register(listPagesRoute);
|
|
636
1013
|
await server.register(getPageRoute);
|
|
637
|
-
//
|
|
638
|
-
// CALL time
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1014
|
+
// Checkout launcher + reader — commerce PRIMITIVES (D-64, #5796; the GET
|
|
1015
|
+
// added by #6084). Registered UNCONDITIONALLY and gated at CALL time on
|
|
1016
|
+
// the JWT `commerce` claim, the same posture content-write has
|
|
1017
|
+
// (requirePaidTier above), so adding commerce to a live site is a licence
|
|
1018
|
+
// change and lands no files on its repo. Without the claim the gate
|
|
1019
|
+
// answers the module-disabled 404 — byte-identical to the build that
|
|
1020
|
+
// never registered the route. `checkoutRoutes` (`routes/checkout/index.ts`)
|
|
1021
|
+
// registers BOTH `POST /v1/checkout/sessions` and
|
|
1022
|
+
// `GET /v1/checkout/sessions/:id` — the GET in its own nested,
|
|
1023
|
+
// rate-limited sub-scope (D16), so this preHandler applies to both while
|
|
1024
|
+
// the GET's own limiter applies to it alone.
|
|
1025
|
+
await server.register(async (checkoutScope) => {
|
|
1026
|
+
checkoutScope.addHook("preHandler", requireCommerceClaim);
|
|
1027
|
+
await checkoutScope.register(await sliceRoutePlugin("checkoutRoutes"));
|
|
1028
|
+
});
|
|
1029
|
+
// Request-and-confirm (#5970, D-67 / SUB-100) — the tenant's confirm form
|
|
1030
|
+
// and the Payment Link it mints. A money path, so it sits INSIDE the
|
|
1031
|
+
// commerce claim (DECISION-LOG D-63; D-67 assigns request-and-confirm to
|
|
1032
|
+
// the Commerce rung rather than exempting it), and the gate is applied by
|
|
1033
|
+
// COMPOSITION here — the checkout scope above, line for line. That is what
|
|
1034
|
+
// lets the route files themselves read no licence claim at all (#5970 AC-4).
|
|
1035
|
+
//
|
|
1036
|
+
// Rate-limited at the leads scope's own strict value: the confirm link is
|
|
1037
|
+
// unauthenticated (the token IS the credential), so it belongs on the same
|
|
1038
|
+
// footing as the anonymous surface that mails it. Note the same caveat
|
|
1039
|
+
// `server.ts` records elsewhere — `trustProxy` is on, so an IP limiter is
|
|
1040
|
+
// evadable; the bound that actually holds is per ROW, one live credential
|
|
1041
|
+
// per request, spent on use.
|
|
1042
|
+
await server.register(async (requestsScope) => {
|
|
1043
|
+
requestsScope.register(rateLimitMiddleware, {
|
|
1044
|
+
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1045
|
+
});
|
|
1046
|
+
// `requireCommerceClaim` short-circuits with `reply.send`, so a wrapper is
|
|
1047
|
+
// the only place the unclaimed case can be OBSERVED: the preHandler ends
|
|
1048
|
+
// the request before any handler runs, and the gate itself logs only on
|
|
1049
|
+
// `undetermined` (`lib/require-commerce-claim.ts`).
|
|
1050
|
+
//
|
|
1051
|
+
// Spec D3 names this line as one of two bounds on the residual it
|
|
1052
|
+
// discloses — a site configured for request-and-confirm but carrying no
|
|
1053
|
+
// `commerce` claim emails a tenant a confirm link that answers 404. The
|
|
1054
|
+
// spec puts the log in the route, where it cannot be emitted; this is the
|
|
1055
|
+
// registration, which is where it can (#5970 acceptance review, advisory
|
|
1056
|
+
// 4). Without it the misconfiguration is visible only in the tenant's
|
|
1057
|
+
// inbox, which is the one place the operator is not looking.
|
|
1058
|
+
requestsScope.addHook("preHandler", async (request, reply) => {
|
|
1059
|
+
await requireCommerceClaim(request, reply);
|
|
1060
|
+
if (reply.sent) {
|
|
1061
|
+
request.log.warn(
|
|
1062
|
+
{ outcome: "confirm_link_unclaimed", path: request.routeOptions.url },
|
|
1063
|
+
"request-confirm: a confirm link was opened on a site without the commerce claim",
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
await requestsScope.register(requestRoutes);
|
|
1068
|
+
});
|
|
651
1069
|
// Deliverable download — a capability-token URL (#3961 D5); the token
|
|
652
1070
|
// itself is the sole gate, so no auth preHandler applies. Module-conditional
|
|
653
1071
|
// (signal-collectors, D-48): gated PDFs and reports.
|
|
@@ -675,12 +1093,15 @@ export async function build() {
|
|
|
675
1093
|
done(null, body);
|
|
676
1094
|
},
|
|
677
1095
|
);
|
|
678
|
-
// Stripe webhook receiver —
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1096
|
+
// Stripe webhook receiver + customer mapping — commerce PRIMITIVES
|
|
1097
|
+
// (D-64, #5796), registered UNCONDITIONALLY alongside checkout above.
|
|
1098
|
+
// NO `requireCommerceClaim` preHandler here, deliberately: D3 puts
|
|
1099
|
+
// signature verification BEFORE the claim check, so the handler itself
|
|
1100
|
+
// consults `hasCommerceClaim()` once the signature is settled — a signed
|
|
1101
|
+
// event on an unclaimed site is acked 200 and dropped rather than
|
|
1102
|
+
// retried forever, while an unsigned body gets the module-disabled 404.
|
|
1103
|
+
// See routes/webhooks/README.md § Commerce claim.
|
|
1104
|
+
await webhooksScope.register(await sliceRoutePlugin("webhooksRoutes"));
|
|
684
1105
|
// Project-listing operator decision (Slack Approve/Reject) — the "hand an
|
|
685
1106
|
// operator a decision" half of D-48's grouping sentence. Module-conditional
|
|
686
1107
|
// (signal-collectors).
|
|
@@ -689,6 +1110,35 @@ export async function build() {
|
|
|
689
1110
|
}
|
|
690
1111
|
});
|
|
691
1112
|
|
|
1113
|
+
// Auth transactional-email relay (#5962 § D3) — its OWN scope, deliberately
|
|
1114
|
+
// OUTSIDE `authScope`: the caller is `apps/web`'s Better Auth callbacks
|
|
1115
|
+
// acting for an anonymous visitor who is by definition not signed in, so an
|
|
1116
|
+
// auth preHandler in front of it would reject every legitimate call. The
|
|
1117
|
+
// gate is the HMAC signature over the raw body plus the 300 s replay window,
|
|
1118
|
+
// both verified inside the handler.
|
|
1119
|
+
//
|
|
1120
|
+
// Raw-body parser scoped to this sub-router only, for the same reason
|
|
1121
|
+
// `webhooksScope` carries one: the signature must be checked over the EXACT
|
|
1122
|
+
// bytes the caller signed, never a re-serialized parse.
|
|
1123
|
+
//
|
|
1124
|
+
// `DEFAULT_PER_IP_PER_MINUTE_STRICT` (10/min) — the constant `leadsScope`
|
|
1125
|
+
// below already uses for its anonymous surface. It is defence in depth, not
|
|
1126
|
+
// the control: an unsigned body never reaches a send, and per-victim volume
|
|
1127
|
+
// is bounded by `lib/auth-email-budget.ts`.
|
|
1128
|
+
await server.register(async (authEmailScope) => {
|
|
1129
|
+
authEmailScope.register(rateLimitMiddleware, {
|
|
1130
|
+
perIpPerMinute: DEFAULT_PER_IP_PER_MINUTE_STRICT,
|
|
1131
|
+
});
|
|
1132
|
+
authEmailScope.addContentTypeParser(
|
|
1133
|
+
"application/json",
|
|
1134
|
+
{ parseAs: "buffer" },
|
|
1135
|
+
(_req, body, done) => {
|
|
1136
|
+
done(null, body);
|
|
1137
|
+
},
|
|
1138
|
+
);
|
|
1139
|
+
await authEmailScope.register(transactionalEmailRoute);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
692
1142
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
693
1143
|
// default (10/min, pre-#4730) rather than inheriting the wider
|
|
694
1144
|
// flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
|
|
@@ -726,11 +1176,15 @@ export async function build() {
|
|
|
726
1176
|
// every recipient — the residual the first one left, and the one that
|
|
727
1177
|
// decides whether a flood can cost the site its sending domain.
|
|
728
1178
|
if (isModuleEnabled("signal-collectors")) {
|
|
729
|
-
await leadsScope.register(
|
|
1179
|
+
await leadsScope.register(
|
|
1180
|
+
await sliceRoutePlugin("projectListingStartRoute"),
|
|
1181
|
+
);
|
|
730
1182
|
await leadsScope.register(
|
|
731
1183
|
await sliceRoutePlugin("projectListingDraftPatchRoute"),
|
|
732
1184
|
);
|
|
733
|
-
await leadsScope.register(
|
|
1185
|
+
await leadsScope.register(
|
|
1186
|
+
await sliceRoutePlugin("projectListingSubmitRoute"),
|
|
1187
|
+
);
|
|
734
1188
|
// #5407 — the resume link's verification write. Anonymous, and gated
|
|
735
1189
|
// on the resume token AND the nonce that only the resume email
|
|
736
1190
|
// carries, so it belongs in the same rate-limited scope as the other
|
|
@@ -738,7 +1192,9 @@ export async function build() {
|
|
|
738
1192
|
await leadsScope.register(
|
|
739
1193
|
await sliceRoutePlugin("projectListingVerifyEmailRoute"),
|
|
740
1194
|
);
|
|
741
|
-
await leadsScope.register(
|
|
1195
|
+
await leadsScope.register(
|
|
1196
|
+
await sliceRoutePlugin("projectListingPublicRoute"),
|
|
1197
|
+
);
|
|
742
1198
|
}
|
|
743
1199
|
});
|
|
744
1200
|
|
|
@@ -756,13 +1212,17 @@ export async function build() {
|
|
|
756
1212
|
if (isModuleEnabled("signal-collectors")) {
|
|
757
1213
|
await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
|
|
758
1214
|
await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
|
|
759
|
-
await flowsScope.register(
|
|
1215
|
+
await flowsScope.register(
|
|
1216
|
+
await sliceRoutePlugin("getFlowAggregateRoute"),
|
|
1217
|
+
);
|
|
760
1218
|
}
|
|
761
1219
|
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
762
1220
|
// rate-limited posture as the three above. Module-conditional (commerce,
|
|
763
1221
|
// D-47): the recommender leaves Pro flows and moves to commerce whole.
|
|
764
1222
|
if (isModuleEnabled("commerce")) {
|
|
765
|
-
await flowsScope.register(
|
|
1223
|
+
await flowsScope.register(
|
|
1224
|
+
await sliceRoutePlugin("saveRecommendationRoute"),
|
|
1225
|
+
);
|
|
766
1226
|
await flowsScope.register(
|
|
767
1227
|
await sliceRoutePlugin("checkoutRecommendationRoute"),
|
|
768
1228
|
);
|
|
@@ -803,6 +1263,12 @@ export async function build() {
|
|
|
803
1263
|
await scheduleScope.register(
|
|
804
1264
|
await sliceRoutePlugin("createBookingRoute"),
|
|
805
1265
|
);
|
|
1266
|
+
// #6183 S9b D2/D12 — the enabled-meeting-slugs public read. Same scope
|
|
1267
|
+
// as the three routes above: it inherits the module gate and the
|
|
1268
|
+
// strict per-IP rate limit (§9 SEC-4).
|
|
1269
|
+
await scheduleScope.register(
|
|
1270
|
+
await sliceRoutePlugin("meetingTypesRoute"),
|
|
1271
|
+
);
|
|
806
1272
|
});
|
|
807
1273
|
}
|
|
808
1274
|
|