create-ailk 0.5.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 +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- 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/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/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -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/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/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/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__/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/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- 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 +61 -0
- 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 +142 -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 +1 -1
- 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/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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file revalidation.ts
|
|
3
|
+
* @description The signed webhook `apps/api` fires at the site's
|
|
4
|
+
* `/api/revalidate` on publish and unpublish (#5851, roadmap D6). One POST of
|
|
5
|
+
* `{ workspaceId, type, slug, locale }` to `CONTENT_REVALIDATE_URL`, signed
|
|
6
|
+
* HMAC-SHA256 over the exact body bytes with `CONTENT_REVALIDATE_SECRET`.
|
|
7
|
+
*
|
|
8
|
+
* Either var absent means "revalidation off": one `warn` and return. Both set
|
|
9
|
+
* means three bounded attempts (500 ms then 2000 ms backoff, 10 s per-attempt
|
|
10
|
+
* timeout — 32.5 s worst case), every attempt logged, and a final failure
|
|
11
|
+
* LOGGED rather than thrown. `notifyRevalidate` never rejects, which is what
|
|
12
|
+
* lets the publish path `await` it and still always complete.
|
|
13
|
+
*
|
|
14
|
+
* PII boundary: log lines carry only
|
|
15
|
+
* `{ event, workspaceId, type, slug, locale, attempt/attempts, status/lastStatus, errName }`.
|
|
16
|
+
* Never the secret, never the signature, never a response body, never page
|
|
17
|
+
* content.
|
|
18
|
+
*
|
|
19
|
+
* Nothing calls this yet — the publish/unpublish handlers are a sibling child
|
|
20
|
+
* of Epic #5323, and the receiver is Epic #5324. This module fixes the wire
|
|
21
|
+
* contract a receiver implements: method POST; the four-key JSON body;
|
|
22
|
+
* headers `content-type: application/json`, `x-ailk-revalidate-signature:
|
|
23
|
+
* sha256=<lowercase hex>` and `x-ailk-revalidate-event: publish | unpublish`.
|
|
24
|
+
* A receiver verifies by recomputing the HMAC over the RAW request body and
|
|
25
|
+
* comparing in constant time.
|
|
26
|
+
*/
|
|
27
|
+
import { createHmac } from "node:crypto";
|
|
28
|
+
|
|
29
|
+
import { createLogger } from "@working-theory/observability";
|
|
30
|
+
|
|
31
|
+
import { config } from "../config/index.js";
|
|
32
|
+
import { outboundFetch } from "../lib/outbound-http.js";
|
|
33
|
+
|
|
34
|
+
const log = createLogger({ service: "revalidation" });
|
|
35
|
+
|
|
36
|
+
const SIGNATURE_HEADER = "x-ailk-revalidate-signature";
|
|
37
|
+
const EVENT_HEADER = "x-ailk-revalidate-event";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Backoff before attempt 2 and attempt 3. Three attempts total, no fourth:
|
|
41
|
+
* 3 × the wrapper's 10 s timeout plus these two sleeps is 32.5 s worst case,
|
|
42
|
+
* which fits inside the PRD's 60 s publish-to-live budget.
|
|
43
|
+
*/
|
|
44
|
+
const RETRY_BACKOFF_MS = [500, 2000];
|
|
45
|
+
const MAX_ATTEMPTS = RETRY_BACKOFF_MS.length + 1;
|
|
46
|
+
|
|
47
|
+
export interface RevalidateTarget {
|
|
48
|
+
workspaceId: string;
|
|
49
|
+
type: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
locale: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RevalidateOptions {
|
|
55
|
+
event: "publish" | "unpublish";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sleep(ms: number): Promise<void> {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 429 and 5xx are worth another attempt. Every other non-2xx is terminal — a
|
|
64
|
+
* 401 from a wrong secret or a 404 from a wrong path will not succeed on a
|
|
65
|
+
* second try, and retrying it triples the log noise for a misconfiguration.
|
|
66
|
+
*/
|
|
67
|
+
function isRetryableStatus(status: number): boolean {
|
|
68
|
+
return status === 429 || status >= 500;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Tells the site to revalidate one piece of content. Resolves in every case —
|
|
73
|
+
* success, exhausted retries, or unconfigured — so an `await` in the publish
|
|
74
|
+
* path can never fail the publish it follows.
|
|
75
|
+
*/
|
|
76
|
+
export async function notifyRevalidate(
|
|
77
|
+
target: RevalidateTarget,
|
|
78
|
+
options: RevalidateOptions,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const url = config.CONTENT_REVALIDATE_URL;
|
|
81
|
+
const secret = config.CONTENT_REVALIDATE_SECRET;
|
|
82
|
+
|
|
83
|
+
// The PII-safe key set every line in this module carries.
|
|
84
|
+
const base = { event: options.event, ...target };
|
|
85
|
+
|
|
86
|
+
if (!url || !secret) {
|
|
87
|
+
// Signing with an empty key would be worse than skipping: every delivery
|
|
88
|
+
// would be rejected by the receiver with no local signal.
|
|
89
|
+
const reason = !url ? "url_unset" : "secret_unset";
|
|
90
|
+
log.warn({ ...base, reason }, "revalidate.skipped");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ONE serialization: this exact string is both signed and sent, so a
|
|
95
|
+
// key-order or whitespace drift between signer and sender is impossible by
|
|
96
|
+
// construction.
|
|
97
|
+
const body = JSON.stringify({
|
|
98
|
+
workspaceId: target.workspaceId,
|
|
99
|
+
type: target.type,
|
|
100
|
+
slug: target.slug,
|
|
101
|
+
locale: target.locale,
|
|
102
|
+
});
|
|
103
|
+
const signature = createHmac("sha256", secret)
|
|
104
|
+
.update(body, "utf8")
|
|
105
|
+
.digest("hex");
|
|
106
|
+
|
|
107
|
+
let attempt = 0;
|
|
108
|
+
let lastStatus: number | undefined;
|
|
109
|
+
let errName: string | undefined;
|
|
110
|
+
|
|
111
|
+
while (attempt < MAX_ATTEMPTS) {
|
|
112
|
+
attempt++;
|
|
113
|
+
let retryable: boolean;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const res = await outboundFetch(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"content-type": "application/json",
|
|
120
|
+
[SIGNATURE_HEADER]: `sha256=${signature}`,
|
|
121
|
+
[EVENT_HEADER]: options.event,
|
|
122
|
+
},
|
|
123
|
+
body,
|
|
124
|
+
// The SSRF gate is waived deliberately and narrowly: this URL comes
|
|
125
|
+
// from deployment environment, never from a request body or a
|
|
126
|
+
// database row. Leaving it on would break local development (the site
|
|
127
|
+
// is http://localhost:3000) and any single-host deployment.
|
|
128
|
+
allowPrivate: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (res.ok) {
|
|
132
|
+
log.info({ ...base, attempt, status: res.status }, "revalidate.sent");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lastStatus = res.status;
|
|
137
|
+
errName = undefined;
|
|
138
|
+
retryable = isRetryableStatus(res.status);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
lastStatus = undefined;
|
|
141
|
+
errName = err instanceof Error ? err.name : "UnknownError";
|
|
142
|
+
retryable = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log.warn(
|
|
146
|
+
{
|
|
147
|
+
...base,
|
|
148
|
+
attempt,
|
|
149
|
+
...(lastStatus !== undefined && { status: lastStatus }),
|
|
150
|
+
...(errName !== undefined && { errName }),
|
|
151
|
+
},
|
|
152
|
+
"revalidate.failed",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!retryable) break;
|
|
156
|
+
|
|
157
|
+
// Undefined on the last attempt — the schedule has one entry fewer than
|
|
158
|
+
// MAX_ATTEMPTS, so running out of backoffs IS the "no fourth attempt" bound.
|
|
159
|
+
const backoffMs = RETRY_BACKOFF_MS[attempt - 1];
|
|
160
|
+
if (backoffMs === undefined) break;
|
|
161
|
+
await sleep(backoffMs);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
log.error(
|
|
165
|
+
{
|
|
166
|
+
...base,
|
|
167
|
+
attempts: attempt,
|
|
168
|
+
...(lastStatus !== undefined && { lastStatus }),
|
|
169
|
+
...(errName !== undefined && { errName }),
|
|
170
|
+
},
|
|
171
|
+
"revalidate.failed",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file verified-email.ts
|
|
3
|
+
* @description The one `User {id, email, emailVerified}` read every
|
|
4
|
+
* email-keyed authorization path in `apps/api` goes through (#5701 D4).
|
|
5
|
+
*
|
|
6
|
+
* `emailVerified` is load-bearing wherever this codebase joins an identity to
|
|
7
|
+
* data by email. An unverified address is an unproven CLAIM to the identity it
|
|
8
|
+
* names: the operator pin it might match (`services/default-tenant.ts`), the
|
|
9
|
+
* anonymous Lead rows it would promote (`services/lead-promotion.ts`,
|
|
10
|
+
* `services/listing-promotion.ts`), the respondent rows it would read back
|
|
11
|
+
* (`services/waitlist-signups.ts`, `services/flow-checkouts.ts`). Each of
|
|
12
|
+
* those five sites wrote the same read and the same refusal by hand, and
|
|
13
|
+
* "the same read, written six times" is exactly the shape that let #3631
|
|
14
|
+
* ship a sixth one without the check.
|
|
15
|
+
*
|
|
16
|
+
* So the read lives here once, the refusal is inside it rather than at the
|
|
17
|
+
* call site — a service-level invariant, not a caller obligation, which is
|
|
18
|
+
* what makes it unbypassable by a wrong call site — and callers receive the
|
|
19
|
+
* email ONLY when `emailVerified === true`. The two refusals stay distinct
|
|
20
|
+
* (`user_not_found` vs `email_unverified`) because the callers map them to
|
|
21
|
+
* different outcomes on the wire.
|
|
22
|
+
*
|
|
23
|
+
* `select` names exactly the three fields, so nothing else about the User row
|
|
24
|
+
* is read on an authorization path; `tools/lint-email-authz.ts` is the
|
|
25
|
+
* mechanical backstop that fails any NEW `prisma.user` read which selects
|
|
26
|
+
* `email` for a decision and omits `emailVerified`.
|
|
27
|
+
*
|
|
28
|
+
* ONE KNOWN IMPRECISION IN THE CALLERS, deliberate and unchanged by this
|
|
29
|
+
* helper. `User.email` is `@unique` in Postgres, which is byte-exact, while
|
|
30
|
+
* every caller that compares the address lowercases both sides — so two
|
|
31
|
+
* distinct rows (`Operator@x.com` and `operator@x.com`) could both satisfy one
|
|
32
|
+
* comparison. It is not reachable today: `apps/web/auth.ts` configures social
|
|
33
|
+
* providers only, so the address is provider-asserted and normalized rather
|
|
34
|
+
* than caller-chosen. It becomes reachable the moment a credential provider is
|
|
35
|
+
* enabled, and the durable fix is case-insensitive email uniqueness in the
|
|
36
|
+
* schema (citext, or lowercase-at-write) so "matches" and "is unique" agree.
|
|
37
|
+
* Matching case-sensitively instead would be worse: it would lock out an
|
|
38
|
+
* operator who typed their own address with different capitalisation.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { prisma } from "../lib/prisma.js";
|
|
42
|
+
|
|
43
|
+
/** Why a verified-email read refused. Callers map these to their own outcomes. */
|
|
44
|
+
export type VerifiedEmailRefusal = "user_not_found" | "email_unverified";
|
|
45
|
+
|
|
46
|
+
export type VerifiedEmailResult =
|
|
47
|
+
| { ok: true; value: { id: string; email: string } }
|
|
48
|
+
| { ok: false; reason: VerifiedEmailRefusal };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the `User` row for `userId` and return its email ONLY when the address
|
|
52
|
+
* is verified.
|
|
53
|
+
*
|
|
54
|
+
* @param userId the authenticated caller's id — never a request-supplied email.
|
|
55
|
+
* @returns `{ok: true}` with the verified address, or the refusal reason.
|
|
56
|
+
*/
|
|
57
|
+
export async function findVerifiedEmail(
|
|
58
|
+
userId: string,
|
|
59
|
+
): Promise<VerifiedEmailResult> {
|
|
60
|
+
const user = await prisma.user.findUnique({
|
|
61
|
+
where: { id: userId },
|
|
62
|
+
select: { id: true, email: true, emailVerified: true },
|
|
63
|
+
});
|
|
64
|
+
if (!user) {
|
|
65
|
+
return { ok: false, reason: "user_not_found" };
|
|
66
|
+
}
|
|
67
|
+
if (user.emailVerified !== true) {
|
|
68
|
+
return { ok: false, reason: "email_unverified" };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, value: { id: user.id, email: user.email } };
|
|
71
|
+
}
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* This file wraps the Fastify server for Vercel's serverless functions
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { VercelRequest, VercelResponse } from
|
|
7
|
-
import { logger } from
|
|
8
|
-
import type { InjectOptions } from
|
|
6
|
+
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
7
|
+
import { logger } from "@working-theory/observability";
|
|
8
|
+
import type { InjectOptions } from "fastify";
|
|
9
9
|
|
|
10
|
-
import type { build as buildServer } from
|
|
10
|
+
import type { build as buildServer } from "./server.js";
|
|
11
11
|
|
|
12
12
|
type FastifyServer = Awaited<ReturnType<typeof buildServer>>;
|
|
13
13
|
|
|
14
14
|
// Import the build function from server
|
|
15
15
|
async function getBuildFunction(): Promise<typeof buildServer> {
|
|
16
16
|
try {
|
|
17
|
-
const srcModule = await import(
|
|
18
|
-
logger.info(
|
|
17
|
+
const srcModule = await import("./server.js");
|
|
18
|
+
logger.info("[Vercel Handler] Using server from src/server.js");
|
|
19
19
|
return srcModule.build;
|
|
20
20
|
} catch (srcError) {
|
|
21
21
|
logger.error(
|
|
@@ -23,9 +23,12 @@ async function getBuildFunction(): Promise<typeof buildServer> {
|
|
|
23
23
|
err: srcError instanceof Error ? srcError.message : String(srcError),
|
|
24
24
|
stack: srcError instanceof Error ? srcError.stack : undefined,
|
|
25
25
|
},
|
|
26
|
-
|
|
26
|
+
"[Vercel Handler] Failed to import server",
|
|
27
|
+
);
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Cannot find server module: ${srcError instanceof Error ? srcError.message : String(srcError)}`,
|
|
30
|
+
{ cause: srcError },
|
|
27
31
|
);
|
|
28
|
-
throw new Error(`Cannot find server module: ${srcError instanceof Error ? srcError.message : String(srcError)}`, { cause: srcError });
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -45,30 +48,42 @@ const buildPromise = getBuildFunction();
|
|
|
45
48
|
let serverPromise: Promise<FastifyServer> | null = null;
|
|
46
49
|
|
|
47
50
|
async function buildServerInstance(): Promise<FastifyServer> {
|
|
48
|
-
logger.info(
|
|
51
|
+
logger.info("[Vercel Handler] Building server instance...");
|
|
49
52
|
const startTime = Date.now();
|
|
50
53
|
|
|
51
54
|
try {
|
|
52
55
|
const buildTimeout = new Promise<never>((_, reject) => {
|
|
53
|
-
setTimeout(
|
|
56
|
+
setTimeout(
|
|
57
|
+
() => reject(new Error("Server build timeout after 10s")),
|
|
58
|
+
10000,
|
|
59
|
+
);
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
const build = await Promise.race([buildPromise, buildTimeout]);
|
|
57
|
-
logger.info(
|
|
63
|
+
logger.info("[Vercel Handler] Build function obtained, creating server...");
|
|
58
64
|
|
|
59
65
|
const initTimeout = new Promise<never>((_, reject) => {
|
|
60
|
-
setTimeout(
|
|
66
|
+
setTimeout(
|
|
67
|
+
() => reject(new Error("Server initialization timeout after 10s")),
|
|
68
|
+
10000,
|
|
69
|
+
);
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
const serverInstance = await Promise.race([build(), initTimeout]);
|
|
64
73
|
|
|
65
74
|
const buildTime = Date.now() - startTime;
|
|
66
|
-
logger.info({ buildTime },
|
|
75
|
+
logger.info({ buildTime }, "[Vercel Handler] Server built successfully");
|
|
67
76
|
return serverInstance;
|
|
68
77
|
} catch (error) {
|
|
69
78
|
const buildTime = Date.now() - startTime;
|
|
70
|
-
logger.error(
|
|
71
|
-
|
|
79
|
+
logger.error(
|
|
80
|
+
{ err: error, buildTime },
|
|
81
|
+
"[Vercel Handler] Failed to build server",
|
|
82
|
+
);
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Server build failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
85
|
+
{ cause: error },
|
|
86
|
+
);
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
|
|
@@ -102,27 +117,46 @@ function getServer(): Promise<FastifyServer> {
|
|
|
102
117
|
*/
|
|
103
118
|
export default async function handler(
|
|
104
119
|
req: VercelRequest,
|
|
105
|
-
res: VercelResponse
|
|
120
|
+
res: VercelResponse,
|
|
106
121
|
): Promise<void> {
|
|
107
122
|
const timeout = setTimeout(() => {
|
|
108
123
|
if (!res.headersSent) {
|
|
109
|
-
logger.error(
|
|
124
|
+
logger.error("[Vercel Handler] Request timeout");
|
|
110
125
|
res.status(504).json({
|
|
111
|
-
error:
|
|
112
|
-
message:
|
|
126
|
+
error: "Gateway Timeout",
|
|
127
|
+
message: "Request took too long to process",
|
|
113
128
|
});
|
|
114
129
|
}
|
|
115
130
|
}, 25000);
|
|
116
131
|
|
|
117
132
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
// The PATHNAME, never the raw target (#5970 security review round 2, H1).
|
|
134
|
+
//
|
|
135
|
+
// This is the production entry point — `vercel.json` routes every request
|
|
136
|
+
// here — and it logs BEFORE Fastify's own (query-stripped) "incoming
|
|
137
|
+
// request" line, so it is the first and last word on what reaches the log
|
|
138
|
+
// drain. `req.url` is the raw target INCLUDING the query string, and a
|
|
139
|
+
// route may carry its credential there: `GET
|
|
140
|
+
// /v1/requests/:id/confirm?token=…` does, because a link in an email has
|
|
141
|
+
// nowhere else to put one. Logging it wrote a live seven-day credential
|
|
142
|
+
// into the drain on every open.
|
|
143
|
+
//
|
|
144
|
+
// The high-entropy redactor in `@working-theory/observability` is NOT the
|
|
145
|
+
// control here: it is a probabilistic backstop, and a 43-char base64url
|
|
146
|
+
// token falls under its entropy threshold often enough to leak in full.
|
|
147
|
+
// Stripping the query is the control; the redactor stays the backstop.
|
|
148
|
+
//
|
|
149
|
+
// Derived once, above the first log line, and used by both — the pathname
|
|
150
|
+
// was already computed below and logged safely there, three lines too late.
|
|
151
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
123
152
|
const pathname = url.pathname;
|
|
124
153
|
|
|
125
|
-
logger.info(
|
|
154
|
+
logger.info(
|
|
155
|
+
{ method: req.method, pathname },
|
|
156
|
+
"[Vercel Handler] Starting request",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const server = await getServer();
|
|
126
160
|
|
|
127
161
|
// Strip content-length/content-encoding before forwarding. Both
|
|
128
162
|
// describe the ORIGINAL byte stream the client sent; req.body here is
|
|
@@ -137,13 +171,16 @@ export default async function handler(
|
|
|
137
171
|
// incoming header names in practice.
|
|
138
172
|
const forwardedHeaders = { ...req.headers } as Record<string, string>;
|
|
139
173
|
for (const key of Object.keys(forwardedHeaders)) {
|
|
140
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
key.toLowerCase() === "content-length" ||
|
|
176
|
+
key.toLowerCase() === "content-encoding"
|
|
177
|
+
) {
|
|
141
178
|
delete forwardedHeaders[key];
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
const fastifyRequest = {
|
|
146
|
-
method: req.method ||
|
|
183
|
+
method: req.method || "GET",
|
|
147
184
|
url: pathname,
|
|
148
185
|
headers: forwardedHeaders,
|
|
149
186
|
body: req.body,
|
|
@@ -152,7 +189,10 @@ export default async function handler(
|
|
|
152
189
|
|
|
153
190
|
const response = await server.inject(fastifyRequest as InjectOptions);
|
|
154
191
|
|
|
155
|
-
logger.info(
|
|
192
|
+
logger.info(
|
|
193
|
+
{ status: response.statusCode },
|
|
194
|
+
"[Vercel Handler] Response status",
|
|
195
|
+
);
|
|
156
196
|
|
|
157
197
|
clearTimeout(timeout);
|
|
158
198
|
|
|
@@ -161,14 +201,14 @@ export default async function handler(
|
|
|
161
201
|
Object.entries(response.headers).forEach(([key, value]) => {
|
|
162
202
|
if (value !== undefined && !res.headersSent) {
|
|
163
203
|
const headerValue = Array.isArray(value)
|
|
164
|
-
? value.map((v) => String(v)).join(
|
|
204
|
+
? value.map((v) => String(v)).join(", ")
|
|
165
205
|
: String(value);
|
|
166
206
|
res.setHeader(key, headerValue);
|
|
167
207
|
}
|
|
168
208
|
});
|
|
169
209
|
|
|
170
|
-
if (!res.getHeader(
|
|
171
|
-
res.setHeader(
|
|
210
|
+
if (!res.getHeader("Content-Type") && !res.headersSent) {
|
|
211
|
+
res.setHeader("Content-Type", "application/json");
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
if (!res.headersSent) {
|
|
@@ -179,20 +219,20 @@ export default async function handler(
|
|
|
179
219
|
// `rawPayload` is byte-identical to what Fastify sent regardless of
|
|
180
220
|
// content type, so this also covers the JSON case unchanged.
|
|
181
221
|
res.send(response.rawPayload);
|
|
182
|
-
logger.info(
|
|
222
|
+
logger.info("[Vercel Handler] Response sent successfully");
|
|
183
223
|
}
|
|
184
224
|
} catch (error) {
|
|
185
225
|
clearTimeout(timeout);
|
|
186
226
|
|
|
187
|
-
logger.error({ err: error },
|
|
227
|
+
logger.error({ err: error }, "[Vercel Handler] Error handling request");
|
|
188
228
|
|
|
189
229
|
if (!res.headersSent) {
|
|
190
230
|
// Generic body only (security review on #146): this catch sits above
|
|
191
231
|
// Fastify's error serializer, so error.message could carry hosts,
|
|
192
232
|
// paths, or driver detail. The logger.error above keeps the detail.
|
|
193
233
|
res.status(500).json({
|
|
194
|
-
error:
|
|
195
|
-
message:
|
|
234
|
+
error: "Internal Server Error",
|
|
235
|
+
message: "Something went wrong on our end.",
|
|
196
236
|
});
|
|
197
237
|
}
|
|
198
238
|
}
|
|
@@ -53,8 +53,25 @@ MCP_CORS_WRITE_ALLOWED_ORIGINS=
|
|
|
53
53
|
# Maximum number of capture_lead submissions per IP per day.
|
|
54
54
|
MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
|
|
55
55
|
|
|
56
|
+
# Maximum number of start_purchase calls per IP per day (#5971). Its own
|
|
57
|
+
# budget, never shared with capture_lead's — each call creates a real Stripe
|
|
58
|
+
# hosted Checkout Session, so the default is deliberately lower.
|
|
59
|
+
MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
|
|
60
|
+
|
|
56
61
|
# Comma-separated list of User-Agent substrings to block (case-insensitive).
|
|
57
62
|
# Built-in list already blocks curl, wget, python-requests, scrapy, etc.
|
|
58
63
|
# Add extra patterns here as needed.
|
|
59
64
|
# Example: BadBot,EvilCrawler
|
|
60
65
|
MCP_ABUSE_BLOCKED_USER_AGENTS=
|
|
66
|
+
|
|
67
|
+
# ── Content adapter (#5876) ──────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
# Which content adapter every content tool resolves: mdx (default, reads
|
|
70
|
+
# /content/*.mdx at the repo root) | projection (compiled manifest) | database
|
|
71
|
+
# (the Artifact store — the server resolves its own single workspace at
|
|
72
|
+
# bootstrap; a deployment that isn't unambiguously one-workspace serves no
|
|
73
|
+
# content rather than guessing).
|
|
74
|
+
# CONTENT_ADAPTER=mdx
|
|
75
|
+
|
|
76
|
+
# Required ONLY when CONTENT_ADAPTER=database.
|
|
77
|
+
# DATABASE_URL=
|
|
@@ -28,38 +28,67 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
28
28
|
**Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366): identity-blind and free of any `requirePaidTier()` call like every tool in this list, but registered only when the signal-collectors module is present, the same shape Booking below already has
|
|
29
29
|
**Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`)
|
|
30
30
|
|
|
31
|
-
### Why the
|
|
32
|
-
|
|
33
|
-
- **
|
|
34
|
-
`packages/metadata/src/mcp-tool-catalog.ts` defines
|
|
35
|
-
test counts that full
|
|
36
|
-
- **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
`
|
|
31
|
+
### Why the parity test counts 20 and a keyless server registers 19
|
|
32
|
+
|
|
33
|
+
- **20 in the `TOOLS` array / parity test**: the `TOOLS` array in
|
|
34
|
+
`packages/metadata/src/mcp-tool-catalog.ts` defines 20 tools; the parity
|
|
35
|
+
test counts that full array.
|
|
36
|
+
- **19 registered on a KEYLESS deployment, 20 on a commerce-claimed one**
|
|
37
|
+
(#5971): `createMcpServer` (`apps/mcp/src/server.ts`) registers
|
|
38
|
+
`getMcpToolRegistrations()` minus `OSS_DISABLED_TOOLS` (now empty), minus
|
|
39
|
+
module-gated tools whose module is off, minus any `addon`-tagged tool whose
|
|
40
|
+
claim this deployment does not hold. `start_purchase` carries
|
|
41
|
+
`addon: "commerce"`, so it is the one entry whose presence depends on the
|
|
42
|
+
licence — 19 without the claim, 20 with it.
|
|
43
|
+
- **19 free anonymous tools**: since #5853 retired the three content-write
|
|
44
|
+
tools, NO registered tool is gated at call time by `requirePaidTier()`, so
|
|
45
|
+
every registered tool is free and anonymous. The five carrying `tier: "pro"`
|
|
46
|
+
(`get_flow`, `submit_flow_step`, `list_availability`, `get_event_meta`,
|
|
47
|
+
`create_booking`) are MODULE-gated at registration, not payment-gated at
|
|
48
|
+
call time — the shape the Flow and Booking rows above describe.
|
|
42
49
|
`index.ts` is the transport entry point and exports no tools — registration
|
|
43
50
|
is entirely via `createMcpServer`.
|
|
44
51
|
|
|
45
52
|
## Pro + disabled tools (not part of the free surface)
|
|
46
53
|
|
|
47
|
-
- `create_page`, `update_page`, `delete_page` —
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
|
|
54
|
+
- `create_page`, `update_page`, `delete_page` — **RETIRED by #5853**, not
|
|
55
|
+
merely disabled. Their HTTP twins (the untenanted `/v1/content` writes) were
|
|
56
|
+
deleted in the same PR, so keeping the tools would have left three MCP tools
|
|
57
|
+
with no HTTP equivalent — a direct breach of the API↔MCP parity rule. Their
|
|
58
|
+
replacement is the tenant-guarded
|
|
59
|
+
`/v1/workspaces/:workspaceId/content` family, which resolves its scope from a
|
|
60
|
+
session-bound `TenantContext`; an anonymous caller supplying a `workspaceId`
|
|
61
|
+
is exactly the client-supplied scope that surface exists to refuse, so the
|
|
62
|
+
identity-blind MCP catalog can never carry it. `OSS_DISABLED_TOOLS` was the
|
|
63
|
+
wrong instrument: it promises a re-enable path, and there is none until the
|
|
64
|
+
authed MCP surface (#3591) exists.
|
|
65
|
+
- `start_purchase` — **no longer disabled** (#5971). It returns a
|
|
66
|
+
Stripe-HOSTED Checkout URL for a named offering and charges nothing in-tool;
|
|
67
|
+
the buyer pays on Stripe's own page. It left `OSS_DISABLED_TOOLS` — which is
|
|
68
|
+
now empty — and its visibility is gated instead on the deployment's
|
|
69
|
+
`commerce` licence claim, per its `addon: "commerce"` catalog marker. A
|
|
70
|
+
keyless site advertises it in neither the discovery manifest nor the running
|
|
71
|
+
server's tool list, and calling it by name there returns the module-disabled
|
|
72
|
+
`not_found` body. The claim is resolved once at boot
|
|
73
|
+
(`src/lib/commerce-claim.ts`, awaited in `src/index.ts`) and read
|
|
74
|
+
synchronously by both the registration filter and the handler's own
|
|
75
|
+
re-check. Calls are rate-limited per IP by the AbuseGuard
|
|
76
|
+
(`MCP_ABUSE_START_PURCHASE_DAILY_CAP`, default 20, on its own tracker).
|
|
77
|
+
The disposition of the old Phase 3 re-enable criteria — 1 and 2 retired as
|
|
78
|
+
inapplicable to a hosted handoff, 3 and 4 met — is recorded at
|
|
55
79
|
`docs/internal/roadmaps/phase-3-readiness.md`.
|
|
56
80
|
|
|
57
81
|
**Re-derive these counts rather than trusting them here** — the catalog in
|
|
58
82
|
`packages/metadata/src/mcp-tool-catalog.ts` is the source; free anonymous =
|
|
59
|
-
catalog − `OSS_DISABLED_TOOLS` − `tier: "pro"
|
|
83
|
+
catalog − `OSS_DISABLED_TOOLS` − `tier: "pro"` − any `addon` whose claim this
|
|
84
|
+
deployment does not hold.
|
|
60
85
|
|
|
61
|
-
Don't add identity logic to any tool signature. Don't
|
|
62
|
-
|
|
86
|
+
Don't add identity logic to any tool signature. Don't un-gate an `addon`-tagged
|
|
87
|
+
tool — the claim is a DEPLOYMENT property read from the licence, never a
|
|
88
|
+
caller-supplied argument, and every gate fails closed. Don't add a name to
|
|
89
|
+
`OSS_DISABLED_TOOLS` where an entitlement gate is what the tool actually
|
|
90
|
+
needs; and don't remove one from it without the review
|
|
91
|
+
`docs/internal/roadmaps/phase-3-readiness.md` describes.
|
|
63
92
|
|
|
64
93
|
## Tool implementation pattern
|
|
65
94
|
|
|
@@ -87,8 +116,12 @@ Example (lead routing):
|
|
|
87
116
|
- Don't add a tool without the matching `apps/api` endpoint.
|
|
88
117
|
- Don't put business logic in tool handlers — they're adapters.
|
|
89
118
|
|
|
90
|
-
Last updated: 2026-
|
|
119
|
+
Last updated: 2026-09-17 — #5971: `start_purchase` re-enabled behind the `commerce` licence claim. `OSS_DISABLED_TOOLS` is now empty; the registered count is 19 on a keyless deployment and 20 on a commerce-claimed one; the tool's input field is `productCode` (was `priceId`), matching its HTTP twin; a per-IP daily cap (`MCP_ABUSE_START_PURCHASE_DAILY_CAP`) covers it.
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
Prior: 2026-07-09 — corrected the tool-count explanation (18 catalog → 17 registered → 14 free anonymous) and the Pro-tool gating (registered + `requirePaidTier()`-gated, not `OSS_DISABLED_TOOLS`-disabled).
|
|
122
|
+
|
|
123
|
+
Last updated: 2026-09-17 — doc-sync: tool counts "23 catalog → 22 registered → 19 free anonymous" → "20 in TOOLS → 19 registered → 19 free anonymous", and the Pro-tools section rewritten to record that `create_page`/`update_page`/`delete_page` were RETIRED by #5853 rather than disabled (D7) — derived from `packages/metadata/src/mcp-tool-catalog.ts` and verified against `apps/mcp/src/server.ts`.
|
|
124
|
+
|
|
125
|
+
Prior: 2026-08-26 — doc-sync: tool-count explanation "18 catalog → 17 registered → 14 free anonymous" → "22 catalog → 21 registered → 18 free anonymous" (derived from `packages/metadata/src/mcp-tool-catalog.ts`: `get_flow`/`submit_flow_step` landed via PR #3973, `list_availability`/`create_booking` landed via PR #4421). Added the Booking category to the free-tool enumeration above. `CLAUDE.md` and `docs/ARCHITECTURE.md` re-verified as non-drift (already correct); `packages/create-ailk/templates/apps/mcp/CLAUDE.md` no longer exists in the scaffolder template tree (structure changed since the count was scoped there).
|
|
93
126
|
|
|
94
127
|
Last updated: 2026-08-31 — doc-sync: tool counts 22/21/18 → 23/22/19 and Booking (2) → Booking (3): `get_event_meta` added (#4633), the meta-only scheduling read twinning `GET /v1/schedule/event-meta` — derived from `packages/metadata/src/mcp-tool-catalog.ts`.
|
|
@@ -25,7 +25,10 @@ import {
|
|
|
25
25
|
entityMarkup,
|
|
26
26
|
faqHowtoIncentive,
|
|
27
27
|
jsonldValidity,
|
|
28
|
+
offerOnPricedPage,
|
|
29
|
+
policiesPresent,
|
|
28
30
|
schemaCoverage,
|
|
31
|
+
taxCodeWhenStripeTax,
|
|
29
32
|
type Rule,
|
|
30
33
|
} from "@working-theory/aeo";
|
|
31
34
|
|
|
@@ -44,6 +47,9 @@ const RULES: Rule[] = [
|
|
|
44
47
|
jsonldValidity,
|
|
45
48
|
faqHowtoIncentive,
|
|
46
49
|
entityMarkup,
|
|
50
|
+
policiesPresent,
|
|
51
|
+
offerOnPricedPage,
|
|
52
|
+
taxCodeWhenStripeTax,
|
|
47
53
|
];
|
|
48
54
|
|
|
49
55
|
const FIXTURE_SLUG = "home";
|