create-ailk 0.22.0 → 0.24.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 +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -28,50 +28,36 @@
|
|
|
28
28
|
* composed from the deployment's `_site.mdx` identity (identity node, optional
|
|
29
29
|
* secondary, WebSite) rather than the page title. Every other type's body is
|
|
30
30
|
* the page's own JSON-LD, spread at the root, as before.
|
|
31
|
+
*
|
|
32
|
+
* #7169: the document is built by `readContentDocument` in
|
|
33
|
+
* `@working-theory/content-adapters` — the shared seam the `read_content` MCP
|
|
34
|
+
* tool answers through, parsing the same `@working-theory/validation` exports —
|
|
35
|
+
* over the `_site.mdx` this route still reads (the package may not import
|
|
36
|
+
* `@working-theory/site-config`).
|
|
31
37
|
*/
|
|
32
38
|
import { readFileSync } from "node:fs";
|
|
33
39
|
import { join } from "node:path";
|
|
34
40
|
|
|
35
41
|
import {
|
|
36
|
-
|
|
42
|
+
readContentDocument,
|
|
37
43
|
type ContentAdapter,
|
|
38
|
-
type ContentPage,
|
|
39
44
|
type MdxContentAdapter,
|
|
40
|
-
type MdxPageSource,
|
|
41
|
-
type SchemaOrgType,
|
|
42
45
|
} from "@working-theory/content-adapters";
|
|
43
|
-
import {
|
|
44
|
-
buildJsonLd,
|
|
45
|
-
composeGraph,
|
|
46
|
-
graphNodes,
|
|
47
|
-
type ContentPageInput,
|
|
48
|
-
type JsonLdObject,
|
|
49
|
-
type SitewideIdentityInput,
|
|
50
|
-
} from "@working-theory/schema";
|
|
51
46
|
import {
|
|
52
47
|
SiteConfigSchema,
|
|
53
48
|
type SiteConfig,
|
|
54
49
|
} from "@working-theory/site-config/schema";
|
|
50
|
+
import {
|
|
51
|
+
contentPageParamsSchema,
|
|
52
|
+
contentPageQuerySchema,
|
|
53
|
+
} from "@working-theory/validation";
|
|
55
54
|
import type { FastifyPluginAsync } from "fastify";
|
|
56
55
|
import { parse as parseYaml } from "yaml";
|
|
57
|
-
import { z } from "zod";
|
|
58
56
|
|
|
59
57
|
import { getContentRoot } from "../../lib/content-adapter.js";
|
|
60
58
|
import { resolveLocale } from "../../lib/locales.js";
|
|
61
59
|
import { resolvePublicContentAdapter } from "../../services/public-site-resolver.js";
|
|
62
60
|
|
|
63
|
-
import { SLUG_PATTERN } from "./slug.js";
|
|
64
|
-
|
|
65
|
-
const paramsSchema = z.object({
|
|
66
|
-
type: z.enum(SCHEMA_ORG_TYPES),
|
|
67
|
-
slug: z.string().regex(SLUG_PATTERN),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const querySchema = z.object({
|
|
71
|
-
locale: z.string().optional(),
|
|
72
|
-
include: z.enum(["body"]).optional(),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
61
|
/**
|
|
76
62
|
* The raw-body capability, probed by the exact name the MDX adapter declares
|
|
77
63
|
* and the database adapter borrows (#5874) — the same idiom, and the same
|
|
@@ -114,38 +100,6 @@ function readSiteConfig(contentRoot: string): SiteConfig | undefined {
|
|
|
114
100
|
return siteConfigByRoot.get(contentRoot) ?? undefined;
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
/**
|
|
118
|
-
* The `HomePage` body's `@graph` (#6597, spec D11): the spine plus the site's
|
|
119
|
-
* sitewide identity nodes. The WebSite's `inLanguage` is the spine's own, so
|
|
120
|
-
* both carry one value; the Organization falls back to the page's `image` as
|
|
121
|
-
* its `logo` when the identity declares none, as the title-derived node did.
|
|
122
|
-
*/
|
|
123
|
-
function homePageGraph(
|
|
124
|
-
primary: JsonLdObject,
|
|
125
|
-
image: string | undefined,
|
|
126
|
-
): { "@context": "https://schema.org"; "@graph": JsonLdObject[] } {
|
|
127
|
-
const config = readSiteConfig(getContentRoot());
|
|
128
|
-
let site: SitewideIdentityInput | undefined;
|
|
129
|
-
if (config !== undefined) {
|
|
130
|
-
const identity = config.siteIdentity;
|
|
131
|
-
site = {
|
|
132
|
-
origin: config.baseUrl.replace(/\/+$/, ""),
|
|
133
|
-
siteName: config.siteName,
|
|
134
|
-
baseUrl: config.baseUrl,
|
|
135
|
-
inLanguage:
|
|
136
|
-
typeof primary["inLanguage"] === "string" ? primary["inLanguage"] : undefined,
|
|
137
|
-
identity:
|
|
138
|
-
identity.kind !== "Person" && identity.logo === undefined && image !== undefined
|
|
139
|
-
? { ...identity, logo: image }
|
|
140
|
-
: identity,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
"@context": "https://schema.org",
|
|
145
|
-
"@graph": graphNodes(composeGraph({ primary, site, isHomePage: true })),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
103
|
export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
150
104
|
fastify.get<{
|
|
151
105
|
Params: { type: string; slug: string };
|
|
@@ -153,7 +107,7 @@ export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
153
107
|
}>(
|
|
154
108
|
"/v1/content/:type/:slug",
|
|
155
109
|
async (request, reply) => {
|
|
156
|
-
const parsed =
|
|
110
|
+
const parsed = contentPageParamsSchema.safeParse(request.params);
|
|
157
111
|
if (!parsed.success) {
|
|
158
112
|
return reply.status(400).send({
|
|
159
113
|
error: "Invalid type or slug",
|
|
@@ -161,7 +115,7 @@ export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
161
115
|
});
|
|
162
116
|
}
|
|
163
117
|
|
|
164
|
-
const queryParsed =
|
|
118
|
+
const queryParsed = contentPageQuerySchema.safeParse(request.query);
|
|
165
119
|
if (!queryParsed.success) {
|
|
166
120
|
return reply.status(400).send({
|
|
167
121
|
error: "Invalid query parameters",
|
|
@@ -191,74 +145,35 @@ export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
191
145
|
// Refused before any content query, so a caller asking for a body this
|
|
192
146
|
// deployment cannot serve is never charged a read whose result is then
|
|
193
147
|
// discarded.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
],
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
readSource = adapter.getPageSource.bind(adapter);
|
|
148
|
+
if (include === "body" && !hasPageSource(adapter)) {
|
|
149
|
+
return reply.status(400).send({
|
|
150
|
+
error: "Invalid query parameters",
|
|
151
|
+
details: [
|
|
152
|
+
{
|
|
153
|
+
path: ["include"],
|
|
154
|
+
message: "body is not available from this content adapter",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
208
158
|
}
|
|
209
159
|
|
|
210
|
-
// #5927: ONE adapter call supplies everything the response sends
|
|
211
|
-
// `
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
source = await readSource(type as SchemaOrgType, slug, locale);
|
|
221
|
-
page = source === null ? null : source.frontmatter;
|
|
222
|
-
} else {
|
|
223
|
-
page = await adapter.getPageBySlug(type as SchemaOrgType, slug, locale);
|
|
224
|
-
}
|
|
225
|
-
if (!page) {
|
|
160
|
+
// #5927: ONE adapter call supplies everything the response sends —
|
|
161
|
+
// the builder reads `getPageSource` only when `include=body`, and
|
|
162
|
+
// `getPageBySlug` alone otherwise, so the two halves can never
|
|
163
|
+
// straddle a publish that lands between two independent calls.
|
|
164
|
+
const document = await readContentDocument(
|
|
165
|
+
adapter,
|
|
166
|
+
{ type, slug, locale, include },
|
|
167
|
+
readSiteConfig(getContentRoot()),
|
|
168
|
+
);
|
|
169
|
+
if (!document) {
|
|
226
170
|
return reply.status(404).send({
|
|
227
171
|
error: "Page not found",
|
|
228
172
|
type,
|
|
229
173
|
slug,
|
|
230
174
|
});
|
|
231
175
|
}
|
|
232
|
-
|
|
233
|
-
// #8746 — the site's origin, from `_site.mdx`'s `baseUrl`, for a
|
|
234
|
-
// builder that needs an absolute URL (JobPosting's ApplyAction).
|
|
235
|
-
const origin = readSiteConfig(getContentRoot())?.baseUrl.replace(/\/+$/, "");
|
|
236
|
-
const input: ContentPageInput = {
|
|
237
|
-
type: page.type,
|
|
238
|
-
slug: page.slug,
|
|
239
|
-
title: page.title,
|
|
240
|
-
description: page.description,
|
|
241
|
-
...(origin !== undefined && { origin }),
|
|
242
|
-
...(page.publishedAt !== undefined && {
|
|
243
|
-
publishedAt: page.publishedAt,
|
|
244
|
-
}),
|
|
245
|
-
...(page.updatedAt !== undefined && { updatedAt: page.updatedAt }),
|
|
246
|
-
...(page.image !== undefined && { image: page.image }),
|
|
247
|
-
...(page.schema !== undefined && { schema: page.schema }),
|
|
248
|
-
// ADR-0026: carry the page's typed sections so recipes compose entity
|
|
249
|
-
// JSON-LD from real section data (e.g. FAQPage.mainEntity from `faq`
|
|
250
|
-
// sections) rather than a hand-rolled `schema` override. The adapter
|
|
251
|
-
// now preserves `sections`; before, it was stripped at validation.
|
|
252
|
-
...(page.sections !== undefined && { sections: page.sections }),
|
|
253
|
-
};
|
|
254
|
-
const primary = buildJsonLd(input);
|
|
255
|
-
const jsonLd =
|
|
256
|
-
page.type === "HomePage" ? homePageGraph(primary, page.image) : primary;
|
|
257
|
-
return reply.send({
|
|
258
|
-
...jsonLd,
|
|
259
|
-
page,
|
|
260
|
-
...(source && { body: source.body }),
|
|
261
|
-
});
|
|
176
|
+
return reply.send(document);
|
|
262
177
|
} catch (error) {
|
|
263
178
|
request.log.error({ err: error }, "[Content] getPage failed");
|
|
264
179
|
return reply.status(500).send({ error: "Internal server error" });
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public content API.
|
|
3
3
|
*
|
|
4
|
-
* Read endpoints, all GET,
|
|
4
|
+
* Read endpoints, all GET, JSON-LD-shaped at the document root unless noted:
|
|
5
5
|
* GET /v1/content/types — supported page types
|
|
6
6
|
* GET /v1/content/search — keyword search (#5831)
|
|
7
|
+
* GET /v1/content/offerings — Services + Products (#7177); answers the
|
|
8
|
+
* `{ ok, data }` envelope, not JSON-LD
|
|
9
|
+
* GET /v1/content/pricing — pricing tiers (#7178); answers the
|
|
10
|
+
* `{ ok, data }` envelope, not JSON-LD
|
|
11
|
+
* GET /v1/content/shown-offerings — the /pricing page's Offerings read
|
|
12
|
+
* (#8924); answers the { ok, data }
|
|
13
|
+
* envelope, not JSON-LD
|
|
7
14
|
* GET /v1/content/:type — paginated list of pages
|
|
8
15
|
* GET /v1/content/:type/:slug — single page with resolved JSON-LD
|
|
9
16
|
*
|
|
@@ -24,12 +31,19 @@ import type { FastifyPluginAsync } from "fastify";
|
|
|
24
31
|
import { getPageRoute } from "./get-page.js";
|
|
25
32
|
import { listPagesRoute } from "./list-pages.js";
|
|
26
33
|
import { listTypesRoute } from "./list-types.js";
|
|
34
|
+
import { offeringsRoute } from "./offerings.js";
|
|
35
|
+
import { pricingRoute } from "./pricing.js";
|
|
27
36
|
import { searchContentRoute } from "./search.js";
|
|
37
|
+
import { shownOfferingsRoute } from "./shown-offerings.js";
|
|
28
38
|
|
|
29
39
|
export const contentRoutes: FastifyPluginAsync = async (fastify) => {
|
|
30
40
|
await fastify.register(listTypesRoute);
|
|
31
|
-
// Before `listPagesRoute`: `search`
|
|
41
|
+
// Before `listPagesRoute`: `search`, `offerings`, `pricing` and
|
|
42
|
+
// `shown-offerings` must never be read as a `:type`.
|
|
32
43
|
await fastify.register(searchContentRoute);
|
|
44
|
+
await fastify.register(offeringsRoute);
|
|
45
|
+
await fastify.register(pricingRoute);
|
|
46
|
+
await fastify.register(shownOfferingsRoute);
|
|
33
47
|
await fastify.register(listPagesRoute);
|
|
34
48
|
await fastify.register(getPageRoute);
|
|
35
49
|
};
|
|
@@ -5,43 +5,32 @@
|
|
|
5
5
|
* pageSize 20; pageSize is capped at 100 to keep response payloads bounded.
|
|
6
6
|
*
|
|
7
7
|
* Response envelope is an `ItemList` JSON-LD document carrying the page
|
|
8
|
-
* frontmatter under `items` and pagination metadata alongside
|
|
9
|
-
*
|
|
8
|
+
* frontmatter under `items` and pagination metadata alongside, built by
|
|
9
|
+
* `listPagesItemList` in `@working-theory/content-adapters` — the shared seam
|
|
10
|
+
* the `list_pages` MCP tool answers through, parsing the same
|
|
11
|
+
* `@working-theory/validation` exports (#7168). A deployment whose site
|
|
12
|
+
* cannot be resolved (#5852) answers 404, not an empty list.
|
|
10
13
|
*
|
|
11
14
|
* Phase 3 S3: optional `?locale=<locale>` query parameter. Validated against
|
|
12
15
|
* `readLocales()` allowlist; invalid or unknown locale silently falls back to
|
|
13
16
|
* the project's default locale (AC-i18n-07).
|
|
14
17
|
*/
|
|
15
|
-
import {
|
|
18
|
+
import { listPagesItemList } from "@working-theory/content-adapters";
|
|
19
|
+
import {
|
|
20
|
+
contentListParamsSchema,
|
|
21
|
+
contentListQuerySchema,
|
|
22
|
+
} from "@working-theory/validation";
|
|
16
23
|
import type { FastifyPluginAsync } from "fastify";
|
|
17
|
-
import { z } from "zod";
|
|
18
24
|
|
|
19
25
|
import { resolveLocale } from "../../lib/locales.js";
|
|
20
26
|
import { resolvePublicContentAdapter } from "../../services/public-site-resolver.js";
|
|
21
27
|
|
|
22
|
-
const paramsSchema = z.object({
|
|
23
|
-
type: z.enum(SCHEMA_ORG_TYPES),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const PAGE_SIZE_MAX = 100;
|
|
27
|
-
|
|
28
|
-
const querySchema = z.object({
|
|
29
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
30
|
-
pageSize: z.coerce
|
|
31
|
-
.number()
|
|
32
|
-
.int()
|
|
33
|
-
.min(1)
|
|
34
|
-
.default(20)
|
|
35
|
-
.transform((v) => Math.min(v, PAGE_SIZE_MAX)),
|
|
36
|
-
locale: z.string().optional(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
28
|
export const listPagesRoute: FastifyPluginAsync = async (fastify) => {
|
|
40
29
|
fastify.get<{
|
|
41
30
|
Params: { type: string };
|
|
42
31
|
Querystring: { page?: string; pageSize?: string; locale?: string };
|
|
43
32
|
}>("/v1/content/:type", async (request, reply) => {
|
|
44
|
-
const params =
|
|
33
|
+
const params = contentListParamsSchema.safeParse(request.params);
|
|
45
34
|
if (!params.success) {
|
|
46
35
|
return reply.status(400).send({
|
|
47
36
|
error: "Invalid type",
|
|
@@ -49,7 +38,7 @@ export const listPagesRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
49
38
|
});
|
|
50
39
|
}
|
|
51
40
|
|
|
52
|
-
const query =
|
|
41
|
+
const query = contentListQuerySchema.safeParse(request.query);
|
|
53
42
|
if (!query.success) {
|
|
54
43
|
return reply.status(400).send({
|
|
55
44
|
error: "Invalid query parameters",
|
|
@@ -74,19 +63,9 @@ export const listPagesRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
74
63
|
});
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const start = (page - 1) * pageSize;
|
|
81
|
-
const items = all.slice(start, start + pageSize);
|
|
82
|
-
|
|
83
|
-
return reply.send({
|
|
84
|
-
"@context": "https://schema.org",
|
|
85
|
-
"@type": "ItemList",
|
|
86
|
-
type,
|
|
87
|
-
items,
|
|
88
|
-
pagination: { page, pageSize, totalItems, totalPages },
|
|
89
|
-
});
|
|
66
|
+
return reply.send(
|
|
67
|
+
await listPagesItemList(adapter, { type, page, pageSize, locale }),
|
|
68
|
+
);
|
|
90
69
|
} catch (error) {
|
|
91
70
|
request.log.error({ err: error }, "[Content] listPages failed");
|
|
92
71
|
return reply.status(500).send({ error: "Internal server error" });
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /v1/content/offerings
|
|
3
|
+
*
|
|
4
|
+
* The site's Services and Products as one offerings list (#7177). The HTTP
|
|
5
|
+
* twin of the `get_offerings` MCP tool: same arguments, parsed with the same
|
|
6
|
+
* `@working-theory/validation` schema, and the same result, produced by the
|
|
7
|
+
* same function — `listOfferings` in `@working-theory/content-adapters`, the
|
|
8
|
+
* shared seam the tool also answers through. That is the API ↔ MCP parity
|
|
9
|
+
* contract in the root CLAUDE.md; the parity guard's `get_offerings` row is a
|
|
10
|
+
* `twin` naming this route.
|
|
11
|
+
*
|
|
12
|
+
* Query:
|
|
13
|
+
* type optional — `service` or `product`; omit for both
|
|
14
|
+
* locale optional — BCP-47 (`xx` or `xx-XX`). A well-formed tag outside the
|
|
15
|
+
* project's allowlist silently falls back to the default locale
|
|
16
|
+
* (AC-i18n-07), matching every other content read; a malformed one
|
|
17
|
+
* is a 400, as the tool refuses it.
|
|
18
|
+
*
|
|
19
|
+
* Success: `{ ok: true, data: { offerings } }` — the API's documented
|
|
20
|
+
* envelope, not the JSON-LD `ItemList` the older content routes answer.
|
|
21
|
+
* Refusals: `400 validation_failed` (issues are not echoed), `404 not_found`
|
|
22
|
+
* when no public site resolves, `500 internal_error`.
|
|
23
|
+
*
|
|
24
|
+
* Registered BEFORE `/v1/content/:type` in `index.ts`. Fastify's radix router
|
|
25
|
+
* prefers a static segment over a parametric one regardless of order — the
|
|
26
|
+
* ordering is belt-and-braces, as it is for search.
|
|
27
|
+
*
|
|
28
|
+
* Rate limiting is inherited: `server.ts` registers the limiter GLOBALLY with
|
|
29
|
+
* the content ceiling, so this route is covered without one of its own.
|
|
30
|
+
*
|
|
31
|
+
* Which site this read serves is resolved BEFORE any content query (#5852)
|
|
32
|
+
* through the same `resolvePublicContentAdapter(request)` seam the sibling
|
|
33
|
+
* content reads use. The resolver reads `x-ailk-spoke-key`, so every answer
|
|
34
|
+
* past the query parse appends that header to `Vary`: a cache keyed on the
|
|
35
|
+
* URL alone must never share one site's answer — or one bad key's 404 — with
|
|
36
|
+
* another (SEC-7177-C). It appends rather than sets, because `@fastify/cors`
|
|
37
|
+
* has already written `Vary: Origin`.
|
|
38
|
+
*
|
|
39
|
+
* Offerings (#8933, spec D4): on a connected workspace with a current shown
|
|
40
|
+
* set, every read but `type=service` answers the Service pages followed by
|
|
41
|
+
* that set's Products — in place of the Product pages, since Stripe is that
|
|
42
|
+
* site's price authority — through `readShownOfferings` (#8924's read, the
|
|
43
|
+
* one code path every agent surface shares) and `listOfferingsWithShownSet`
|
|
44
|
+
* in `@working-theory/content-adapters`, the mapping the tool answers through
|
|
45
|
+
* too. Each shown Product carries `prices`: every price with its interval and
|
|
46
|
+
* full lookup key. `type=service`, and every workspace with no connection, no
|
|
47
|
+
* claim or no current set, answers page content exactly as before; a failed
|
|
48
|
+
* shown-set read takes the content path too. The read refreshes, or not,
|
|
49
|
+
* exactly as the shown-offerings route does (`options`, D5): never with the
|
|
50
|
+
* owner's override, never per request (T7).
|
|
51
|
+
*
|
|
52
|
+
* Anonymous and identity-blind: no `Authorization`, cookie, session or user
|
|
53
|
+
* id is read.
|
|
54
|
+
*/
|
|
55
|
+
import {
|
|
56
|
+
currentShownSet,
|
|
57
|
+
listOfferings,
|
|
58
|
+
listOfferingsWithShownSet,
|
|
59
|
+
type ContentAdapter,
|
|
60
|
+
type ListOfferingsOptions,
|
|
61
|
+
} from "@working-theory/content-adapters";
|
|
62
|
+
import {
|
|
63
|
+
getOfferingsInputSchema,
|
|
64
|
+
type GetOfferingsOutput,
|
|
65
|
+
} from "@working-theory/validation";
|
|
66
|
+
import type {
|
|
67
|
+
FastifyPluginAsync,
|
|
68
|
+
FastifyReply,
|
|
69
|
+
FastifyRequest,
|
|
70
|
+
} from "fastify";
|
|
71
|
+
|
|
72
|
+
import { resolveLocale } from "../../lib/locales.js";
|
|
73
|
+
import { resolvePublicContentAdapter } from "../../services/public-site-resolver.js";
|
|
74
|
+
|
|
75
|
+
import {
|
|
76
|
+
readShownOfferings,
|
|
77
|
+
type ShownOfferingsRouteOptions,
|
|
78
|
+
} from "./shown-offerings.js";
|
|
79
|
+
|
|
80
|
+
/** The request header the resolver picks the site's workspace from. */
|
|
81
|
+
const SPOKE_KEY_HEADER = "x-ailk-spoke-key";
|
|
82
|
+
|
|
83
|
+
/** Adds the spoke-key header to `Vary`, keeping every value already there. */
|
|
84
|
+
function varyOnSpokeKey(reply: FastifyReply): void {
|
|
85
|
+
const existing = reply.getHeader("vary");
|
|
86
|
+
const values = (
|
|
87
|
+
Array.isArray(existing) ? existing.join(",") : String(existing ?? "")
|
|
88
|
+
)
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((value) => value.trim())
|
|
91
|
+
.filter((value) => value !== "");
|
|
92
|
+
if (!values.some((value) => value.toLowerCase() === SPOKE_KEY_HEADER)) {
|
|
93
|
+
values.push(SPOKE_KEY_HEADER);
|
|
94
|
+
}
|
|
95
|
+
reply.header("Vary", values.join(", "));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** An error's class name only — never its message, which may carry Stripe text. */
|
|
99
|
+
function errName(err: unknown): string {
|
|
100
|
+
return err instanceof Error ? err.constructor.name : typeof err;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The page Services followed by the connected workspace's shown Products, or
|
|
105
|
+
* `null` for the content path (#8933 D4). A failed read or mapping is one
|
|
106
|
+
* fixed `warn` line and the content path, never a 500 and never an `{ err }`
|
|
107
|
+
* line.
|
|
108
|
+
*/
|
|
109
|
+
async function shownSetOfferings(
|
|
110
|
+
request: FastifyRequest,
|
|
111
|
+
options: ShownOfferingsRouteOptions,
|
|
112
|
+
adapter: ContentAdapter,
|
|
113
|
+
listOptions: ListOfferingsOptions,
|
|
114
|
+
): Promise<GetOfferingsOutput | null> {
|
|
115
|
+
try {
|
|
116
|
+
const shownSet = currentShownSet(
|
|
117
|
+
await readShownOfferings(request, options),
|
|
118
|
+
);
|
|
119
|
+
return shownSet === null
|
|
120
|
+
? null
|
|
121
|
+
: await listOfferingsWithShownSet(adapter, listOptions, shownSet);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
request.log.warn(
|
|
124
|
+
{ outcome: "shown_set_failed", errName: errName(err) },
|
|
125
|
+
"[Content] offerings: shown set unavailable, answering page content",
|
|
126
|
+
);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const offeringsRoute: FastifyPluginAsync<
|
|
132
|
+
ShownOfferingsRouteOptions
|
|
133
|
+
> = async (fastify, options) => {
|
|
134
|
+
fastify.get<{
|
|
135
|
+
Querystring: { type?: string; locale?: string };
|
|
136
|
+
}>("/v1/content/offerings", async (request, reply) => {
|
|
137
|
+
const query = getOfferingsInputSchema.safeParse(request.query);
|
|
138
|
+
if (!query.success) {
|
|
139
|
+
return reply.status(400).send({
|
|
140
|
+
ok: false,
|
|
141
|
+
error: {
|
|
142
|
+
code: "validation_failed",
|
|
143
|
+
message: "Invalid query parameters",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const locale = resolveLocale(query.data.locale);
|
|
149
|
+
// Every answer from here on depends on the spoke key — the 404 and 500
|
|
150
|
+
// included — so each carries the Vary.
|
|
151
|
+
varyOnSpokeKey(reply);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const adapter = await resolvePublicContentAdapter(request);
|
|
155
|
+
if (adapter === null) {
|
|
156
|
+
return reply.status(404).send({
|
|
157
|
+
ok: false,
|
|
158
|
+
error: { code: "not_found", message: "Page not found" },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (query.data.type !== "service") {
|
|
163
|
+
const shown = await shownSetOfferings(request, options, adapter, {
|
|
164
|
+
type: query.data.type,
|
|
165
|
+
locale,
|
|
166
|
+
});
|
|
167
|
+
if (shown !== null) return reply.send({ ok: true, data: shown });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = await listOfferings(adapter, {
|
|
171
|
+
type: query.data.type,
|
|
172
|
+
locale,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return reply.send({ ok: true, data });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
request.log.error({ err: error }, "[Content] listOfferings failed");
|
|
178
|
+
return reply.status(500).send({
|
|
179
|
+
ok: false,
|
|
180
|
+
error: { code: "internal_error", message: "Internal server error" },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
};
|