create-ailk 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -20
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +4 -2
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +31 -3
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +65 -29
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +1 -1
- package/dist/lib/fetch-template-snapshot.js +7 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +34 -12
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +117 -550
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- package/dist/template-record.d.ts +14 -4
- package/dist/template-record.js +23 -5
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +31 -19
- package/dist/update.d.ts +10 -1
- package/dist/update.js +540 -16
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +5 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +11 -1
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +2 -2
- package/templates/apps/api/CLAUDE.md +102 -76
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1882
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +67 -20
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -13
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +10 -3
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +2 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/auth-email.ts +1 -1
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +5 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +553 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +48 -35
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -257
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -321
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -2148
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -183
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -100
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -639
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -336
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1211
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2085
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -349
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -564
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/site-config.ts +0 -53
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -138
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -81
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
- package/templates/apps/mcp/src/tools/search_content.ts +0 -63
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -981
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -272
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,853 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file get-page.test.ts
|
|
3
|
-
* GET /api/content/:type/:slug — single page with resolved JSON-LD.
|
|
4
|
-
*
|
|
5
|
-
* Uses the real content adapter backed by a per-test temp dir (Issue #507).
|
|
6
|
-
* No internal module mocks — the route exercises the real adapter stack.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { writeFileSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
|
|
12
|
-
import type { FastifyInstance } from "fastify";
|
|
13
|
-
|
|
14
|
-
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
15
|
-
import {
|
|
16
|
-
getContentAdapter,
|
|
17
|
-
type MdxContentAdapter,
|
|
18
|
-
} from "../../../lib/content-adapter.js";
|
|
19
|
-
import { contentRoutes } from "../index";
|
|
20
|
-
|
|
21
|
-
const fixture = setupContentFixture();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* A `_site.mdx` in the fixture's content root — an Organization identity at
|
|
25
|
-
* `https://example.com` (#6597). `logo` adds an identity-declared logo.
|
|
26
|
-
*/
|
|
27
|
-
function writeSiteMdx(root: string, opts: { logo?: string } = {}): void {
|
|
28
|
-
writeFileSync(
|
|
29
|
-
join(root, "_site.mdx"),
|
|
30
|
-
[
|
|
31
|
-
"---",
|
|
32
|
-
'version: "1"',
|
|
33
|
-
"siteName: Fixture Site",
|
|
34
|
-
"baseUrl: https://example.com",
|
|
35
|
-
"defaultLocale: en",
|
|
36
|
-
"siteIdentity:",
|
|
37
|
-
" kind: Organization",
|
|
38
|
-
" name: Fixture Co",
|
|
39
|
-
" url: https://example.com",
|
|
40
|
-
...(opts.logo !== undefined ? [` logo: ${opts.logo}`] : []),
|
|
41
|
-
"---",
|
|
42
|
-
"",
|
|
43
|
-
].join("\n"),
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("GET /api/content/:type/:slug", () => {
|
|
48
|
-
let app: FastifyInstance;
|
|
49
|
-
|
|
50
|
-
beforeEach(async () => {
|
|
51
|
-
app = require("fastify")();
|
|
52
|
-
await app.register(contentRoutes);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
afterEach(async () => {
|
|
56
|
-
await app.close();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("returns the page with resolved JSON-LD at the document root", async () => {
|
|
60
|
-
// The fixture writes "home" HomePage in beforeEach by default.
|
|
61
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
62
|
-
const res = await app.inject({
|
|
63
|
-
method: "GET",
|
|
64
|
-
url: "/v1/content/HomePage/home",
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(res.statusCode).toBe(200);
|
|
68
|
-
const body = JSON.parse(res.body);
|
|
69
|
-
expect(body["@context"]).toBe("https://schema.org");
|
|
70
|
-
// A HomePage body keeps its @graph envelope: the WebPage spine plus the
|
|
71
|
-
// sitewide identity from _site.mdx (#6597). Page fields live on the spine.
|
|
72
|
-
const graph = body["@graph"] as Array<Record<string, unknown>>;
|
|
73
|
-
expect(graph.map((n) => n["@type"])).toEqual([
|
|
74
|
-
"WebPage",
|
|
75
|
-
"Organization",
|
|
76
|
-
"WebSite",
|
|
77
|
-
]);
|
|
78
|
-
const spine = graph[0]!;
|
|
79
|
-
// title from fixture frontmatter: "HomePage home title"
|
|
80
|
-
expect(typeof spine.name).toBe("string");
|
|
81
|
-
expect(typeof spine.description).toBe("string");
|
|
82
|
-
expect(body.page).toMatchObject({
|
|
83
|
-
type: "HomePage",
|
|
84
|
-
slug: "home",
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("returns 404 when page is missing", async () => {
|
|
89
|
-
const res = await app.inject({
|
|
90
|
-
method: "GET",
|
|
91
|
-
url: "/v1/content/HomePage/missing-slug",
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(res.statusCode).toBe(404);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("returns 400 on unknown :type", async () => {
|
|
98
|
-
const res = await app.inject({
|
|
99
|
-
method: "GET",
|
|
100
|
-
url: "/v1/content/Bogus/home",
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(res.statusCode).toBe(400);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// #5927 — the read-path slug tightens from `z.string().min(1)` to
|
|
107
|
-
// SLUG_PATTERN, the same boundary create/update/delete already enforce on
|
|
108
|
-
// their own slug params. `paramsSchema.safeParse` runs before any adapter
|
|
109
|
-
// is resolved, so this 400 is unconditionally before any content query.
|
|
110
|
-
it("returns 400 on a traversal-shaped slug, before any adapter call", async () => {
|
|
111
|
-
// The slash is percent-encoded so it stays part of one path segment
|
|
112
|
-
// (`:slug`) rather than splitting into a second one — a literal `../`
|
|
113
|
-
// would fail routing entirely, and a bare `..` dot-segment-normalizes
|
|
114
|
-
// away before find-my-way ever sees it (WHATWG URL §4.7 recognises both
|
|
115
|
-
// "." and ".." — and their sole percent-encoded variants — as
|
|
116
|
-
// normalize-away segments, RFC 3986 §5.2's algorithm). This encoded form
|
|
117
|
-
// survives normalization intact and decodes to "../etc/passwd" only once
|
|
118
|
-
// find-my-way populates `request.params.slug`, which is what the
|
|
119
|
-
// paramsSchema regex actually sees.
|
|
120
|
-
const res = await app.inject({
|
|
121
|
-
method: "GET",
|
|
122
|
-
url: "/v1/content/HomePage/..%2Fetc%2Fpasswd",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(res.statusCode).toBe(400);
|
|
126
|
-
const body = JSON.parse(res.body);
|
|
127
|
-
expect(body.error).toBe("Invalid type or slug");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("includes publishedAt in ContentPageInput when page has publishedAt", async () => {
|
|
131
|
-
// Cover the `publishedAt !== undefined` branch.
|
|
132
|
-
await fixture.writeFixturePage(
|
|
133
|
-
"Product",
|
|
134
|
-
"dated-product",
|
|
135
|
-
{
|
|
136
|
-
type: "Product",
|
|
137
|
-
slug: "dated-product",
|
|
138
|
-
title: "Dated Product",
|
|
139
|
-
description: "An product with publishedAt",
|
|
140
|
-
publishedAt: "2024-01-15T00:00:00.000Z",
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const res = await app.inject({
|
|
145
|
-
method: "GET",
|
|
146
|
-
url: "/v1/content/Product/dated-product",
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
expect(res.statusCode).toBe(200);
|
|
150
|
-
const body = JSON.parse(res.body);
|
|
151
|
-
expect(body["@type"]).toBe("WebPage");
|
|
152
|
-
expect(body.mainEntity["@type"]).toBe("SoftwareApplication");
|
|
153
|
-
expect(body.page.publishedAt).toBe("2024-01-15T00:00:00.000Z");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("includes updatedAt and image in ContentPageInput when page has both fields", async () => {
|
|
157
|
-
// Cover the `updatedAt !== undefined` and `image !== undefined` branches.
|
|
158
|
-
await fixture.writeFixturePage(
|
|
159
|
-
"Product",
|
|
160
|
-
"updated-product",
|
|
161
|
-
{
|
|
162
|
-
type: "Product",
|
|
163
|
-
slug: "updated-product",
|
|
164
|
-
title: "Updated Product",
|
|
165
|
-
description: "Has an image",
|
|
166
|
-
updatedAt: "2024-06-01T00:00:00.000Z",
|
|
167
|
-
image: "https://example.com/image.jpg",
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const res = await app.inject({
|
|
172
|
-
method: "GET",
|
|
173
|
-
url: "/v1/content/Product/updated-product",
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(res.statusCode).toBe(200);
|
|
177
|
-
const body = JSON.parse(res.body);
|
|
178
|
-
expect(body.page.updatedAt).toBe("2024-06-01T00:00:00.000Z");
|
|
179
|
-
expect(body.page.image).toBe("https://example.com/image.jpg");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("includes schema in ContentPageInput when page has schema override", async () => {
|
|
183
|
-
// Cover the `schema !== undefined` branch.
|
|
184
|
-
// Note: schema is a JSON object in frontmatter; MDX frontmatter doesn't support
|
|
185
|
-
// nested objects directly — this test verifies the route reads the adapter response.
|
|
186
|
-
// Use the default fixture "test-product" Product page.
|
|
187
|
-
const res = await app.inject({
|
|
188
|
-
method: "GET",
|
|
189
|
-
url: "/v1/content/Product/test-product",
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
expect(res.statusCode).toBe(200);
|
|
193
|
-
const body = JSON.parse(res.body);
|
|
194
|
-
expect(body["@type"]).toBe("WebPage");
|
|
195
|
-
expect(body.mainEntity["@type"]).toBe("SoftwareApplication");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* D3 — API↔MCP parity evidence for #6030's AC-1 ("both ends"). `apps/api`
|
|
200
|
-
* cannot import `apps/mcp` (the hard import boundary), so the shared seam
|
|
201
|
-
* is asserted instead: the route must resolve through the SAME
|
|
202
|
-
* `ContentAdapter.getPageSource(type, slug, locale)` call the MCP tool
|
|
203
|
-
* builds, for BlogPosting and DocsDetail — the two types this issue
|
|
204
|
-
* admits. `apps/mcp/__tests__/read_content.test.ts`'s "MDX leg" describe
|
|
205
|
-
* pins the other half, the same shape `search.test.ts`'s D6 section
|
|
206
|
-
* established for #5831. `get-page.ts` itself is unedited (N3) — it
|
|
207
|
-
* already accepted both types via `SCHEMA_ORG_TYPES` before this issue.
|
|
208
|
-
*/
|
|
209
|
-
// #8746 D2, AC-1 — the site origin (`_site.mdx` `baseUrl`) reaches the
|
|
210
|
-
// JobPosting builder, so the posting names where to apply.
|
|
211
|
-
describe("a JobPosting carries its ApplyAction when the site is known (#8746 AC-1)", () => {
|
|
212
|
-
const writePosting = () =>
|
|
213
|
-
fixture.writeFixturePage(
|
|
214
|
-
"JobPosting",
|
|
215
|
-
"content-strategist",
|
|
216
|
-
{
|
|
217
|
-
type: "JobPosting",
|
|
218
|
-
slug: "content-strategist",
|
|
219
|
-
title: "Content Strategist",
|
|
220
|
-
description: "Own the content pipeline.",
|
|
221
|
-
},
|
|
222
|
-
"# Content Strategist",
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
it("targets <baseUrl>/<locale>/careers/<slug>/apply", async () => {
|
|
226
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
227
|
-
await writePosting();
|
|
228
|
-
const res = await app.inject({ method: "GET", url: "/v1/content/JobPosting/content-strategist" });
|
|
229
|
-
expect(res.statusCode).toBe(200);
|
|
230
|
-
const entity = JSON.parse(res.body).mainEntity as Record<string, unknown>;
|
|
231
|
-
expect(entity.potentialAction).toEqual({
|
|
232
|
-
"@type": "ApplyAction",
|
|
233
|
-
target: "https://example.com/en/careers/content-strategist/apply",
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("emits no ApplyAction when the deployment has no _site.mdx", async () => {
|
|
238
|
-
await writePosting();
|
|
239
|
-
const res = await app.inject({ method: "GET", url: "/v1/content/JobPosting/content-strategist" });
|
|
240
|
-
expect(res.statusCode).toBe(200);
|
|
241
|
-
expect("potentialAction" in (JSON.parse(res.body).mainEntity as Record<string, unknown>)).toBe(false);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("parity with the adapter the MCP tool calls (D3)", () => {
|
|
246
|
-
it.each(["BlogPosting", "DocsDetail"] as const)(
|
|
247
|
-
"GET /v1/content/%s/:slug?include=body deep-equals adapter.getPageSource",
|
|
248
|
-
async (type) => {
|
|
249
|
-
const slug = "launch-post";
|
|
250
|
-
await fixture.writeFixturePage(
|
|
251
|
-
type,
|
|
252
|
-
slug,
|
|
253
|
-
{
|
|
254
|
-
type,
|
|
255
|
-
slug,
|
|
256
|
-
title: `${type} ${slug} title`,
|
|
257
|
-
description: `${type} ${slug} description`,
|
|
258
|
-
},
|
|
259
|
-
"# Written body\n\nWith a second paragraph.",
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const res = await app.inject({
|
|
263
|
-
method: "GET",
|
|
264
|
-
url: `/v1/content/${type}/${slug}?include=body`,
|
|
265
|
-
});
|
|
266
|
-
expect(res.statusCode).toBe(200);
|
|
267
|
-
|
|
268
|
-
const adapter = getContentAdapter() as MdxContentAdapter;
|
|
269
|
-
const expected = await adapter.getPageSource(type, slug, "en");
|
|
270
|
-
expect(expected).not.toBeNull();
|
|
271
|
-
|
|
272
|
-
const body = JSON.parse(res.body);
|
|
273
|
-
expect(body.page).toMatchObject(expected!.frontmatter);
|
|
274
|
-
expect(body.body).toBe(expected!.body);
|
|
275
|
-
},
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// ─── #5852 — the database path: which workspace a public read serves ────────
|
|
281
|
-
//
|
|
282
|
-
// Appended, so every MDX case above is untouched (AC-4). Database-free: the
|
|
283
|
-
// module-level Prisma singleton is faked via the repo's manual-mock convention,
|
|
284
|
-
// and `jest.mock` is hoisted above the imports by ts-jest regardless of where
|
|
285
|
-
// it is written. The MDX cases never load `lib/prisma.js` — the resolver's
|
|
286
|
-
// import is deferred behind the `CONTENT_ADAPTER=database` branch — so the mock
|
|
287
|
-
// is inert for them.
|
|
288
|
-
|
|
289
|
-
// eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); __mocks__/prisma.ts is the repo-standard test double for this boundary.
|
|
290
|
-
jest.mock("../../../lib/prisma.js");
|
|
291
|
-
|
|
292
|
-
import {
|
|
293
|
-
DEFAULT_ORG_ID,
|
|
294
|
-
DEFAULT_WORKSPACE_ID,
|
|
295
|
-
} from "@working-theory/database/tenancy";
|
|
296
|
-
|
|
297
|
-
import { prisma } from "../../../lib/prisma.js";
|
|
298
|
-
|
|
299
|
-
const PUBLISHED_VERSION_ID = "ver_published";
|
|
300
|
-
|
|
301
|
-
/** The artifact row a resolved, published `probe` page reads from. */
|
|
302
|
-
const PUBLISHED_ARTIFACT = {
|
|
303
|
-
id: "art_probe",
|
|
304
|
-
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
305
|
-
contentType: "text/page",
|
|
306
|
-
locale: "en",
|
|
307
|
-
slug: "probe",
|
|
308
|
-
title: "Probe",
|
|
309
|
-
publishedVersionId: PUBLISHED_VERSION_ID,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
/** The published version's `text/page` blob. */
|
|
313
|
-
const PUBLISHED_VERSION = {
|
|
314
|
-
id: PUBLISHED_VERSION_ID,
|
|
315
|
-
artifactId: PUBLISHED_ARTIFACT.id,
|
|
316
|
-
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
317
|
-
isCurrent: false,
|
|
318
|
-
content: {
|
|
319
|
-
type: "HomePage",
|
|
320
|
-
slug: "probe",
|
|
321
|
-
title: "Probe",
|
|
322
|
-
description: "The published revision of the probe page.",
|
|
323
|
-
body: "Published body.",
|
|
324
|
-
},
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
/** A one-workspace deployment seeded with the well-known default. */
|
|
328
|
-
function seedOneWorkspaceDeployment(): void {
|
|
329
|
-
(prisma.workspace.count as jest.Mock).mockResolvedValue(1);
|
|
330
|
-
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
331
|
-
id: DEFAULT_WORKSPACE_ID,
|
|
332
|
-
organizationId: DEFAULT_ORG_ID,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
describe("GET /v1/content/:type/:slug under CONTENT_ADAPTER=database (#5852)", () => {
|
|
337
|
-
let app: FastifyInstance;
|
|
338
|
-
|
|
339
|
-
beforeEach(async () => {
|
|
340
|
-
jest.clearAllMocks();
|
|
341
|
-
process.env.CONTENT_ADAPTER = "database";
|
|
342
|
-
app = require("fastify")();
|
|
343
|
-
await app.register(contentRoutes);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
afterEach(async () => {
|
|
347
|
-
delete process.env.CONTENT_ADAPTER;
|
|
348
|
-
await app.close();
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// AC-1 — the seeded default workspace's PUBLISHED revision, with no workspace
|
|
352
|
-
// id anywhere in the URL (AC-WCE-06, read half).
|
|
353
|
-
it("serves the default workspace's published revision, with no workspace id in the URL", async () => {
|
|
354
|
-
seedOneWorkspaceDeployment();
|
|
355
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue(
|
|
356
|
-
PUBLISHED_ARTIFACT,
|
|
357
|
-
);
|
|
358
|
-
(prisma.artifactVersion.findFirst as jest.Mock).mockResolvedValue(
|
|
359
|
-
PUBLISHED_VERSION,
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
const url = "/v1/content/HomePage/probe";
|
|
363
|
-
const res = await app.inject({ method: "GET", url });
|
|
364
|
-
|
|
365
|
-
expect(res.statusCode).toBe(200);
|
|
366
|
-
expect(JSON.parse(res.body).page.slug).toBe("probe");
|
|
367
|
-
// The tenant is resolved server-side; it is never addressable from the wire.
|
|
368
|
-
expect(url).not.toMatch(/wksp_/);
|
|
369
|
-
expect(prisma.artifact.findFirst).toHaveBeenCalledWith(
|
|
370
|
-
expect.objectContaining({
|
|
371
|
-
where: expect.objectContaining({ workspaceId: DEFAULT_WORKSPACE_ID }),
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// AC-2 — a draft with no published pointer is not public (the read-side
|
|
377
|
-
// invariant of AC-WCE-09). The draft tip is never even fetched.
|
|
378
|
-
it("404s a page whose artifact has a draft but no publishedVersionId", async () => {
|
|
379
|
-
seedOneWorkspaceDeployment();
|
|
380
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue({
|
|
381
|
-
...PUBLISHED_ARTIFACT,
|
|
382
|
-
publishedVersionId: null,
|
|
383
|
-
});
|
|
384
|
-
// The draft tip EXISTS and would parse — the read must still not reach it.
|
|
385
|
-
(prisma.artifactVersion.findFirst as jest.Mock).mockResolvedValue({
|
|
386
|
-
...PUBLISHED_VERSION,
|
|
387
|
-
id: "ver_draft",
|
|
388
|
-
isCurrent: true,
|
|
389
|
-
content: { ...PUBLISHED_VERSION.content, title: "Unpublished draft" },
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const res = await app.inject({
|
|
393
|
-
method: "GET",
|
|
394
|
-
url: "/v1/content/HomePage/probe",
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
expect(res.statusCode).toBe(404);
|
|
398
|
-
expect(prisma.artifactVersion.findFirst).not.toHaveBeenCalled();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// AC-3 — enumeration: a foreign workspace's slug, a slug published nowhere,
|
|
402
|
-
// and both ambiguity arms are the SAME 404 bytes for the same request
|
|
403
|
-
// (AC-WCE-05), and the ambiguity arms emit theirs before any content query.
|
|
404
|
-
it("answers a foreign slug, a missing slug and both ambiguity arms with identical 404 bodies", async () => {
|
|
405
|
-
// (a) resolved deployment; `probe` is published only in a FOREIGN
|
|
406
|
-
// workspace, so the workspace-bound query cannot see it.
|
|
407
|
-
seedOneWorkspaceDeployment();
|
|
408
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue(null);
|
|
409
|
-
const foreign = await app.inject({
|
|
410
|
-
method: "GET",
|
|
411
|
-
url: "/v1/content/HomePage/probe",
|
|
412
|
-
});
|
|
413
|
-
expect(foreign.statusCode).toBe(404);
|
|
414
|
-
// What makes this a FOREIGN-workspace test rather than a mock returning
|
|
415
|
-
// null on request: the query is bound to the resolved workspace.
|
|
416
|
-
expect(prisma.artifact.findFirst).toHaveBeenCalledWith(
|
|
417
|
-
expect.objectContaining({
|
|
418
|
-
where: expect.objectContaining({ workspaceId: DEFAULT_WORKSPACE_ID }),
|
|
419
|
-
}),
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
// (b) resolved deployment; `probe` is published nowhere at all.
|
|
423
|
-
jest.clearAllMocks();
|
|
424
|
-
seedOneWorkspaceDeployment();
|
|
425
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue(null);
|
|
426
|
-
const missing = await app.inject({
|
|
427
|
-
method: "GET",
|
|
428
|
-
url: "/v1/content/HomePage/probe",
|
|
429
|
-
});
|
|
430
|
-
expect(missing.statusCode).toBe(404);
|
|
431
|
-
|
|
432
|
-
// (c) two workspaces — ambiguous, so no site resolves.
|
|
433
|
-
jest.clearAllMocks();
|
|
434
|
-
(prisma.workspace.count as jest.Mock).mockResolvedValue(2);
|
|
435
|
-
const ambiguous = await app.inject({
|
|
436
|
-
method: "GET",
|
|
437
|
-
url: "/v1/content/HomePage/probe",
|
|
438
|
-
});
|
|
439
|
-
expect(ambiguous.statusCode).toBe(404);
|
|
440
|
-
expect(prisma.artifact.findFirst).not.toHaveBeenCalled();
|
|
441
|
-
|
|
442
|
-
// (d) no workspace at all — the same refusal.
|
|
443
|
-
jest.clearAllMocks();
|
|
444
|
-
(prisma.workspace.count as jest.Mock).mockResolvedValue(0);
|
|
445
|
-
const empty = await app.inject({
|
|
446
|
-
method: "GET",
|
|
447
|
-
url: "/v1/content/HomePage/probe",
|
|
448
|
-
});
|
|
449
|
-
expect(empty.statusCode).toBe(404);
|
|
450
|
-
expect(prisma.artifact.findFirst).not.toHaveBeenCalled();
|
|
451
|
-
|
|
452
|
-
// The four bodies are byte-identical: a resolution failure and a missing
|
|
453
|
-
// page are indistinguishable to an unauthenticated caller.
|
|
454
|
-
expect(missing.body).toBe(foreign.body);
|
|
455
|
-
expect(ambiguous.body).toBe(foreign.body);
|
|
456
|
-
expect(empty.body).toBe(foreign.body);
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// ─── #5890 — the published body over HTTP: an opt-in `?include=body` ────────
|
|
461
|
-
//
|
|
462
|
-
// Appended again, so every case above — the MDX cases (#507) and the database
|
|
463
|
-
// cases (#5852) — stays byte-identical. The inline snapshot below was recorded
|
|
464
|
-
// against the PRE-change handler and must survive the change unmodified; that,
|
|
465
|
-
// not a hand-copied literal, is the byte-identity evidence AC-1 asks for.
|
|
466
|
-
//
|
|
467
|
-
// The body-less-adapter cases use the real PROJECTION adapter rather than a
|
|
468
|
-
// module spy: it satisfies `ContentAdapter` and has no `getPageSource`, so the
|
|
469
|
-
// route's capability branch is exercised through the real resolver — the file
|
|
470
|
-
// header's "no internal module mocks" (#507) holds for these cases too.
|
|
471
|
-
|
|
472
|
-
import { mkdtempSync } from "node:fs";
|
|
473
|
-
import { tmpdir } from "node:os";
|
|
474
|
-
|
|
475
|
-
import * as contentAdapterModule from "../../../lib/content-adapter.js";
|
|
476
|
-
import { resetContentAdapter } from "../../../lib/content-adapter.js";
|
|
477
|
-
|
|
478
|
-
/** A projection manifest carrying the one page these cases read. */
|
|
479
|
-
function withProjectionAdapter(): void {
|
|
480
|
-
const manifestPath = join(
|
|
481
|
-
mkdtempSync(join(tmpdir(), "ailk-api-5890-")),
|
|
482
|
-
"manifest.json",
|
|
483
|
-
);
|
|
484
|
-
writeFileSync(
|
|
485
|
-
manifestPath,
|
|
486
|
-
JSON.stringify({
|
|
487
|
-
HomePage: [
|
|
488
|
-
{
|
|
489
|
-
type: "HomePage",
|
|
490
|
-
slug: "home",
|
|
491
|
-
title: "Projection home",
|
|
492
|
-
description: "Served by an adapter with no getPageSource.",
|
|
493
|
-
},
|
|
494
|
-
],
|
|
495
|
-
}),
|
|
496
|
-
);
|
|
497
|
-
process.env.CONTENT_ADAPTER = "projection";
|
|
498
|
-
process.env.MANIFEST_PATH = manifestPath;
|
|
499
|
-
resetContentAdapter();
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
describe("GET /v1/content/:type/:slug?include=body (#5890)", () => {
|
|
503
|
-
let app: FastifyInstance;
|
|
504
|
-
|
|
505
|
-
beforeEach(async () => {
|
|
506
|
-
app = require("fastify")();
|
|
507
|
-
await app.register(contentRoutes);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
afterEach(async () => {
|
|
511
|
-
jest.restoreAllMocks();
|
|
512
|
-
delete process.env.CONTENT_ADAPTER;
|
|
513
|
-
delete process.env.MANIFEST_PATH;
|
|
514
|
-
resetContentAdapter();
|
|
515
|
-
await app.close();
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// AC-1, the floor half — the unparameterised read is unchanged to the byte.
|
|
519
|
-
// #6597 rewrote this pin by hand: the HomePage @graph is now composed from
|
|
520
|
-
// the fixture's _site.mdx rather than the page title (AC-9 checks every
|
|
521
|
-
// field of the pre-#6597 body is still present). The #5890 claim it guards
|
|
522
|
-
// — no `body` key without the parameter — is unchanged.
|
|
523
|
-
it("serves a response byte-identical to the pre-change handler when the parameter is absent", async () => {
|
|
524
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
525
|
-
const res = await app.inject({
|
|
526
|
-
method: "GET",
|
|
527
|
-
url: "/v1/content/HomePage/home",
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
expect(res.statusCode).toBe(200);
|
|
531
|
-
expect(res.body).toMatchInlineSnapshot(
|
|
532
|
-
`"{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"HomePage home title","description":"HomePage home description","mainEntity":{"@id":"https://example.com/#organization"},"isPartOf":{"@id":"https://example.com/#website"}},{"@context":"https://schema.org","@type":"Organization","@id":"https://example.com/#organization","name":"Fixture Co","url":"https://example.com"},{"@context":"https://schema.org","@type":"WebSite","@id":"https://example.com/#website","name":"Fixture Site","url":"https://example.com","inLanguage":"en","publisher":{"@id":"https://example.com/#organization"}}],"page":{"type":"HomePage","slug":"home","title":"HomePage home title","description":"HomePage home description"}}"`,
|
|
533
|
-
);
|
|
534
|
-
expect(JSON.parse(res.body)).not.toHaveProperty("body");
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// AC-1, the additive half — the raw MDX body, beside `page`, on request.
|
|
538
|
-
it("adds the raw published MDX body beside `page` when asked", async () => {
|
|
539
|
-
await fixture.writeFixturePage(
|
|
540
|
-
"HomePage",
|
|
541
|
-
"home",
|
|
542
|
-
{
|
|
543
|
-
type: "HomePage",
|
|
544
|
-
slug: "home",
|
|
545
|
-
title: "HomePage home title",
|
|
546
|
-
description: "HomePage home description",
|
|
547
|
-
},
|
|
548
|
-
"# Written body\n\nWith a second paragraph.",
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
const res = await app.inject({
|
|
552
|
-
method: "GET",
|
|
553
|
-
url: "/v1/content/HomePage/home?include=body",
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
expect(res.statusCode).toBe(200);
|
|
557
|
-
const body = JSON.parse(res.body);
|
|
558
|
-
// RAW, not trimmed: the body is handed to `next-mdx-remote` verbatim, so
|
|
559
|
-
// the route must not normalise whitespace the author wrote.
|
|
560
|
-
expect(body.body).toBe("\n# Written body\n\nWith a second paragraph.\n");
|
|
561
|
-
// Additive: everything the unparameterised read serves is still served.
|
|
562
|
-
expect(body.page).toMatchObject({ type: "HomePage", slug: "home" });
|
|
563
|
-
expect(body["@context"]).toBe("https://schema.org");
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
// The query idiom `list-pages.ts` uses: an unrecognised value is a 400 with
|
|
567
|
-
// the zod issues under `details`, never a silently-ignored parameter.
|
|
568
|
-
it("400s an unrecognised include value", async () => {
|
|
569
|
-
const res = await app.inject({
|
|
570
|
-
method: "GET",
|
|
571
|
-
url: "/v1/content/HomePage/home?include=other",
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
expect(res.statusCode).toBe(400);
|
|
575
|
-
const body = JSON.parse(res.body);
|
|
576
|
-
expect(body.error).toBe("Invalid query parameters");
|
|
577
|
-
expect(JSON.stringify(body.details)).toContain("include");
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
// AC-2's refusal arm — a body-less adapter answers 400 naming the parameter,
|
|
581
|
-
// never a 500 from `getPageSource is not a function`, and never after having
|
|
582
|
-
// read content it then throws away.
|
|
583
|
-
it("400s naming `include` when the resolved adapter has no getPageSource, before any content read", async () => {
|
|
584
|
-
withProjectionAdapter();
|
|
585
|
-
// The adapter is REAL — a projection adapter genuinely has no
|
|
586
|
-
// `getPageSource`. The module spy only PINS the instance (the resolver
|
|
587
|
-
// hands out a fresh object per call), so the read spy below observes the
|
|
588
|
-
// object the route actually uses instead of a discarded twin. Nothing
|
|
589
|
-
// about the adapter's behaviour is faked.
|
|
590
|
-
const real = contentAdapterModule.getContentAdapter();
|
|
591
|
-
const read = jest.spyOn(real, "getPageBySlug");
|
|
592
|
-
jest
|
|
593
|
-
.spyOn(contentAdapterModule, "getContentAdapter")
|
|
594
|
-
.mockReturnValue(real);
|
|
595
|
-
|
|
596
|
-
const res = await app.inject({
|
|
597
|
-
method: "GET",
|
|
598
|
-
url: "/v1/content/HomePage/home?include=body",
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
expect(res.statusCode).toBe(400);
|
|
602
|
-
const body = JSON.parse(res.body);
|
|
603
|
-
expect(body.error).toBe("Invalid query parameters");
|
|
604
|
-
expect(body.details).toEqual([
|
|
605
|
-
{
|
|
606
|
-
path: ["include"],
|
|
607
|
-
message: "body is not available from this content adapter",
|
|
608
|
-
},
|
|
609
|
-
]);
|
|
610
|
-
// The refusal precedes the lookup: nothing was read to be discarded.
|
|
611
|
-
expect(read).not.toHaveBeenCalled();
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
// The same adapter still serves the unparameterised read unchanged — the
|
|
615
|
-
// capability gate is scoped to the parameter, not to the route.
|
|
616
|
-
it("leaves the unparameterised read working on a body-less adapter", async () => {
|
|
617
|
-
withProjectionAdapter();
|
|
618
|
-
|
|
619
|
-
const res = await app.inject({
|
|
620
|
-
method: "GET",
|
|
621
|
-
url: "/v1/content/HomePage/home",
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
expect(res.statusCode).toBe(200);
|
|
625
|
-
expect(JSON.parse(res.body).page.title).toBe("Projection home");
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// #5927 — the fix's whole point: `include=body` performs exactly ONE
|
|
629
|
-
// adapter read, so `page` and `body` can never straddle two revisions. A
|
|
630
|
-
// mocked adapter whose two methods disagree proves it: if the route still
|
|
631
|
-
// called `getPageBySlug` as well as `getPageSource`, `page` would read the
|
|
632
|
-
// revision `getPageBySlug` returns while `body` read the (different)
|
|
633
|
-
// revision `getPageSource` returns — the mixed response this test forbids.
|
|
634
|
-
it("performs a single adapter read for include=body, so page and body can never straddle two revisions", async () => {
|
|
635
|
-
const staleFrontmatter = {
|
|
636
|
-
type: "HomePage" as const,
|
|
637
|
-
slug: "probe",
|
|
638
|
-
title: "Stale — the revision a second, independent read would see",
|
|
639
|
-
description: "Would only be returned by a getPageBySlug call.",
|
|
640
|
-
};
|
|
641
|
-
const published = {
|
|
642
|
-
frontmatter: {
|
|
643
|
-
type: "HomePage" as const,
|
|
644
|
-
slug: "probe",
|
|
645
|
-
title: "Published — the revision the single read actually returns",
|
|
646
|
-
description: "Returned by the one getPageSource call.",
|
|
647
|
-
},
|
|
648
|
-
body: "Published body.",
|
|
649
|
-
};
|
|
650
|
-
const getPageBySlug = jest.fn().mockResolvedValue(staleFrontmatter);
|
|
651
|
-
const getPageSource = jest.fn().mockResolvedValue(published);
|
|
652
|
-
// Typed directly at the boundary (no `as unknown as` cast) — every
|
|
653
|
-
// `MdxContentAdapter` member is stubbed, so the object literal satisfies
|
|
654
|
-
// the interface on its own.
|
|
655
|
-
const fakeAdapter: MdxContentAdapter = {
|
|
656
|
-
getPageBySlug,
|
|
657
|
-
getPageSource,
|
|
658
|
-
listPages: jest.fn(),
|
|
659
|
-
getDetailBySlug: jest.fn(),
|
|
660
|
-
listDetail: jest.fn(),
|
|
661
|
-
searchPages: jest.fn(),
|
|
662
|
-
extractStructuredData: jest.fn(),
|
|
663
|
-
createPage: jest.fn(),
|
|
664
|
-
updatePage: jest.fn(),
|
|
665
|
-
deletePage: jest.fn(),
|
|
666
|
-
getPageVersion: jest.fn(),
|
|
667
|
-
};
|
|
668
|
-
jest
|
|
669
|
-
.spyOn(contentAdapterModule, "getContentAdapter")
|
|
670
|
-
.mockReturnValue(fakeAdapter);
|
|
671
|
-
|
|
672
|
-
const res = await app.inject({
|
|
673
|
-
method: "GET",
|
|
674
|
-
url: "/v1/content/HomePage/probe?include=body",
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
expect(res.statusCode).toBe(200);
|
|
678
|
-
const body = JSON.parse(res.body);
|
|
679
|
-
// getPageBySlug is never called on this path — getPageSource alone
|
|
680
|
-
// supplies both halves, from the SAME read.
|
|
681
|
-
expect(getPageBySlug).not.toHaveBeenCalled();
|
|
682
|
-
expect(getPageSource).toHaveBeenCalledTimes(1);
|
|
683
|
-
expect(body.page.title).toBe(
|
|
684
|
-
"Published — the revision the single read actually returns",
|
|
685
|
-
);
|
|
686
|
-
expect(body.body).toBe("Published body.");
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
describe("GET /v1/content/:type/:slug?include=body under CONTENT_ADAPTER=database (#5890)", () => {
|
|
691
|
-
let app: FastifyInstance;
|
|
692
|
-
|
|
693
|
-
beforeEach(async () => {
|
|
694
|
-
jest.clearAllMocks();
|
|
695
|
-
process.env.CONTENT_ADAPTER = "database";
|
|
696
|
-
app = require("fastify")();
|
|
697
|
-
await app.register(contentRoutes);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
afterEach(async () => {
|
|
701
|
-
delete process.env.CONTENT_ADAPTER;
|
|
702
|
-
await app.close();
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
// AC-2 — #5874's `getPageSource` on the database adapter, reached over HTTP.
|
|
706
|
-
it("serves the published version's body", async () => {
|
|
707
|
-
seedOneWorkspaceDeployment();
|
|
708
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue(
|
|
709
|
-
PUBLISHED_ARTIFACT,
|
|
710
|
-
);
|
|
711
|
-
(prisma.artifactVersion.findFirst as jest.Mock).mockResolvedValue(
|
|
712
|
-
PUBLISHED_VERSION,
|
|
713
|
-
);
|
|
714
|
-
|
|
715
|
-
const res = await app.inject({
|
|
716
|
-
method: "GET",
|
|
717
|
-
url: "/v1/content/HomePage/probe?include=body",
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
expect(res.statusCode).toBe(200);
|
|
721
|
-
const body = JSON.parse(res.body);
|
|
722
|
-
expect(body.body).toBe("Published body.");
|
|
723
|
-
expect(body.page.slug).toBe("probe");
|
|
724
|
-
// The body comes from the PUBLISHED pointer, not the draft tip.
|
|
725
|
-
expect(prisma.artifactVersion.findFirst).toHaveBeenCalledWith(
|
|
726
|
-
expect.objectContaining({
|
|
727
|
-
where: expect.objectContaining({ id: PUBLISHED_VERSION_ID }),
|
|
728
|
-
}),
|
|
729
|
-
);
|
|
730
|
-
// #5927 — ONE read of each table, not two: `page` and `body` come from
|
|
731
|
-
// the SAME `getPageSource` call, never a `getPageBySlug` call plus a
|
|
732
|
-
// second, independent `getPageSource` call.
|
|
733
|
-
expect(prisma.artifact.findFirst).toHaveBeenCalledTimes(1);
|
|
734
|
-
expect(prisma.artifactVersion.findFirst).toHaveBeenCalledTimes(1);
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// AC-2 — the parameter buys no access a plain read does not have.
|
|
738
|
-
it("404s a draft-only page with the parameter, exactly as without it", async () => {
|
|
739
|
-
seedOneWorkspaceDeployment();
|
|
740
|
-
(prisma.artifact.findFirst as jest.Mock).mockResolvedValue({
|
|
741
|
-
...PUBLISHED_ARTIFACT,
|
|
742
|
-
publishedVersionId: null,
|
|
743
|
-
});
|
|
744
|
-
(prisma.artifactVersion.findFirst as jest.Mock).mockResolvedValue({
|
|
745
|
-
...PUBLISHED_VERSION,
|
|
746
|
-
id: "ver_draft",
|
|
747
|
-
isCurrent: true,
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
const withParam = await app.inject({
|
|
751
|
-
method: "GET",
|
|
752
|
-
url: "/v1/content/HomePage/probe?include=body",
|
|
753
|
-
});
|
|
754
|
-
const without = await app.inject({
|
|
755
|
-
method: "GET",
|
|
756
|
-
url: "/v1/content/HomePage/probe",
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
expect(withParam.statusCode).toBe(404);
|
|
760
|
-
expect(withParam.body).toBe(without.body);
|
|
761
|
-
expect(prisma.artifactVersion.findFirst).not.toHaveBeenCalled();
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
// ─── #6597 AC-9 — the /v1 HomePage response keeps its @graph envelope ─────────
|
|
766
|
-
|
|
767
|
-
describe("GET /v1/content/HomePage/home — sitewide identity envelope (#6597)", () => {
|
|
768
|
-
let app: FastifyInstance;
|
|
769
|
-
|
|
770
|
-
beforeEach(async () => {
|
|
771
|
-
app = require("fastify")();
|
|
772
|
-
await app.register(contentRoutes);
|
|
773
|
-
await fixture.writeFixturePage("HomePage", "home", {
|
|
774
|
-
type: "HomePage",
|
|
775
|
-
slug: "home",
|
|
776
|
-
title: "HomePage home title",
|
|
777
|
-
description: "HomePage home description",
|
|
778
|
-
image: "https://example.com/home.png",
|
|
779
|
-
});
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
afterEach(async () => {
|
|
783
|
-
await app.close();
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
async function homeBody(query = ""): Promise<{
|
|
787
|
-
body: Record<string, unknown>;
|
|
788
|
-
graph: Array<Record<string, unknown>>;
|
|
789
|
-
}> {
|
|
790
|
-
const res = await app.inject({ method: "GET", url: `/v1/content/HomePage/home${query}` });
|
|
791
|
-
expect(res.statusCode).toBe(200);
|
|
792
|
-
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
793
|
-
return { body, graph: body["@graph"] as Array<Record<string, unknown>> };
|
|
794
|
-
}
|
|
795
|
-
const byType = (graph: Array<Record<string, unknown>>, type: string) =>
|
|
796
|
-
graph.find((n) => n["@type"] === type)!;
|
|
797
|
-
|
|
798
|
-
it("composes WebPage + Organization + WebSite from _site.mdx, keeping every pre-#6597 field", async () => {
|
|
799
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
800
|
-
const { body, graph } = await homeBody();
|
|
801
|
-
expect(body["@context"]).toBe("https://schema.org");
|
|
802
|
-
expect(graph.map((n) => n["@type"])).toEqual(["WebPage", "Organization", "WebSite"]);
|
|
803
|
-
|
|
804
|
-
const org = byType(graph, "Organization");
|
|
805
|
-
expect(org["@id"]).toBe("https://example.com/#organization");
|
|
806
|
-
expect(org["name"]).toBe("Fixture Co");
|
|
807
|
-
// The pre-#6597 Organization took `logo` from the page image; kept as the
|
|
808
|
-
// fallback when the identity declares none.
|
|
809
|
-
expect(org["logo"]).toBe("https://example.com/home.png");
|
|
810
|
-
|
|
811
|
-
// Every key of every node of the pre-#6597 pinned body survives.
|
|
812
|
-
const pinned: Record<string, string[]> = {
|
|
813
|
-
WebPage: ["@context", "@type", "inLanguage", "name", "description"],
|
|
814
|
-
Organization: ["@context", "@type", "name"],
|
|
815
|
-
WebSite: ["@context", "@type", "name", "inLanguage"],
|
|
816
|
-
};
|
|
817
|
-
for (const [type, keys] of Object.entries(pinned)) {
|
|
818
|
-
for (const key of keys) expect(byType(graph, type)).toHaveProperty([key]);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const webPage = byType(graph, "WebPage");
|
|
822
|
-
expect(byType(graph, "WebSite")["inLanguage"]).toBe(webPage["inLanguage"]);
|
|
823
|
-
expect(webPage["inLanguage"]).toBe("en");
|
|
824
|
-
expect(graph.filter((n) => n["name"] === "HomePage home title")).toEqual([webPage]);
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it("the WebSite and WebPage inLanguage stay one value under ?locale=<second locale>", async () => {
|
|
828
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
829
|
-
const { graph } = await homeBody("?locale=pt-BR");
|
|
830
|
-
expect(byType(graph, "WebSite")["inLanguage"]).toBe(byType(graph, "WebPage")["inLanguage"]);
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it("an identity-declared logo wins over the page image", async () => {
|
|
834
|
-
writeSiteMdx(fixture.getContentRoot(), { logo: "https://example.com/logo.png" });
|
|
835
|
-
const { graph } = await homeBody();
|
|
836
|
-
expect(byType(graph, "Organization")["logo"]).toBe("https://example.com/logo.png");
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it("with no _site.mdx the body still has @context and a one-node @graph", async () => {
|
|
840
|
-
const { body, graph } = await homeBody();
|
|
841
|
-
expect(body["@context"]).toBe("https://schema.org");
|
|
842
|
-
expect(graph.map((n) => n["@type"])).toEqual(["WebPage"]);
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
it("other types keep their bare JSON-LD — no @graph key", async () => {
|
|
846
|
-
writeSiteMdx(fixture.getContentRoot());
|
|
847
|
-
const res = await app.inject({ method: "GET", url: "/v1/content/AboutPage/about" });
|
|
848
|
-
expect(res.statusCode).toBe(200);
|
|
849
|
-
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
850
|
-
expect(body).not.toHaveProperty("@graph");
|
|
851
|
-
expect(body["@type"]).toBe("AboutPage");
|
|
852
|
-
});
|
|
853
|
-
});
|