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
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
READINESS_SCAN_TYPES,
|
|
21
|
+
type AuditInput,
|
|
22
|
+
type ReadinessPageFacts,
|
|
23
|
+
} from "@working-theory/aeo";
|
|
24
|
+
import {
|
|
25
|
+
type ContentAdapter,
|
|
26
|
+
type ContentPage,
|
|
27
|
+
type SchemaOrgType,
|
|
28
|
+
} from "@working-theory/content-adapters";
|
|
21
29
|
import { buildJsonLd, pageTypeRegistry } from "@working-theory/schema";
|
|
22
30
|
|
|
31
|
+
import { getMcpContentAdapter } from "./content-adapter.js";
|
|
23
32
|
import { resolveLocale } from "./resolve-locale.js";
|
|
33
|
+
import { readStripeTaxSignal } from "./stripe-tax-signal.js";
|
|
24
34
|
|
|
25
35
|
export interface SlugInput {
|
|
26
36
|
slug: string;
|
|
@@ -121,15 +131,86 @@ function extractTypeSlugFromUrl(raw: string): {
|
|
|
121
131
|
return { type, slug };
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The site-level view the readiness rules grade (#5949 FR9/D5).
|
|
136
|
+
*
|
|
137
|
+
* Five adapter calls, bounded and type-closed: the collection-style pair for
|
|
138
|
+
* `Product` / `Service`, the landing-style pair for the three policy types.
|
|
139
|
+
* Independent of site size and of which page was audited (NFR4). The five
|
|
140
|
+
* are independent of each other, so they run concurrently (#6012).
|
|
141
|
+
*
|
|
142
|
+
* Fail-open-to-pass, stated: under an unresolved adapter every list is `[]`,
|
|
143
|
+
* `sitePages` is empty, and all three rules pass — a site with no known priced
|
|
144
|
+
* page owes no policy page, which is the correct verdict for "the audit could
|
|
145
|
+
* not see the site". A type the adapter legitimately has no pages of resolves
|
|
146
|
+
* this way too, silently — it is not an error.
|
|
147
|
+
*
|
|
148
|
+
* A GENUINE adapter failure (the call throws) is a different condition from
|
|
149
|
+
* that silent "no pages of this type" case, and collapsing the two loses the
|
|
150
|
+
* distinction: never a throw that would take down an audit of an unrelated
|
|
151
|
+
* page, but the failure is surfaced rather than vanishing indistinguishably
|
|
152
|
+
* into the same vacuous pass (#6012, from the #6010 review).
|
|
153
|
+
*
|
|
154
|
+
* Logged via `console.error`, NOT `console.debug`/`console.log` — on the
|
|
155
|
+
* default stdio MCP transport (`apps/mcp/src/index.ts`), stdout IS the
|
|
156
|
+
* JSON-RPC framing channel, and `console.debug`/`console.log` both write
|
|
157
|
+
* there (they are the same stream in Node). `console.error` (stderr) is the
|
|
158
|
+
* convention this file's own bootstrap already uses for exactly this reason
|
|
159
|
+
* (`index.ts:43,63`) — writing anywhere else would corrupt the protocol
|
|
160
|
+
* stream for a mere adapter hiccup on one readiness-scan type.
|
|
161
|
+
*/
|
|
162
|
+
async function scanSitePages(
|
|
163
|
+
adapter: ContentAdapter,
|
|
164
|
+
locale: string,
|
|
165
|
+
): Promise<ReadinessPageFacts[]> {
|
|
166
|
+
const perType = await Promise.all(
|
|
167
|
+
READINESS_SCAN_TYPES.map(async (type) => {
|
|
168
|
+
const isDetail = type === "Product" || type === "Service";
|
|
169
|
+
try {
|
|
170
|
+
const pages = isDetail
|
|
171
|
+
? await adapter.listDetail(type, locale)
|
|
172
|
+
: await adapter.listPages(type, locale);
|
|
173
|
+
return { type, pages };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(
|
|
176
|
+
`[aeo] readiness scan: adapter threw listing type "${type}" — ` +
|
|
177
|
+
"treating as no pages for it",
|
|
178
|
+
err,
|
|
179
|
+
);
|
|
180
|
+
return { type, pages: [] as ContentPage[] };
|
|
181
|
+
}
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const facts: ReadinessPageFacts[] = [];
|
|
186
|
+
for (const { type, pages } of perType) {
|
|
187
|
+
for (const page of pages) {
|
|
188
|
+
facts.push({
|
|
189
|
+
slug: page.slug,
|
|
190
|
+
type,
|
|
191
|
+
...(page.price !== undefined && { price: page.price }),
|
|
192
|
+
...(page.stripe?.taxCode !== undefined && {
|
|
193
|
+
taxCode: page.stripe.taxCode,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return facts;
|
|
199
|
+
}
|
|
200
|
+
|
|
124
201
|
/**
|
|
125
202
|
* Builds an `AuditInput` for a single page.
|
|
126
203
|
*
|
|
127
204
|
* Accepts an optional `contentRoot` override for testing. When omitted,
|
|
128
205
|
* resolves the content root relative to cwd (supports ESM runtime).
|
|
206
|
+
*
|
|
207
|
+
* `projectRoot` is the matching seam for the project.yaml the Stripe-Tax signal
|
|
208
|
+
* is read from (#5949 D9); it likewise defaults to the process cwd.
|
|
129
209
|
*/
|
|
130
210
|
export async function buildSinglePageAuditInput(
|
|
131
211
|
input: SinglePageInput,
|
|
132
212
|
contentRoot?: string,
|
|
213
|
+
projectRoot?: string,
|
|
133
214
|
): Promise<AuditInput> {
|
|
134
215
|
// Validate: exactly one of slug or url, not both, not neither.
|
|
135
216
|
const hasSlug = "slug" in input;
|
|
@@ -146,7 +227,7 @@ export async function buildSinglePageAuditInput(
|
|
|
146
227
|
|
|
147
228
|
const root = contentRoot ?? join(process.cwd(), "content");
|
|
148
229
|
const locale = resolveLocale(input.locale);
|
|
149
|
-
const adapter =
|
|
230
|
+
const adapter = getMcpContentAdapter({ contentRoot: root, locale });
|
|
150
231
|
|
|
151
232
|
let type: SchemaOrgType;
|
|
152
233
|
let slug: string;
|
|
@@ -184,11 +265,17 @@ export async function buildSinglePageAuditInput(
|
|
|
184
265
|
// This mirrors packages/aeo/src/cli/build-input.ts behavior.
|
|
185
266
|
}
|
|
186
267
|
|
|
268
|
+
const stripeTaxEnabled = readStripeTaxSignal(projectRoot ?? process.cwd());
|
|
269
|
+
|
|
187
270
|
return {
|
|
188
271
|
pages: [{ slug, type, locale }],
|
|
189
272
|
locale,
|
|
190
273
|
identityMode: DEFAULT_IDENTITY_MODE,
|
|
191
274
|
registry: pageTypeRegistry,
|
|
192
275
|
jsonLdByPage,
|
|
276
|
+
readiness: {
|
|
277
|
+
sitePages: await scanSitePages(adapter, locale),
|
|
278
|
+
...(stripeTaxEnabled !== undefined && { stripeTaxEnabled }),
|
|
279
|
+
},
|
|
193
280
|
};
|
|
194
281
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment's `commerce` add-on claim, resolved ONCE at boot and read
|
|
3
|
+
* synchronously thereafter (#5971 spec D2).
|
|
4
|
+
*
|
|
5
|
+
* Why memoised: `createMcpServer()` and `listRegisteredTools()`
|
|
6
|
+
* (`apps/mcp/src/server.ts`) are synchronous and must stay that way —
|
|
7
|
+
* `checkLicense()` is not. `lib/public-workspace.ts` (#5876) already solved
|
|
8
|
+
* this exact shape for the content adapter, and this file follows it
|
|
9
|
+
* verbatim rather than inventing a second idiom: `apps/mcp/src/index.ts`
|
|
10
|
+
* awaits the initialiser once, before either transport branch, and every
|
|
11
|
+
* reader takes the memoised value.
|
|
12
|
+
*
|
|
13
|
+
* Why TWO states where `apps/api/src/lib/require-commerce-claim.ts` has
|
|
14
|
+
* three: that file's `undetermined` arm exists for exactly one caller — the
|
|
15
|
+
* Stripe webhook receiver, which must hold an event with a retryable 503
|
|
16
|
+
* rather than drop a charge that already moved. `start_purchase` is the twin
|
|
17
|
+
* of the checkout LAUNCHER, not the webhook, and the launcher answers the
|
|
18
|
+
* module-disabled response on both non-serving states because a checkout that
|
|
19
|
+
* cannot be started loses nothing. So `undetermined` collapses into
|
|
20
|
+
* `unclaimed` at the boundary, and this gate needs neither the stored-JWT read
|
|
21
|
+
* nor the env-presence read the API twin carries.
|
|
22
|
+
*
|
|
23
|
+
* Fail closed (NFR-3): the initial state is `"unclaimed"`, and every path that
|
|
24
|
+
* cannot POSITIVELY establish the claim — an unresolved boot, a thrown
|
|
25
|
+
* `checkLicense()`, a revoked licence, an expired or unreadable one — leaves
|
|
26
|
+
* it that way.
|
|
27
|
+
*
|
|
28
|
+
* `apps/*` may not import `apps/*`, so the small duplication of the API-side
|
|
29
|
+
* gate is the same shape `apps/mcp/src/config/modules.ts` already documents
|
|
30
|
+
* for `isModuleEnabled`.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { checkLicense } from "@working-theory/license";
|
|
34
|
+
|
|
35
|
+
/** The licence `features[]` claim that turns the commerce add-on on — the
|
|
36
|
+
* same literal `apps/api/src/lib/require-commerce-claim.ts` reads. */
|
|
37
|
+
const COMMERCE_FEATURE = "commerce";
|
|
38
|
+
|
|
39
|
+
/** Two states: the claim is positively held, or it is not. */
|
|
40
|
+
export type McpCommerceClaim = "claimed" | "unclaimed";
|
|
41
|
+
|
|
42
|
+
let claim: McpCommerceClaim = "unclaimed";
|
|
43
|
+
|
|
44
|
+
export interface InitMcpCommerceClaimOptions {
|
|
45
|
+
/** Injection seam for tests — when supplied, used in place of the real
|
|
46
|
+
* `checkLicense()` so no suite reaches a licence server (NFR-6). */
|
|
47
|
+
checkLicense?: typeof checkLicense;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the deployment's commerce claim, once, and memoises it for
|
|
52
|
+
* {@link mcpCommerceClaim}'s synchronous read. Safe to call more than once
|
|
53
|
+
* (each call re-resolves and re-memoises); production calls it exactly once,
|
|
54
|
+
* at bootstrap (`index.ts`).
|
|
55
|
+
*
|
|
56
|
+
* Never throws: a licence read that fails resolves to the fail-closed
|
|
57
|
+
* `"unclaimed"` rather than propagating.
|
|
58
|
+
*/
|
|
59
|
+
export async function initMcpCommerceClaim(
|
|
60
|
+
options: InitMcpCommerceClaimOptions = {},
|
|
61
|
+
): Promise<McpCommerceClaim> {
|
|
62
|
+
const read = options.checkLicense ?? checkLicense;
|
|
63
|
+
try {
|
|
64
|
+
const result = await read();
|
|
65
|
+
// Revoked paid licences keep verifying until JWT exp (up to 24h after the
|
|
66
|
+
// server reports `revoked`) — the same window `lib/require-paid-tier.ts`
|
|
67
|
+
// closes. See packages/license/src/types.ts § LicenseResult, ADR 0014.
|
|
68
|
+
claim =
|
|
69
|
+
result.revoked !== true && result.features.includes(COMMERCE_FEATURE)
|
|
70
|
+
? "claimed"
|
|
71
|
+
: "unclaimed";
|
|
72
|
+
} catch {
|
|
73
|
+
claim = "unclaimed";
|
|
74
|
+
}
|
|
75
|
+
return claim;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The memoised claim, read synchronously by both gates in `server.ts` and
|
|
79
|
+
* by `tools/purchase.ts`'s call-time re-check. */
|
|
80
|
+
export function mcpCommerceClaim(): McpCommerceClaim {
|
|
81
|
+
return claim;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Test-only injector — sets the memoised claim directly, bypassing the
|
|
85
|
+
* licence read entirely. The seam every claim-state matrix in
|
|
86
|
+
* `apps/mcp/__tests__` drives both halves through. */
|
|
87
|
+
export function __setMcpCommerceClaimForTest(next: McpCommerceClaim): void {
|
|
88
|
+
claim = next;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Test-only reset — returns to the module's initial, fail-closed state. */
|
|
92
|
+
export function resetMcpCommerceClaim(): void {
|
|
93
|
+
claim = "unclaimed";
|
|
94
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one content-adapter seam for apps/mcp (#5805).
|
|
3
|
+
*
|
|
4
|
+
* Adapter selection is NOT decided here — it is `resolveContentAdapter()`'s job
|
|
5
|
+
* (#5804, roadmap D3: one resolver, one env var). This module supplies the MCP
|
|
6
|
+
* server's own inputs (content root, resolved locale) and nothing else, so the
|
|
7
|
+
* twelve content tools and `audit-single-page` no longer each build their own
|
|
8
|
+
* MDX adapter against their own root. Mirrors
|
|
9
|
+
* `apps/api/src/lib/content-adapter.ts`, which adopted the resolver in #5827.
|
|
10
|
+
*
|
|
11
|
+
* `CONTENT_ADAPTER` selects: `mdx` (default) | `projection` | `database`.
|
|
12
|
+
* `CONTENT_BACKEND` is read for one more release by the resolver's shim.
|
|
13
|
+
*
|
|
14
|
+
* `CONTENT_ADAPTER=database` (#5876 spec D2/D4/D5/D6/D13). The `mdx` and
|
|
15
|
+
* `projection` paths below are byte-identical to before this issue — no
|
|
16
|
+
* `workspaceId`, no `await import()`, no workspace read (D6, NFR-1/NFR-2). On
|
|
17
|
+
* `database`, this seam reads `mcpPublicWorkspace()` — the resolution
|
|
18
|
+
* `apps/mcp/src/index.ts` already awaited once at bootstrap
|
|
19
|
+
* (`public-workspace.ts`, D4) — and forwards `workspaceId` + `prisma` to the
|
|
20
|
+
* resolver on a resolved deployment; every other arm (unresolved, or a tool
|
|
21
|
+
* invoked before bootstrap resolved) returns the fail-closed
|
|
22
|
+
* `emptyContentAdapter()` (D5), never a client-supplied workspace, never a
|
|
23
|
+
* throw. On BOTH arms the returned adapter is READ-ONLY under `database`:
|
|
24
|
+
* `createPage`/`updatePage`/`deletePage`/`getPageVersion` throw the named
|
|
25
|
+
* write error (D13) — this seam also serves `apps/mcp/src/tools/create_page.ts`
|
|
26
|
+
* et al. when they exist, so the boundary is held here, not per tool.
|
|
27
|
+
*
|
|
28
|
+
* The `contentRoot` argument is the test-override seam every tool already
|
|
29
|
+
* exposes as its optional second parameter — tests inject a fixture root
|
|
30
|
+
* through it, and that keeps working unchanged.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
resolveContentAdapter,
|
|
37
|
+
resolvedAdapterKind,
|
|
38
|
+
type ContentAdapter,
|
|
39
|
+
type MdxContentAdapter,
|
|
40
|
+
} from "@working-theory/content-adapters";
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
emptyContentAdapter,
|
|
44
|
+
readOnlyContentAdapter,
|
|
45
|
+
} from "./empty-content-adapter.js";
|
|
46
|
+
import { mcpPublicWorkspace } from "./public-workspace.js";
|
|
47
|
+
|
|
48
|
+
export interface McpContentAdapterOptions {
|
|
49
|
+
/** Filesystem content root. Defaults to `<cwd>/content` — the tools' prior default. */
|
|
50
|
+
contentRoot?: string;
|
|
51
|
+
/** Locale already resolved against the project allowlist (`resolveLocale()`). */
|
|
52
|
+
locale?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the `ContentAdapter` the environment selects, rooted at the caller's
|
|
57
|
+
* content root. The resolver memoises per distinct option set, so this is safe
|
|
58
|
+
* to call on every tool invocation.
|
|
59
|
+
*/
|
|
60
|
+
export function getMcpContentAdapter(
|
|
61
|
+
options: McpContentAdapterOptions = {},
|
|
62
|
+
): ContentAdapter {
|
|
63
|
+
const contentRoot = options.contentRoot ?? join(process.cwd(), "content");
|
|
64
|
+
const defaultLocale = options.locale;
|
|
65
|
+
|
|
66
|
+
if (resolvedAdapterKind() !== "database") {
|
|
67
|
+
return resolveContentAdapter({ contentRoot, defaultLocale });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const workspace = mcpPublicWorkspace();
|
|
71
|
+
if (workspace.kind !== "resolved") {
|
|
72
|
+
return emptyContentAdapter();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const adapter = resolveContentAdapter({
|
|
76
|
+
workspaceId: workspace.workspaceId,
|
|
77
|
+
prisma: workspace.prisma,
|
|
78
|
+
contentRoot,
|
|
79
|
+
defaultLocale,
|
|
80
|
+
});
|
|
81
|
+
// Under CONTENT_ADAPTER=database, resolveContentAdapter() always builds a
|
|
82
|
+
// DatabaseContentAdapter (packages/content-adapters/src/database/adapter.ts),
|
|
83
|
+
// which always exposes getPageSource — the read-only wrapper needs the
|
|
84
|
+
// MdxContentAdapter shape to pass the six reads + getPageSource through
|
|
85
|
+
// untouched and gate only the four write methods (D13).
|
|
86
|
+
return readOnlyContentAdapter(adapter as MdxContentAdapter);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The same resolution, narrowed to the MDX adapter's richer surface.
|
|
91
|
+
*
|
|
92
|
+
* `read_content` returns a page's raw MDX body, which only
|
|
93
|
+
* `MdxContentAdapter.getPageSource()` provides — the base `ContentAdapter`
|
|
94
|
+
* contract returns frontmatter only. Rather than cast, this asserts the
|
|
95
|
+
* capability on the adapter the resolver actually built, so a server pointed
|
|
96
|
+
* at a non-MDX `CONTENT_ADAPTER` gets a named error instead of a
|
|
97
|
+
* `getPageSource is not a function` TypeError. On the default `mdx` path the
|
|
98
|
+
* check always passes and behaviour is unchanged. Under `database`, both the
|
|
99
|
+
* fail-closed empty adapter and the read-only-wrapped database adapter also
|
|
100
|
+
* provide `getPageSource`, so the check passes there too (D5).
|
|
101
|
+
*/
|
|
102
|
+
export function getMcpMdxContentAdapter(
|
|
103
|
+
options: McpContentAdapterOptions = {},
|
|
104
|
+
): MdxContentAdapter {
|
|
105
|
+
const adapter = getMcpContentAdapter(options);
|
|
106
|
+
if (typeof (adapter as MdxContentAdapter).getPageSource !== "function") {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"This tool needs the MDX adapter's getPageSource(): the adapter selected by " +
|
|
109
|
+
"CONTENT_ADAPTER does not provide a raw MDX body. Use CONTENT_ADAPTER=mdx (the default).",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return adapter as MdxContentAdapter;
|
|
113
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fail-closed EMPTY adapter (#5876 spec D5) — a null object that holds
|
|
3
|
+
* no client and issues no query. `getMcpContentAdapter()` (`content-adapter.ts`)
|
|
4
|
+
* returns it whenever the environment selects `CONTENT_ADAPTER=database` and
|
|
5
|
+
* the deployment's workspace resolution (`public-workspace.ts`) did not land
|
|
6
|
+
* at `{ kind: "resolved" }`.
|
|
7
|
+
*
|
|
8
|
+
* All eleven `MdxContentAdapter` methods are specified, not left to throw a
|
|
9
|
+
* generic TypeError: the six reads plus `getPageSource` return their own
|
|
10
|
+
* not-found/empty shape, and the four write methods throw the SAME named
|
|
11
|
+
* error `readOnlyContentAdapter()` (below) throws on the RESOLVED arm
|
|
12
|
+
* (#5876 spec D13) — the seam is read-only under `CONTENT_ADAPTER=database`
|
|
13
|
+
* on both arms, not only this fail-closed one.
|
|
14
|
+
*
|
|
15
|
+
* Three properties follow for free from this shape, over the rejected
|
|
16
|
+
* alternatives (throwing the resolver's own "workspaceId required" error, or
|
|
17
|
+
* returning `null` from the seam and guarding per tool):
|
|
18
|
+
*
|
|
19
|
+
* 1. Every content tool's own not-found/empty mapping fires with NO tool
|
|
20
|
+
* edit — `read_content` and `get_*` already treat `null` as not-found;
|
|
21
|
+
* `list_pages` and `search_content` already treat `[]` as empty.
|
|
22
|
+
* 2. "Before any content lookup" (NFR-5) is structural, not asserted —
|
|
23
|
+
* there is nothing here to look anything up in.
|
|
24
|
+
* 3. `getMcpMdxContentAdapter()`'s `typeof getPageSource === "function"`
|
|
25
|
+
* capability check (`content-adapter.ts`) passes, so `read_content`
|
|
26
|
+
* never sees THAT named throw on the fail-closed path.
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
ContentPage,
|
|
30
|
+
ExtractionType,
|
|
31
|
+
MdxContentAdapter,
|
|
32
|
+
MdxPageSource,
|
|
33
|
+
SchemaOrgType,
|
|
34
|
+
SearchOptions,
|
|
35
|
+
SearchResult,
|
|
36
|
+
StructuredDataFor,
|
|
37
|
+
} from "@working-theory/content-adapters";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one message every write method throws, on BOTH the empty adapter and
|
|
41
|
+
* the read-only wrapper of a resolved database adapter — the seam is
|
|
42
|
+
* read-only under `CONTENT_ADAPTER=database`, full stop (#5876 spec D13).
|
|
43
|
+
* The three write MCP tools this boundary was drawn against
|
|
44
|
+
* (`create_page`/`update_page`/`delete_page`) were retired ahead of this
|
|
45
|
+
* issue (#5853); this is the seam's own STOP boundary, held independent of
|
|
46
|
+
* which tools currently reach it.
|
|
47
|
+
*/
|
|
48
|
+
const WRITE_NOT_SUPPORTED_MESSAGE =
|
|
49
|
+
"This MCP server's CONTENT_ADAPTER=database seam is read-only: " +
|
|
50
|
+
"createPage/updatePage/deletePage/getPageVersion are not available here. " +
|
|
51
|
+
"Content writes go through apps/api's tenant-guarded workspace content routes.";
|
|
52
|
+
|
|
53
|
+
function throwWriteNotSupported(): never {
|
|
54
|
+
throw new Error(WRITE_NOT_SUPPORTED_MESSAGE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A null object satisfying `MdxContentAdapter`. Holds no client; every read
|
|
59
|
+
* method resolves synchronously to its empty shape with no I/O.
|
|
60
|
+
*/
|
|
61
|
+
export function emptyContentAdapter(): MdxContentAdapter {
|
|
62
|
+
const adapter: MdxContentAdapter = {
|
|
63
|
+
async getPageBySlug(
|
|
64
|
+
_type: SchemaOrgType,
|
|
65
|
+
_slug: string,
|
|
66
|
+
_locale?: string,
|
|
67
|
+
): Promise<ContentPage | null> {
|
|
68
|
+
return null;
|
|
69
|
+
},
|
|
70
|
+
async listPages(
|
|
71
|
+
_type: SchemaOrgType,
|
|
72
|
+
_locale?: string,
|
|
73
|
+
): Promise<ContentPage[]> {
|
|
74
|
+
return [];
|
|
75
|
+
},
|
|
76
|
+
async getDetailBySlug(
|
|
77
|
+
_type: SchemaOrgType,
|
|
78
|
+
_slug: string,
|
|
79
|
+
_locale?: string,
|
|
80
|
+
): Promise<ContentPage | null> {
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
async listDetail(
|
|
84
|
+
_type: SchemaOrgType,
|
|
85
|
+
_locale?: string,
|
|
86
|
+
): Promise<ContentPage[]> {
|
|
87
|
+
return [];
|
|
88
|
+
},
|
|
89
|
+
async searchPages(
|
|
90
|
+
_query: string,
|
|
91
|
+
_options?: SearchOptions,
|
|
92
|
+
): Promise<SearchResult[]> {
|
|
93
|
+
return [];
|
|
94
|
+
},
|
|
95
|
+
async extractStructuredData<T extends ExtractionType>(
|
|
96
|
+
_type: T,
|
|
97
|
+
_slug: string,
|
|
98
|
+
_locale?: string,
|
|
99
|
+
): Promise<StructuredDataFor<T> | null> {
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
async getPageSource(
|
|
103
|
+
_type: SchemaOrgType,
|
|
104
|
+
_slug: string,
|
|
105
|
+
_locale?: string,
|
|
106
|
+
): Promise<MdxPageSource | null> {
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
createPage: throwWriteNotSupported,
|
|
110
|
+
updatePage: throwWriteNotSupported,
|
|
111
|
+
deletePage: throwWriteNotSupported,
|
|
112
|
+
getPageVersion: throwWriteNotSupported,
|
|
113
|
+
};
|
|
114
|
+
return adapter;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wraps a RESOLVED database adapter so its four write methods throw the same
|
|
119
|
+
* read-only error the empty adapter throws — the seam is read-only under
|
|
120
|
+
* `CONTENT_ADAPTER=database` on both arms (#5876 spec D13). The six read
|
|
121
|
+
* methods and `getPageSource` pass through untouched.
|
|
122
|
+
*/
|
|
123
|
+
export function readOnlyContentAdapter(
|
|
124
|
+
adapter: MdxContentAdapter,
|
|
125
|
+
): MdxContentAdapter {
|
|
126
|
+
return {
|
|
127
|
+
getPageBySlug: adapter.getPageBySlug.bind(adapter),
|
|
128
|
+
listPages: adapter.listPages.bind(adapter),
|
|
129
|
+
getDetailBySlug: adapter.getDetailBySlug.bind(adapter),
|
|
130
|
+
listDetail: adapter.listDetail.bind(adapter),
|
|
131
|
+
searchPages: adapter.searchPages.bind(adapter),
|
|
132
|
+
extractStructuredData: adapter.extractStructuredData.bind(adapter),
|
|
133
|
+
getPageSource: adapter.getPageSource.bind(adapter),
|
|
134
|
+
createPage: throwWriteNotSupported,
|
|
135
|
+
updatePage: throwWriteNotSupported,
|
|
136
|
+
deletePage: throwWriteNotSupported,
|
|
137
|
+
getPageVersion: throwWriteNotSupported,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy Prisma client singleton for apps/mcp's `CONTENT_ADAPTER=database` path
|
|
3
|
+
* (#5876 spec D7).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT the eager, module-scope-construction shape of
|
|
6
|
+
* `apps/api/src/lib/prisma.ts` / `apps/web/lib/prisma-for-auth.ts`: this
|
|
7
|
+
* module is reached only from `initMcpPublicWorkspace()`'s `database` branch
|
|
8
|
+
* (`public-workspace.ts`), via `await import("./prisma.js")`, and only when
|
|
9
|
+
* the caller supplied no `prisma` of its own — the injection seam every test
|
|
10
|
+
* uses instead (FR-1). Constructing at MODULE scope — with a
|
|
11
|
+
* `connectionString` that is `undefined` on every MDX-only deployment (the
|
|
12
|
+
* 350 pre-existing apps/mcp tests set no `DATABASE_URL`) — would break the
|
|
13
|
+
* no-`DATABASE_URL` scaffold the instant this file was imported anywhere,
|
|
14
|
+
* even transitively. Construction happens on first CALL and is memoised
|
|
15
|
+
* after that, mirroring #5852's identical deferral of the apps/api client
|
|
16
|
+
* (`apps/api/src/services/public-site-resolver.ts` § D6).
|
|
17
|
+
*/
|
|
18
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
19
|
+
import { PrismaClient } from "@working-theory/database";
|
|
20
|
+
|
|
21
|
+
let client: PrismaClient | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The process-wide lazy Prisma client for the MCP server's database path.
|
|
25
|
+
* Constructs on first call; every subsequent call returns the same instance.
|
|
26
|
+
*/
|
|
27
|
+
export function getMcpPrismaClient(): PrismaClient {
|
|
28
|
+
if (client === undefined) {
|
|
29
|
+
const adapter = new PrismaPg({
|
|
30
|
+
connectionString: process.env.DATABASE_URL,
|
|
31
|
+
});
|
|
32
|
+
client = new PrismaClient({
|
|
33
|
+
adapter,
|
|
34
|
+
log:
|
|
35
|
+
process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server's own workspace resolution (#5876 spec D2/D4/D6/D7) — which
|
|
3
|
+
* workspace `CONTENT_ADAPTER=database` content reads serve, resolved ONCE at
|
|
4
|
+
* bootstrap and read synchronously thereafter by `content-adapter.ts`.
|
|
5
|
+
*
|
|
6
|
+
* WHY RESOLUTION HAPPENS AT BOOTSTRAP, NOT PER CALL (D4). Workspace
|
|
7
|
+
* resolution is I/O (a `workspace.count()` + `findUnique()`), but
|
|
8
|
+
* `getMcpContentAdapter()` stays SYNCHRONOUS — every one of its thirteen
|
|
9
|
+
* call sites (nine read tools, `audit-single-page.ts`, and the three write
|
|
10
|
+
* tools) calls it without `await`. Making it async would force `await` at
|
|
11
|
+
* every one of those sites, a needless cross-cutting edit to a hard-floor
|
|
12
|
+
* surface (`apps/mcp/src/tools/**`). So `apps/mcp/src/index.ts` awaits
|
|
13
|
+
* `initMcpPublicWorkspace()` once, before either transport branch, and the
|
|
14
|
+
* result is memoised here for `mcpPublicWorkspace()`'s synchronous read.
|
|
15
|
+
*
|
|
16
|
+
* A tool invoked before `initMcpPublicWorkspace()` has resolved sees the
|
|
17
|
+
* module's initial `{ kind: "not-required" }` state — which the seam reads
|
|
18
|
+
* exactly like an unresolved deployment under `database` mode (the empty
|
|
19
|
+
* adapter, never content). That is the accepted, safe-direction cost D4
|
|
20
|
+
* names, and it's what keeps the 350 pre-existing tests — which never call
|
|
21
|
+
* `init` — on the byte-identical MDX path.
|
|
22
|
+
*
|
|
23
|
+
* NO-OP UNLESS THE ENVIRONMENT SELECTS `database` (D6). On `mdx` and
|
|
24
|
+
* `projection`, `initMcpPublicWorkspace()`'s first statement is the same
|
|
25
|
+
* `resolvedAdapterKind()` test the seam itself uses, and it returns before
|
|
26
|
+
* any `await import()` — no database round-trip, no `DATABASE_URL` read, on
|
|
27
|
+
* every MDX-only deployment.
|
|
28
|
+
*
|
|
29
|
+
* THE ONE-WORKSPACE PREDICATE IS SHARED, NOT REIMPLEMENTED (D2). This module
|
|
30
|
+
* issues no direct tenant-model query of its own — it delegates to
|
|
31
|
+
* `@working-theory/database/tenancy`'s `resolveDefaultWorkspaceRow`, the same
|
|
32
|
+
* predicate `apps/api/src/services/default-tenant.ts` re-points at. One
|
|
33
|
+
* predicate, reached from two apps, never two predicates (NFR-7).
|
|
34
|
+
*
|
|
35
|
+
* THE LAZY CLIENT (D7). The `prisma` injection option is the seam AC-1/AC-2
|
|
36
|
+
* are scored through: when the caller supplies one, `./prisma.js` (the
|
|
37
|
+
* module that constructs a real `PrismaClient`) is never imported — which is
|
|
38
|
+
* what keeps every test in this repo's ordinary Jest run off a real
|
|
39
|
+
* database connection (NFR-6).
|
|
40
|
+
*/
|
|
41
|
+
import {
|
|
42
|
+
resolvedAdapterKind,
|
|
43
|
+
type ArtifactPrismaClient,
|
|
44
|
+
} from "@working-theory/content-adapters";
|
|
45
|
+
import {
|
|
46
|
+
resolveDefaultWorkspaceRow,
|
|
47
|
+
type WorkspacePredicateClient,
|
|
48
|
+
} from "@working-theory/database/tenancy";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The three answers the MCP server's own workspace resolution can reach.
|
|
52
|
+
* `reason` exists for the caller's own logging only — no MCP tool emits it
|
|
53
|
+
* to the (anonymous, unauthenticated) caller (NFR-4, mirroring
|
|
54
|
+
* `apps/api/src/services/public-site-resolver.ts`'s identical
|
|
55
|
+
* `PublicSiteResolution`). Unlike that HTTP twin, there is no `host-unmatched`
|
|
56
|
+
* arm here: an MCP stdio session carries no HTTP request and no host to
|
|
57
|
+
* mismatch against (§2.3 — a host→workspace branch is explicitly out of
|
|
58
|
+
* scope, #3635's to add).
|
|
59
|
+
*/
|
|
60
|
+
export type McpWorkspaceResolution =
|
|
61
|
+
| { kind: "not-required" }
|
|
62
|
+
| { kind: "resolved"; workspaceId: string; prisma: ArtifactPrismaClient }
|
|
63
|
+
| { kind: "unresolved"; reason: "ambiguous" };
|
|
64
|
+
|
|
65
|
+
export interface InitMcpPublicWorkspaceOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Injection seam for tests (D7, D11) — when supplied, `initMcpPublicWorkspace()`
|
|
68
|
+
* uses it and never loads `./prisma.js`. Typed as the narrow surface THIS
|
|
69
|
+
* function itself calls (`workspace.count`/`findUnique`); the real
|
|
70
|
+
* `PrismaClient` (or a caller-injected structural equivalent) is
|
|
71
|
+
* structurally wider and also satisfies `ArtifactPrismaClient`, which is
|
|
72
|
+
* what the seam later forwards to `resolveContentAdapter()`.
|
|
73
|
+
*/
|
|
74
|
+
prisma?: WorkspacePredicateClient;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let resolution: McpWorkspaceResolution = { kind: "not-required" };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the MCP server's workspace, once. Memoises the result for
|
|
81
|
+
* `mcpPublicWorkspace()`'s synchronous read. Safe to call more than once
|
|
82
|
+
* (each call re-resolves and re-memoises) — production calls it exactly once,
|
|
83
|
+
* at bootstrap (`index.ts`).
|
|
84
|
+
*/
|
|
85
|
+
export async function initMcpPublicWorkspace(
|
|
86
|
+
options: InitMcpPublicWorkspaceOptions = {},
|
|
87
|
+
): Promise<McpWorkspaceResolution> {
|
|
88
|
+
if (resolvedAdapterKind() !== "database") {
|
|
89
|
+
resolution = { kind: "not-required" };
|
|
90
|
+
return resolution;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Deferred — see this file's header (D7) and #5852's identical deferral of
|
|
94
|
+
// apps/api's own client (`services/public-site-resolver.ts`).
|
|
95
|
+
const prisma =
|
|
96
|
+
options.prisma ?? (await import("./prisma.js")).getMcpPrismaClient();
|
|
97
|
+
const workspace = await resolveDefaultWorkspaceRow(prisma);
|
|
98
|
+
resolution =
|
|
99
|
+
workspace === null
|
|
100
|
+
? { kind: "unresolved", reason: "ambiguous" }
|
|
101
|
+
: {
|
|
102
|
+
kind: "resolved",
|
|
103
|
+
workspaceId: workspace.id,
|
|
104
|
+
prisma: prisma as unknown as ArtifactPrismaClient,
|
|
105
|
+
};
|
|
106
|
+
return resolution;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The memoised resolution, read synchronously. `content-adapter.ts`'s
|
|
111
|
+
* `database` branch is the one caller in production; every arm other than
|
|
112
|
+
* `{ kind: "resolved" }` maps to the fail-closed empty adapter.
|
|
113
|
+
*/
|
|
114
|
+
export function mcpPublicWorkspace(): McpWorkspaceResolution {
|
|
115
|
+
return resolution;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Test-only injector — sets the memoised resolution directly, bypassing the
|
|
119
|
+
* tenancy predicate entirely. This is the seam `read_content.test.ts`,
|
|
120
|
+
* `list_pages.test.ts` and `fail-closed-tools.test.ts` use: they care about
|
|
121
|
+
* the CONTENT tools' behaviour under a given resolution, not about exercising
|
|
122
|
+
* the resolution logic itself (that's `public-workspace.test.ts`'s job, via
|
|
123
|
+
* the real `initMcpPublicWorkspace({ prisma: <stub> })`). */
|
|
124
|
+
export function __setMcpPublicWorkspaceForTest(
|
|
125
|
+
next: McpWorkspaceResolution,
|
|
126
|
+
): void {
|
|
127
|
+
resolution = next;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Test-only reset — returns to the module's initial, pre-bootstrap state. */
|
|
131
|
+
export function resetMcpPublicWorkspace(): void {
|
|
132
|
+
resolution = { kind: "not-required" };
|
|
133
|
+
}
|