create-ailk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file delete.test.ts
|
|
3
|
-
* DELETE /api/content/:type/:slug — D14a Stage 1 Unit B.
|
|
4
|
-
*
|
|
5
|
-
* Covers: Pro-tier gating (402), missing/stale If-Match (409), happy path
|
|
6
|
-
* (204 no body), not-found (404), unknown :type (400).
|
|
7
|
-
*
|
|
8
|
-
* Issue #398 follow-up: bespoke version-probe + getPageBySlug pre-check
|
|
9
|
-
* removed; the adapter throws `AdapterWriteError("not_found" | "conflict")`
|
|
10
|
-
* and the hoisted `mapAdapterError` translates it.
|
|
11
|
-
*
|
|
12
|
-
* Uses the real content adapter backed by a per-test temp dir (Issue #507).
|
|
13
|
-
* @working-theory/license stays mocked — it is an external package boundary.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { FastifyInstance } from "fastify";
|
|
17
|
-
|
|
18
|
-
const mockCheckLicense = jest.fn();
|
|
19
|
-
|
|
20
|
-
// @working-theory/license is an external package boundary — stays mocked.
|
|
21
|
-
jest.mock("@working-theory/license", () => ({
|
|
22
|
-
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
26
|
-
import { contentRoutes } from "../index";
|
|
27
|
-
|
|
28
|
-
const fixture = setupContentFixture();
|
|
29
|
-
|
|
30
|
-
function proLicense() {
|
|
31
|
-
return {
|
|
32
|
-
tier: "pro",
|
|
33
|
-
plan: "pro",
|
|
34
|
-
features: [],
|
|
35
|
-
expiresAt: null,
|
|
36
|
-
revoked: false,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function freeLicense() {
|
|
41
|
-
return {
|
|
42
|
-
tier: "free",
|
|
43
|
-
plan: null,
|
|
44
|
-
features: [],
|
|
45
|
-
expiresAt: null,
|
|
46
|
-
revoked: false,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe("DELETE /api/content/:type/:slug", () => {
|
|
51
|
-
let app: FastifyInstance;
|
|
52
|
-
// The write handlers only register when content-write is enabled
|
|
53
|
-
// (routes/content/index.ts's isModuleEnabled('content-write') gate); an
|
|
54
|
-
// unset AILK_ENABLED_MODULES no longer defaults to every module on
|
|
55
|
-
// (D-44, #5368), so this suite enables it explicitly.
|
|
56
|
-
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
57
|
-
|
|
58
|
-
beforeEach(async () => {
|
|
59
|
-
process.env.AILK_ENABLED_MODULES = "content-write";
|
|
60
|
-
app = require("fastify")();
|
|
61
|
-
await app.register(contentRoutes);
|
|
62
|
-
jest.clearAllMocks();
|
|
63
|
-
mockCheckLicense.mockReset();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
afterAll(() => {
|
|
67
|
-
if (priorEnabledModules === undefined) {
|
|
68
|
-
delete process.env.AILK_ENABLED_MODULES;
|
|
69
|
-
} else {
|
|
70
|
-
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(async () => {
|
|
75
|
-
await app.close();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("returns 204 with no body on success", async () => {
|
|
79
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
80
|
-
|
|
81
|
-
// First create the page, get its version, then delete it.
|
|
82
|
-
const createRes = await app.inject({
|
|
83
|
-
method: "POST",
|
|
84
|
-
url: "/v1/content/HomePage",
|
|
85
|
-
payload: {
|
|
86
|
-
type: "HomePage",
|
|
87
|
-
slug: "page-to-delete",
|
|
88
|
-
title: "Page To Delete",
|
|
89
|
-
description: "This page will be deleted",
|
|
90
|
-
body: "Goodbye",
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
expect(createRes.statusCode).toBe(201);
|
|
94
|
-
const { version } = JSON.parse(createRes.body);
|
|
95
|
-
|
|
96
|
-
const res = await app.inject({
|
|
97
|
-
method: "DELETE",
|
|
98
|
-
url: "/v1/content/HomePage/page-to-delete",
|
|
99
|
-
headers: { "if-match": version },
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(res.statusCode).toBe(204);
|
|
103
|
-
expect(res.body).toBe("");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns 402 payment_required on free tier", async () => {
|
|
107
|
-
mockCheckLicense.mockResolvedValue(freeLicense());
|
|
108
|
-
|
|
109
|
-
const res = await app.inject({
|
|
110
|
-
method: "DELETE",
|
|
111
|
-
url: "/v1/content/HomePage/home",
|
|
112
|
-
headers: { "if-match": "v1" },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(res.statusCode).toBe(402);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("returns 402 payment_required when license is revoked even at pro tier", async () => {
|
|
119
|
-
mockCheckLicense.mockResolvedValue({
|
|
120
|
-
tier: "pro",
|
|
121
|
-
plan: "pro",
|
|
122
|
-
features: [],
|
|
123
|
-
expiresAt: null,
|
|
124
|
-
revoked: true,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const res = await app.inject({
|
|
128
|
-
method: "DELETE",
|
|
129
|
-
url: "/v1/content/HomePage/home",
|
|
130
|
-
headers: { "if-match": "v1" },
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
expect(res.statusCode).toBe(402);
|
|
134
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("returns 409 conflict when If-Match header is missing", async () => {
|
|
138
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
139
|
-
|
|
140
|
-
const res = await app.inject({
|
|
141
|
-
method: "DELETE",
|
|
142
|
-
url: "/v1/content/HomePage/home",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
expect(res.statusCode).toBe(409);
|
|
146
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("returns 409 conflict when If-Match is empty string", async () => {
|
|
150
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
151
|
-
|
|
152
|
-
const res = await app.inject({
|
|
153
|
-
method: "DELETE",
|
|
154
|
-
url: "/v1/content/HomePage/home",
|
|
155
|
-
headers: { "if-match": "" },
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(res.statusCode).toBe(409);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("returns 409 conflict with currentVersion echoed when adapter signals stale If-Match", async () => {
|
|
162
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
163
|
-
|
|
164
|
-
// Pass a stale version — the real adapter will reject it with conflict.
|
|
165
|
-
const res = await app.inject({
|
|
166
|
-
method: "DELETE",
|
|
167
|
-
url: "/v1/content/HomePage/home",
|
|
168
|
-
headers: { "if-match": "stale-version-that-does-not-match" },
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(res.statusCode).toBe(409);
|
|
172
|
-
const body = JSON.parse(res.body);
|
|
173
|
-
expect(body).toMatchObject({ error: "conflict" });
|
|
174
|
-
if (body.details) {
|
|
175
|
-
expect(body.details).toHaveProperty("currentVersion");
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("returns 404 not_found when adapter signals page missing", async () => {
|
|
180
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
181
|
-
|
|
182
|
-
const res = await app.inject({
|
|
183
|
-
method: "DELETE",
|
|
184
|
-
url: "/v1/content/HomePage/slug-that-does-not-exist",
|
|
185
|
-
headers: { "if-match": "any-version" },
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(res.statusCode).toBe(404);
|
|
189
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "not_found" });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("returns 400 validation_failed on unknown :type", async () => {
|
|
193
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
194
|
-
|
|
195
|
-
const res = await app.inject({
|
|
196
|
-
method: "DELETE",
|
|
197
|
-
url: "/v1/content/Bogus/home",
|
|
198
|
-
headers: { "if-match": "v1" },
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
expect(res.statusCode).toBe(400);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("returns 400 validation_failed on disallowed slug shape", async () => {
|
|
205
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
206
|
-
|
|
207
|
-
const res = await app.inject({
|
|
208
|
-
method: "DELETE",
|
|
209
|
-
url: "/v1/content/HomePage/..%2Fetc%2Fpasswd",
|
|
210
|
-
headers: { "if-match": "v1" },
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
expect(res.statusCode).toBe(400);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file update.test.ts
|
|
3
|
-
* PATCH /api/content/:type/:slug — D14a Stage 1 Unit B.
|
|
4
|
-
*
|
|
5
|
-
* Covers: Pro-tier gating (402), missing If-Match (409), stale If-Match (409
|
|
6
|
-
* with current version echoed), validation (400), happy path (200 with
|
|
7
|
-
* {page, version}), not-found (404), unknown :type (400).
|
|
8
|
-
*
|
|
9
|
-
* Issue #398 follow-up: bespoke version-probe + getPageBySlug pre-check
|
|
10
|
-
* removed; the adapter throws `AdapterWriteError("not_found" | "conflict")`
|
|
11
|
-
* and the hoisted `mapAdapterError` translates it.
|
|
12
|
-
*
|
|
13
|
-
* Uses the real content adapter backed by a per-test temp dir (Issue #507).
|
|
14
|
-
* @working-theory/license stays mocked — it is an external package boundary.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { FastifyInstance } from "fastify";
|
|
18
|
-
|
|
19
|
-
const mockCheckLicense = jest.fn();
|
|
20
|
-
|
|
21
|
-
// @working-theory/license is an external package boundary — stays mocked.
|
|
22
|
-
jest.mock("@working-theory/license", () => ({
|
|
23
|
-
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
import { AdapterWriteError } from "@working-theory/content-adapters";
|
|
27
|
-
|
|
28
|
-
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
29
|
-
import { contentRoutes } from "../index";
|
|
30
|
-
|
|
31
|
-
const fixture = setupContentFixture();
|
|
32
|
-
|
|
33
|
-
function proLicense() {
|
|
34
|
-
return {
|
|
35
|
-
tier: "pro",
|
|
36
|
-
plan: "pro",
|
|
37
|
-
features: [],
|
|
38
|
-
expiresAt: null,
|
|
39
|
-
revoked: false,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function freeLicense() {
|
|
44
|
-
return {
|
|
45
|
-
tier: "free",
|
|
46
|
-
plan: null,
|
|
47
|
-
features: [],
|
|
48
|
-
expiresAt: null,
|
|
49
|
-
revoked: false,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Update body must match the slug from the URL.
|
|
54
|
-
const validBody = {
|
|
55
|
-
type: "HomePage",
|
|
56
|
-
slug: "home",
|
|
57
|
-
title: "Welcome (updated)",
|
|
58
|
-
description: "Updated description",
|
|
59
|
-
body: "Updated body content",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
describe("PATCH /api/content/:type/:slug", () => {
|
|
63
|
-
let app: FastifyInstance;
|
|
64
|
-
// The write handlers only register when content-write is enabled
|
|
65
|
-
// (routes/content/index.ts's isModuleEnabled('content-write') gate); an
|
|
66
|
-
// unset AILK_ENABLED_MODULES no longer defaults to every module on
|
|
67
|
-
// (D-44, #5368), so this suite enables it explicitly.
|
|
68
|
-
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
69
|
-
|
|
70
|
-
beforeEach(async () => {
|
|
71
|
-
process.env.AILK_ENABLED_MODULES = "content-write";
|
|
72
|
-
app = require("fastify")();
|
|
73
|
-
await app.register(contentRoutes);
|
|
74
|
-
jest.clearAllMocks();
|
|
75
|
-
mockCheckLicense.mockReset();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterAll(() => {
|
|
79
|
-
if (priorEnabledModules === undefined) {
|
|
80
|
-
delete process.env.AILK_ENABLED_MODULES;
|
|
81
|
-
} else {
|
|
82
|
-
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
afterEach(async () => {
|
|
87
|
-
await app.close();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("returns 200 with {page, version} on success", async () => {
|
|
91
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
92
|
-
|
|
93
|
-
// The fixture writes "home" in beforeEach; PATCH it with If-Match.
|
|
94
|
-
// First POST to get the version header, then PATCH.
|
|
95
|
-
// For simplicity: the real MDX adapter returns a version on create/update.
|
|
96
|
-
// We pass a version token that matches — the adapter uses content hash.
|
|
97
|
-
// Get current version first by creating it via POST:
|
|
98
|
-
const createRes = await app.inject({
|
|
99
|
-
method: "POST",
|
|
100
|
-
url: "/v1/content/HomePage",
|
|
101
|
-
payload: {
|
|
102
|
-
type: "HomePage",
|
|
103
|
-
slug: "update-target",
|
|
104
|
-
title: "Original",
|
|
105
|
-
description: "Original description",
|
|
106
|
-
body: "Original body",
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
expect(createRes.statusCode).toBe(201);
|
|
110
|
-
const { version } = JSON.parse(createRes.body);
|
|
111
|
-
|
|
112
|
-
const res = await app.inject({
|
|
113
|
-
method: "PATCH",
|
|
114
|
-
url: "/v1/content/HomePage/update-target",
|
|
115
|
-
headers: { "if-match": version },
|
|
116
|
-
payload: {
|
|
117
|
-
type: "HomePage",
|
|
118
|
-
slug: "update-target",
|
|
119
|
-
title: "Welcome (updated)",
|
|
120
|
-
description: "Updated description",
|
|
121
|
-
body: "Updated body",
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(res.statusCode).toBe(200);
|
|
126
|
-
const body = JSON.parse(res.body);
|
|
127
|
-
expect(body).toHaveProperty("page");
|
|
128
|
-
expect(body).toHaveProperty("version");
|
|
129
|
-
expect(body.page).toMatchObject({ type: "HomePage", slug: "update-target" });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("returns 402 payment_required on free tier", async () => {
|
|
133
|
-
mockCheckLicense.mockResolvedValue(freeLicense());
|
|
134
|
-
|
|
135
|
-
const res = await app.inject({
|
|
136
|
-
method: "PATCH",
|
|
137
|
-
url: "/v1/content/HomePage/home",
|
|
138
|
-
headers: { "if-match": "v1" },
|
|
139
|
-
payload: validBody,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(res.statusCode).toBe(402);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("returns 402 payment_required when license is revoked even at pro tier", async () => {
|
|
146
|
-
mockCheckLicense.mockResolvedValue({
|
|
147
|
-
tier: "pro",
|
|
148
|
-
plan: "pro",
|
|
149
|
-
features: [],
|
|
150
|
-
expiresAt: null,
|
|
151
|
-
revoked: true,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const res = await app.inject({
|
|
155
|
-
method: "PATCH",
|
|
156
|
-
url: "/v1/content/HomePage/home",
|
|
157
|
-
headers: { "if-match": "v1" },
|
|
158
|
-
payload: validBody,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(res.statusCode).toBe(402);
|
|
162
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("returns 409 conflict when If-Match header is missing", async () => {
|
|
166
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
167
|
-
|
|
168
|
-
const res = await app.inject({
|
|
169
|
-
method: "PATCH",
|
|
170
|
-
url: "/v1/content/HomePage/home",
|
|
171
|
-
payload: validBody,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
expect(res.statusCode).toBe(409);
|
|
175
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("returns 409 conflict when If-Match is empty string", async () => {
|
|
179
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
180
|
-
|
|
181
|
-
const res = await app.inject({
|
|
182
|
-
method: "PATCH",
|
|
183
|
-
url: "/v1/content/HomePage/home",
|
|
184
|
-
headers: { "if-match": "" },
|
|
185
|
-
payload: validBody,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(res.statusCode).toBe(409);
|
|
189
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("returns 409 conflict with currentVersion echoed when adapter signals stale If-Match", async () => {
|
|
193
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
194
|
-
|
|
195
|
-
// Pass a stale version — the real adapter will reject it with a conflict.
|
|
196
|
-
const res = await app.inject({
|
|
197
|
-
method: "PATCH",
|
|
198
|
-
url: "/v1/content/HomePage/home",
|
|
199
|
-
headers: { "if-match": "stale-version-that-does-not-match" },
|
|
200
|
-
payload: validBody,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// The real adapter rejects a stale If-Match with 409 + currentVersion.
|
|
204
|
-
expect(res.statusCode).toBe(409);
|
|
205
|
-
const body = JSON.parse(res.body);
|
|
206
|
-
expect(body).toMatchObject({ error: "conflict" });
|
|
207
|
-
// currentVersion is echoed in details when the adapter provides it.
|
|
208
|
-
if (body.details) {
|
|
209
|
-
expect(body.details).toHaveProperty("currentVersion");
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("returns 404 not_found when adapter signals page missing", async () => {
|
|
214
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
215
|
-
|
|
216
|
-
const res = await app.inject({
|
|
217
|
-
method: "PATCH",
|
|
218
|
-
url: "/v1/content/HomePage/slug-that-does-not-exist",
|
|
219
|
-
headers: { "if-match": "any-version" },
|
|
220
|
-
payload: { ...validBody, slug: "slug-that-does-not-exist" },
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
expect(res.statusCode).toBe(404);
|
|
224
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "not_found" });
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("returns 400 validation_failed on bad body", async () => {
|
|
228
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
229
|
-
|
|
230
|
-
const res = await app.inject({
|
|
231
|
-
method: "PATCH",
|
|
232
|
-
url: "/v1/content/HomePage/home",
|
|
233
|
-
headers: { "if-match": "v1" },
|
|
234
|
-
payload: { type: "HomePage", slug: "home" }, // missing required fields
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
expect(res.statusCode).toBe(400);
|
|
238
|
-
expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("returns 400 validation_failed on unknown :type", async () => {
|
|
242
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
243
|
-
|
|
244
|
-
const res = await app.inject({
|
|
245
|
-
method: "PATCH",
|
|
246
|
-
url: "/v1/content/Bogus/home",
|
|
247
|
-
headers: { "if-match": "v1" },
|
|
248
|
-
payload: validBody,
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
expect(res.statusCode).toBe(400);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("returns 400 when path :type/:slug does not match body type/slug", async () => {
|
|
255
|
-
mockCheckLicense.mockResolvedValue(proLicense());
|
|
256
|
-
|
|
257
|
-
const res = await app.inject({
|
|
258
|
-
method: "PATCH",
|
|
259
|
-
url: "/v1/content/HomePage/home",
|
|
260
|
-
headers: { "if-match": "v1" },
|
|
261
|
-
payload: { ...validBody, slug: "different-slug" },
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
expect(res.statusCode).toBe(400);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* POST /v1/content/:type
|
|
4
|
-
*
|
|
5
|
-
* Creates a new page of `:type` from a JSON body matching `FrontmatterSchema`
|
|
6
|
-
* (from `@working-theory/content-adapters`, the SSOT for content shape) plus a non-empty
|
|
7
|
-
* `body` string. Paid-licensed deploys only — free tier returns 402 via the
|
|
8
|
-
* `requirePaidTier` preHandler from Stage 0.
|
|
9
|
-
*
|
|
10
|
-
* Conflict semantics: if a page with this slug already exists, returns 409
|
|
11
|
-
* with the existing page's `version` echoed in `details` so the caller can
|
|
12
|
-
* pivot to PATCH without a re-read. The conflict signal is delivered by the
|
|
13
|
-
* adapter via `AdapterWriteError("conflict", ..., { version })` and translated
|
|
14
|
-
* by the hoisted `mapAdapterError` (issue #398) — the route no longer
|
|
15
|
-
* pre-checks slug existence (closes the V2-H2 TOCTOU window between the
|
|
16
|
-
* read and the write).
|
|
17
|
-
*
|
|
18
|
-
* Errors all use the canonical `{error, message, details?}` shape from
|
|
19
|
-
* `apps/api/src/lib/errors.ts`.
|
|
20
|
-
*/
|
|
21
|
-
import {
|
|
22
|
-
FrontmatterSchema,
|
|
23
|
-
SCHEMA_ORG_TYPES,
|
|
24
|
-
type SchemaOrgType,
|
|
25
|
-
} from "@working-theory/content-adapters";
|
|
26
|
-
import type { FastifyPluginAsync, FastifyReply } from "fastify";
|
|
27
|
-
import { z } from "zod";
|
|
28
|
-
|
|
29
|
-
import { getContentAdapter } from "../../lib/content-adapter.js";
|
|
30
|
-
import {
|
|
31
|
-
adapterErrorResponse,
|
|
32
|
-
validationError,
|
|
33
|
-
type ErrorBody,
|
|
34
|
-
} from "../../lib/errors.js";
|
|
35
|
-
import { requirePaidTier } from "../../lib/require-paid-tier.js";
|
|
36
|
-
|
|
37
|
-
import { SLUG_PATTERN } from "./slug.js";
|
|
38
|
-
|
|
39
|
-
const paramsSchema = z.object({
|
|
40
|
-
type: z.enum(SCHEMA_ORG_TYPES),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const writeBodySchema = FrontmatterSchema.extend({ body: z.string().min(1) });
|
|
44
|
-
|
|
45
|
-
function send(reply: FastifyReply, status: number, body: ErrorBody) {
|
|
46
|
-
return reply.status(status).send(body);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const createPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
50
|
-
fastify.post<{
|
|
51
|
-
Params: { type: string };
|
|
52
|
-
Body: unknown;
|
|
53
|
-
}>(
|
|
54
|
-
"/v1/content/:type",
|
|
55
|
-
|
|
56
|
-
{ preHandler: requirePaidTier },
|
|
57
|
-
async (request, reply) => {
|
|
58
|
-
const params = paramsSchema.safeParse(request.params);
|
|
59
|
-
if (!params.success) {
|
|
60
|
-
const err = validationError("Invalid type", {
|
|
61
|
-
issues: params.error.issues,
|
|
62
|
-
});
|
|
63
|
-
return send(reply, err.status, err.body);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const parsed = writeBodySchema.safeParse(request.body);
|
|
67
|
-
if (!parsed.success) {
|
|
68
|
-
const err = validationError("Invalid request body", {
|
|
69
|
-
issues: parsed.error.issues,
|
|
70
|
-
});
|
|
71
|
-
return send(reply, err.status, err.body);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { body: pageBody, ...page } = parsed.data;
|
|
75
|
-
|
|
76
|
-
if (!SLUG_PATTERN.test(page.slug)) {
|
|
77
|
-
const err = validationError("Invalid request body", {
|
|
78
|
-
issues: [
|
|
79
|
-
{
|
|
80
|
-
path: ["slug"],
|
|
81
|
-
message:
|
|
82
|
-
"slug must match /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (lowercase, digits, hyphens; no leading/trailing hyphen)",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
return send(reply, err.status, err.body);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (page.type !== params.data.type) {
|
|
90
|
-
const err = validationError(
|
|
91
|
-
"Body `type` must match URL :type parameter",
|
|
92
|
-
{ urlType: params.data.type, bodyType: page.type },
|
|
93
|
-
);
|
|
94
|
-
return send(reply, err.status, err.body);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const { type } = params.data;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const adapter = getContentAdapter();
|
|
101
|
-
const result = await adapter.createPage(
|
|
102
|
-
type as SchemaOrgType,
|
|
103
|
-
page.slug,
|
|
104
|
-
{ ...page, body: pageBody },
|
|
105
|
-
);
|
|
106
|
-
return reply.status(201).send(result);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const err = adapterErrorResponse(error, { type, slug: page.slug });
|
|
109
|
-
if (err) return send(reply, err.status, err.body);
|
|
110
|
-
request.log.error({ err: error }, "[Content] createPage failed");
|
|
111
|
-
return reply.status(500).send({ error: "Internal server error" });
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
);
|
|
115
|
-
};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* DELETE /v1/content/:type/:slug
|
|
4
|
-
*
|
|
5
|
-
* Removes the page of `:type` at `:slug`. Paid-licensed deploys only.
|
|
6
|
-
*
|
|
7
|
-
* Optimistic locking is mandatory: the request MUST carry an `If-Match`
|
|
8
|
-
* header containing the `version` token from a prior read. Missing/empty →
|
|
9
|
-
* 409. Stale → 409 with `details.currentVersion` echoed.
|
|
10
|
-
*
|
|
11
|
-
* 404 when the page does not exist. Success responds 204 No Content.
|
|
12
|
-
*
|
|
13
|
-
* Adapter errors (`AdapterWriteError("not_found" | "conflict" | …)`) are
|
|
14
|
-
* translated by the hoisted `mapAdapterError` from `@working-theory/content-adapters`
|
|
15
|
-
* (issue #398) — single source of truth across the API + MCP surfaces.
|
|
16
|
-
*/
|
|
17
|
-
import {
|
|
18
|
-
SCHEMA_ORG_TYPES,
|
|
19
|
-
type SchemaOrgType,
|
|
20
|
-
} from "@working-theory/content-adapters";
|
|
21
|
-
import type { FastifyPluginAsync, FastifyReply } from "fastify";
|
|
22
|
-
import { z } from "zod";
|
|
23
|
-
|
|
24
|
-
import { getContentAdapter } from "../../lib/content-adapter.js";
|
|
25
|
-
import {
|
|
26
|
-
adapterErrorResponse,
|
|
27
|
-
conflictError,
|
|
28
|
-
validationError,
|
|
29
|
-
type ErrorBody,
|
|
30
|
-
} from "../../lib/errors.js";
|
|
31
|
-
import { requirePaidTier } from "../../lib/require-paid-tier.js";
|
|
32
|
-
|
|
33
|
-
import { SLUG_PATTERN } from "./slug.js";
|
|
34
|
-
|
|
35
|
-
const paramsSchema = z.object({
|
|
36
|
-
type: z.enum(SCHEMA_ORG_TYPES),
|
|
37
|
-
slug: z.string().regex(SLUG_PATTERN),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
function send(reply: FastifyReply, status: number, body: ErrorBody) {
|
|
41
|
-
return reply.status(status).send(body);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const deletePageRoute: FastifyPluginAsync = async (fastify) => {
|
|
45
|
-
fastify.delete<{
|
|
46
|
-
Params: { type: string; slug: string };
|
|
47
|
-
}>(
|
|
48
|
-
"/v1/content/:type/:slug",
|
|
49
|
-
|
|
50
|
-
{ preHandler: requirePaidTier },
|
|
51
|
-
async (request, reply) => {
|
|
52
|
-
const params = paramsSchema.safeParse(request.params);
|
|
53
|
-
if (!params.success) {
|
|
54
|
-
const err = validationError("Invalid type or slug", {
|
|
55
|
-
issues: params.error.issues,
|
|
56
|
-
});
|
|
57
|
-
return send(reply, err.status, err.body);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const ifMatch = request.headers["if-match"];
|
|
61
|
-
if (typeof ifMatch !== "string" || ifMatch.length === 0) {
|
|
62
|
-
const err = conflictError(
|
|
63
|
-
"Missing required If-Match header (version token)",
|
|
64
|
-
);
|
|
65
|
-
return send(reply, err.status, err.body);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const { type, slug } = params.data;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const adapter = getContentAdapter();
|
|
72
|
-
await adapter.deletePage(type as SchemaOrgType, slug, { ifMatch });
|
|
73
|
-
return reply.status(204).send();
|
|
74
|
-
} catch (error) {
|
|
75
|
-
const err = adapterErrorResponse(error, { type, slug });
|
|
76
|
-
if (err) return send(reply, err.status, err.body);
|
|
77
|
-
request.log.error({ err: error }, "[Content] deletePage failed");
|
|
78
|
-
return reply.status(500).send({ error: "Internal server error" });
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
);
|
|
82
|
-
};
|