create-ailk 0.5.0 → 0.6.1
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 +336 -27
- package/component-catalog.md +25 -12
- package/dist/cli.js +54 -5
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- 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 +20 -1
- package/dist/lib/extract-module-bundle.js +64 -5
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.js +1 -1
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/programmatic.js +14 -10
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/DESIGN.md +148 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/DESIGN.md +148 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/DESIGN.md +148 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/saas-product/DESIGN.md +148 -0
- package/site-templates/simple-service/DESIGN.md +148 -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/.env.example +72 -7
- package/templates/CLAUDE.md +2 -3
- package/templates/DESIGN.md +155 -0
- package/templates/README.md +42 -8
- 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 +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- 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 +2 -0
- 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 +238 -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 +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -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/index.ts +100 -13
- 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 +439 -49
- 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-routing.test.ts +160 -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 +70 -31
- 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 +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- 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 +57 -24
- 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 +10 -7
- 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/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- 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 +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- 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 +6 -4
- 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 +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/PRODUCT.md +86 -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 +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- 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/jest.config.cjs +8 -4
- 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__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- 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/site-brand.tsx +4 -8
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
- package/templates/content/_site.mdx +18 -3
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -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__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- 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 +3 -2
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- 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/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- /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
|
@@ -11,13 +11,20 @@
|
|
|
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";
|
|
23
30
|
import { defaultTenantPreHandlerPlugin } from "./middleware/default-tenant.js";
|
|
@@ -37,25 +44,87 @@ import {
|
|
|
37
44
|
import type { RegisteredRoute } from "./openapi/surface-drift.js";
|
|
38
45
|
import { exampleAeoRoute, scoreAeoRoute } from "./routes/aeo/index.js";
|
|
39
46
|
import { tokenRoutes } from "./routes/auth/tokens.js";
|
|
47
|
+
import { transactionalEmailRoute } from "./routes/auth/transactional-email.js";
|
|
40
48
|
import { userRoutes } from "./routes/auth/user.js";
|
|
41
49
|
import { getPageRoute } from "./routes/content/get-page.js";
|
|
42
50
|
import { listPagesRoute } from "./routes/content/list-pages.js";
|
|
43
51
|
import { listTypesRoute } from "./routes/content/list-types.js";
|
|
44
52
|
import {
|
|
53
|
+
CONTENT_RATE_LIMIT_AUTH,
|
|
45
54
|
CONTENT_RATE_LIMIT_UNAUTH,
|
|
46
55
|
CONTENT_RATE_LIMIT_WINDOW,
|
|
47
56
|
} from "./routes/content/rate-limit.js";
|
|
57
|
+
import { searchContentRoute } from "./routes/content/search.js";
|
|
48
58
|
import {
|
|
49
59
|
leadCallbackRoute,
|
|
50
60
|
leadSubmitRoute,
|
|
51
61
|
leadSubscribeRoute,
|
|
52
62
|
} from "./routes/leads/index.js";
|
|
53
63
|
import { leadListRoute } from "./routes/leads/list.js";
|
|
64
|
+
import { requestRoutes } from "./routes/requests/index.js";
|
|
54
65
|
import {
|
|
55
66
|
registerTestAuthRoute,
|
|
56
67
|
shouldMountTestAuth,
|
|
57
68
|
} from "./routes/test/auth/session.js";
|
|
58
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
|
+
|
|
59
128
|
/**
|
|
60
129
|
* Paid-module SLICE entry points, keyed by the export each branch needs
|
|
61
130
|
* (#5182).
|
|
@@ -134,11 +203,21 @@ const WORKSPACES_SESSIONS = "./routes/workspaces/sessions.js";
|
|
|
134
203
|
const WORKSPACES_CONFIG = "./routes/workspaces/config.js";
|
|
135
204
|
const WORKSPACES_GENERAL = "./routes/workspaces/general.js";
|
|
136
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";
|
|
137
219
|
const BILLING_INDEX = "./routes/billing/index.js";
|
|
138
220
|
const WAITLIST_EXPERIMENTS_INDEX = "./routes/waitlist-experiments/index.js";
|
|
139
|
-
const CONTENT_CREATE = "./routes/content/create.js";
|
|
140
|
-
const CONTENT_UPDATE = "./routes/content/update.js";
|
|
141
|
-
const CONTENT_DELETE = "./routes/content/delete.js";
|
|
142
221
|
const SCHEDULE_INDEX = "./routes/schedule/index.js";
|
|
143
222
|
const FLOWS_INDEX = "./routes/flows/index.js";
|
|
144
223
|
const DELIVERABLES_INDEX = "./routes/deliverables/index.js";
|
|
@@ -148,7 +227,9 @@ const WAITLIST_SIGNUPS_INDEX = "./routes/waitlist-signups/index.js";
|
|
|
148
227
|
const CHECKOUT_INDEX = "./routes/checkout/index.js";
|
|
149
228
|
const WEBHOOKS_INDEX = "./routes/webhooks/index.js";
|
|
150
229
|
const FLOW_CHECKOUTS_INDEX = "./routes/flow-checkouts/index.js";
|
|
230
|
+
const SUBSCRIPTION_INDEX = "./routes/subscription/index.js";
|
|
151
231
|
const RECOMMENDER_INDEX = "./routes/flows/recommender.js";
|
|
232
|
+
const MAGNETS_INDEX = "./routes/magnets/index.js";
|
|
152
233
|
|
|
153
234
|
/**
|
|
154
235
|
* Paid-module SLICE entry points, keyed by the export each branch needs.
|
|
@@ -183,6 +264,33 @@ const SLICE_PLUGINS = {
|
|
|
183
264
|
specifier: WORKSPACES_MEMBERS,
|
|
184
265
|
load: () => import(WORKSPACES_MEMBERS),
|
|
185
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
|
+
},
|
|
186
294
|
billingRoutes: {
|
|
187
295
|
specifier: BILLING_INDEX,
|
|
188
296
|
load: () => import(BILLING_INDEX),
|
|
@@ -191,18 +299,6 @@ const SLICE_PLUGINS = {
|
|
|
191
299
|
specifier: WAITLIST_EXPERIMENTS_INDEX,
|
|
192
300
|
load: () => import(WAITLIST_EXPERIMENTS_INDEX),
|
|
193
301
|
},
|
|
194
|
-
createPageRoute: {
|
|
195
|
-
specifier: CONTENT_CREATE,
|
|
196
|
-
load: () => import(CONTENT_CREATE),
|
|
197
|
-
},
|
|
198
|
-
updatePageRoute: {
|
|
199
|
-
specifier: CONTENT_UPDATE,
|
|
200
|
-
load: () => import(CONTENT_UPDATE),
|
|
201
|
-
},
|
|
202
|
-
deletePageRoute: {
|
|
203
|
-
specifier: CONTENT_DELETE,
|
|
204
|
-
load: () => import(CONTENT_DELETE),
|
|
205
|
-
},
|
|
206
302
|
listAvailabilityRoute: {
|
|
207
303
|
specifier: SCHEDULE_INDEX,
|
|
208
304
|
load: () => import(SCHEDULE_INDEX),
|
|
@@ -215,6 +311,20 @@ const SLICE_PLUGINS = {
|
|
|
215
311
|
specifier: SCHEDULE_INDEX,
|
|
216
312
|
load: () => import(SCHEDULE_INDEX),
|
|
217
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
|
+
},
|
|
218
328
|
getFlowRoute: {
|
|
219
329
|
specifier: FLOWS_INDEX,
|
|
220
330
|
load: () => import(FLOWS_INDEX),
|
|
@@ -227,6 +337,12 @@ const SLICE_PLUGINS = {
|
|
|
227
337
|
specifier: FLOWS_INDEX,
|
|
228
338
|
load: () => import(FLOWS_INDEX),
|
|
229
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
|
+
},
|
|
230
346
|
deliverableDownloadRoute: {
|
|
231
347
|
specifier: DELIVERABLES_INDEX,
|
|
232
348
|
load: () => import(DELIVERABLES_INDEX),
|
|
@@ -265,9 +381,13 @@ const SLICE_PLUGINS = {
|
|
|
265
381
|
specifier: WAITLIST_SIGNUPS_INDEX,
|
|
266
382
|
load: () => import(WAITLIST_SIGNUPS_INDEX),
|
|
267
383
|
},
|
|
268
|
-
// Commerce (D-47, #5364)
|
|
269
|
-
//
|
|
270
|
-
//
|
|
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.
|
|
271
391
|
checkoutRoutes: {
|
|
272
392
|
specifier: CHECKOUT_INDEX,
|
|
273
393
|
load: () => import(CHECKOUT_INDEX),
|
|
@@ -280,6 +400,13 @@ const SLICE_PLUGINS = {
|
|
|
280
400
|
specifier: FLOW_CHECKOUTS_INDEX,
|
|
281
401
|
load: () => import(FLOW_CHECKOUTS_INDEX),
|
|
282
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
|
+
},
|
|
283
410
|
saveRecommendationRoute: {
|
|
284
411
|
specifier: RECOMMENDER_INDEX,
|
|
285
412
|
load: () => import(RECOMMENDER_INDEX),
|
|
@@ -386,12 +513,22 @@ export async function build() {
|
|
|
386
513
|
// Startup-time validation: fail fast if LEAD_CAPTURE_FILE points at a missing directory.
|
|
387
514
|
await validateLeadCaptureDir();
|
|
388
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
|
+
|
|
389
525
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
390
526
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
391
|
-
// assertSandboxKey is called again inside a route handler.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
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) {
|
|
395
532
|
await assertCommerceSandboxKey(config.STRIPE_SECRET_KEY);
|
|
396
533
|
}
|
|
397
534
|
|
|
@@ -418,7 +555,27 @@ export async function build() {
|
|
|
418
555
|
// (server-action-forwarded traffic), which was previously broken for
|
|
419
556
|
// every caller.
|
|
420
557
|
const server = Fastify({
|
|
421
|
-
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
|
+
},
|
|
422
579
|
trustProxy: true,
|
|
423
580
|
});
|
|
424
581
|
|
|
@@ -581,6 +738,17 @@ export async function build() {
|
|
|
581
738
|
await authScope.register(await sliceRoutePlugin("flowCheckoutRoutes"));
|
|
582
739
|
}
|
|
583
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
|
+
|
|
584
752
|
// The founder's OWN listings read (#5295) — the same posture again: auth
|
|
585
753
|
// only, NOT tenant-guarded, because a founder is not a workspace member
|
|
586
754
|
// and the tenant guard is default-deny. Identity is request.userId; the
|
|
@@ -657,6 +825,51 @@ export async function build() {
|
|
|
657
825
|
});
|
|
658
826
|
}
|
|
659
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
|
+
|
|
660
873
|
// Workspace SETTINGS routes — a sibling tenant scope, gated on EITHER
|
|
661
874
|
// module (#5371 D2). The site-config routes were registered inside the
|
|
662
875
|
// multi-tenant scope above, which was too narrow: nothing in their logic
|
|
@@ -702,28 +915,157 @@ export async function build() {
|
|
|
702
915
|
await tenantScope.register(
|
|
703
916
|
await sliceRoutePlugin("workspaceMembersRoutes"),
|
|
704
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
|
+
);
|
|
705
995
|
});
|
|
706
996
|
}
|
|
707
997
|
});
|
|
708
998
|
|
|
709
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
|
+
}
|
|
710
1009
|
await server.register(listTypesRoute);
|
|
1010
|
+
// Before `listPagesRoute`: `search` must never be read as a `:type`.
|
|
1011
|
+
await server.register(searchContentRoute);
|
|
711
1012
|
await server.register(listPagesRoute);
|
|
712
1013
|
await server.register(getPageRoute);
|
|
713
|
-
//
|
|
714
|
-
// CALL time
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
});
|
|
727
1069
|
// Deliverable download — a capability-token URL (#3961 D5); the token
|
|
728
1070
|
// itself is the sole gate, so no auth preHandler applies. Module-conditional
|
|
729
1071
|
// (signal-collectors, D-48): gated PDFs and reports.
|
|
@@ -751,12 +1093,15 @@ export async function build() {
|
|
|
751
1093
|
done(null, body);
|
|
752
1094
|
},
|
|
753
1095
|
);
|
|
754
|
-
// Stripe webhook receiver —
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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"));
|
|
760
1105
|
// Project-listing operator decision (Slack Approve/Reject) — the "hand an
|
|
761
1106
|
// operator a decision" half of D-48's grouping sentence. Module-conditional
|
|
762
1107
|
// (signal-collectors).
|
|
@@ -765,6 +1110,35 @@ export async function build() {
|
|
|
765
1110
|
}
|
|
766
1111
|
});
|
|
767
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
|
+
|
|
768
1142
|
// Leads routes — rate-limit middleware scoped to this sub-router. Own
|
|
769
1143
|
// default (10/min, pre-#4730) rather than inheriting the wider
|
|
770
1144
|
// flows-sized default (#5111, DEF-004 — #4730's shared-default bump was
|
|
@@ -802,11 +1176,15 @@ export async function build() {
|
|
|
802
1176
|
// every recipient — the residual the first one left, and the one that
|
|
803
1177
|
// decides whether a flood can cost the site its sending domain.
|
|
804
1178
|
if (isModuleEnabled("signal-collectors")) {
|
|
805
|
-
await leadsScope.register(
|
|
1179
|
+
await leadsScope.register(
|
|
1180
|
+
await sliceRoutePlugin("projectListingStartRoute"),
|
|
1181
|
+
);
|
|
806
1182
|
await leadsScope.register(
|
|
807
1183
|
await sliceRoutePlugin("projectListingDraftPatchRoute"),
|
|
808
1184
|
);
|
|
809
|
-
await leadsScope.register(
|
|
1185
|
+
await leadsScope.register(
|
|
1186
|
+
await sliceRoutePlugin("projectListingSubmitRoute"),
|
|
1187
|
+
);
|
|
810
1188
|
// #5407 — the resume link's verification write. Anonymous, and gated
|
|
811
1189
|
// on the resume token AND the nonce that only the resume email
|
|
812
1190
|
// carries, so it belongs in the same rate-limited scope as the other
|
|
@@ -814,7 +1192,9 @@ export async function build() {
|
|
|
814
1192
|
await leadsScope.register(
|
|
815
1193
|
await sliceRoutePlugin("projectListingVerifyEmailRoute"),
|
|
816
1194
|
);
|
|
817
|
-
await leadsScope.register(
|
|
1195
|
+
await leadsScope.register(
|
|
1196
|
+
await sliceRoutePlugin("projectListingPublicRoute"),
|
|
1197
|
+
);
|
|
818
1198
|
}
|
|
819
1199
|
});
|
|
820
1200
|
|
|
@@ -832,13 +1212,17 @@ export async function build() {
|
|
|
832
1212
|
if (isModuleEnabled("signal-collectors")) {
|
|
833
1213
|
await flowsScope.register(await sliceRoutePlugin("getFlowRoute"));
|
|
834
1214
|
await flowsScope.register(await sliceRoutePlugin("submitFlowStepRoute"));
|
|
835
|
-
await flowsScope.register(
|
|
1215
|
+
await flowsScope.register(
|
|
1216
|
+
await sliceRoutePlugin("getFlowAggregateRoute"),
|
|
1217
|
+
);
|
|
836
1218
|
}
|
|
837
1219
|
// The recommender's email-moment routes (#4841) — same anonymous,
|
|
838
1220
|
// rate-limited posture as the three above. Module-conditional (commerce,
|
|
839
1221
|
// D-47): the recommender leaves Pro flows and moves to commerce whole.
|
|
840
1222
|
if (isModuleEnabled("commerce")) {
|
|
841
|
-
await flowsScope.register(
|
|
1223
|
+
await flowsScope.register(
|
|
1224
|
+
await sliceRoutePlugin("saveRecommendationRoute"),
|
|
1225
|
+
);
|
|
842
1226
|
await flowsScope.register(
|
|
843
1227
|
await sliceRoutePlugin("checkoutRecommendationRoute"),
|
|
844
1228
|
);
|
|
@@ -879,6 +1263,12 @@ export async function build() {
|
|
|
879
1263
|
await scheduleScope.register(
|
|
880
1264
|
await sliceRoutePlugin("createBookingRoute"),
|
|
881
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
|
+
);
|
|
882
1272
|
});
|
|
883
1273
|
}
|
|
884
1274
|
|