Skip to content

feat(api): site-scoped public API contract and beta hardening - #37

Merged
krishna-santosh merged 32 commits into
mainfrom
api
Oct 8, 2026
Merged

krishna-santosh merged 32 commits into
mainfrom
api

Conversation

@krishna-santosh

Copy link
Copy Markdown
Member

Summary

Makes the public API one site-scoped contract across REST, GraphQL, gRPC and MCP, fixes the issues found in an audit of the branch, and rewrites the API docs to match.

Contract

  • Every protocol names the site explicitly: REST /api/v1/sites/{site_id}/…, GraphQL site(id:) plus mutation siteId, gRPC site_id, MCP site_id.
  • Scopes: site.read, schema.read, content.read, content.preview.read, content.write, content.publish, files.read, files.write, mcp.use. Scopes for dashboard-only areas are removed.
  • Drafts need include_drafts and content.preview.read. Entry and singleton updates accept an expected version (REST If-Match).
  • Schema, webhook, deployment, member, key and backup management are dashboard-only.
  • GraphQL root is sites and site(id:). site(id:) is a namespace whose fields each authorize themselves.
  • gRPC uses Struct/Timestamp, token paging, and file URLs. The JSON-text twin fields are removed.

Fixes

  • Request headers (bearer tokens, session cookies) were recorded in tracing spans.
  • Concurrent SQLite writes failed with database is locked; write transactions now use BEGIN IMMEDIATE.
  • MCP rejected 2025-06-18/2025-11-25 clients, and the stdio proxy broke initialize for all of them.
  • Content APIs shared the 100/min login limiter, and GraphQL and gRPC had no limit. They now share a separate 1200/min-per-client limiter.
  • tools/list stopped cleaning input schemas.
  • 403s now name the missing scope, an unknown collection returns 404, and a malformed cursor gets a clear 400.

Cleanup

  • One baseline migration per backend. Cross-site ownership uses composite foreign keys.
  • Removes unreachable code: an orphaned storage trait, unused repository methods, the read-only search mode, and unused helpers.
  • AGENTS.md is the single agent guide; CLAUDE.md imports it.

Breaking for existing dev setups

The baseline migration changed, so delete local vcms_data/ before running. The product has not launched, so this PR carries no data migration.

Test plan

  • cargo fmt --check, cargo clippy --no-default-features --all-targets -- -D warnings
  • Unit (349), REST (124), GraphQL (44), gRPC (30), MCP (55) on SQLite
  • Postgres suite (bun run test:pg), run before the rate-limit and cleanup commits
  • Dashboard typecheck and biome ci; web build
  • Manual: legacy and 2026-07-28 MCP handshakes over HTTP and stdio
  • Packaging dry-runs in CI

🤖 Generated with Claude Code

krishna-santosh and others added 27 commits August 10, 2026 19:42
Adapt handlers to MRTR response enums, result metadata defaults, and renamed session configuration.

BREAKING CHANGE: MCP handlers now target rmcp 3 response types.
Refresh workspace, dashboard, and web package ranges with Bun lockfile.

BREAKING CHANGE: TanStack Table 9 and framework majors require API migrations.
Register table features and row models explicitly under the v9 API.

BREAKING CHANGE: Data table integration now uses TanStack Table 9 APIs.
Use the current Orama static client and remove the obsolete direct Orama setup.
Keep inline animation styles compatible with updated React type definitions.
… and MCP

Move public REST under /api/v1/sites/{site_id}, add preview/publish scopes,
optimistic entry versions, typed gRPC payloads, signed upload replay
protection, and drop schema/webhook management from public adapters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update_public_entry and update_singleton recorded their HeaderMap in the
instrument span, writing bearer tokens, session cookies and CSRF tokens
to the log files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deferred transactions that read before writing failed the lock upgrade
with SQLITE_BUSY immediately, bypassing busy_timeout, so concurrent
edits returned 500. Begin SQLite write transactions with BEGIN IMMEDIATE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One baseline migration per backend. Cross-site ownership is now enforced
declaratively with composite foreign keys instead of trigger sets; entry
versions, per-field file references and signed upload uses are part of the
baseline. Drop legacy-archive restore defaults and the upgrade guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Site settings, schema write, webhook and deployment scopes only guarded
dashboard-only operations that tokens can no longer reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Negotiate the session-lifecycle revisions again, and let the stdio proxy
forward the client's own protocol version instead of stamping 2026-07-28
on every message, which broke initialize for every stdio client.
Re-apply input schema cleanup in tools/list and return object-shaped
structured output from list_sites and list_singletons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 403s name the missing token scope; hard-denied actions say tokens
  cannot reach them
- unknown collection slugs return 404 from the entry list
- public entry and file lists continue only through cursors; malformed
  cursors get their own 400 message
- drop token branches from dashboard-only entry handlers and OpenAPI
  annotations from handlers the public spec does not include

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- content and definitions are google.protobuf.Struct and timestamps are
  google.protobuf.Timestamp; the JSON-text and string-timestamp twins
  are gone
- every list pages with page_size/page_token only, revisions included
- every site-scoped request requires site_id; no implicit site from a
  site key
- files carry delivery URLs instead of storage keys; DeleteResponse
  reports deleted and missing ids return NOT_FOUND
- token verification reuses the REST verifier, so gRPC no longer keeps
  its own copy, and permission errors name the missing scope
- uploads honour the live instance upload limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root now exposes only sites and site(id:). site(id:) is a namespace:
each nested field authorizes its own action, so a content-only token
reads entries without site.read, matching REST. Move entry, collection,
file, file reference and revision reads onto Site; drop currentSite, the
flat root queries, the X-VCMS-Site header, unsupported filter/orderBy
inputs, fake page cursors and unreachable batch file mutations. Rename
Collection.entry to entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Public REST and MCP shared the 100-per-minute login limiter, so a site
build could lock out sign-in and hit 429 quickly, while GraphQL and gRPC
had no limit at all. Content APIs now share a separate per-client limiter
(1200 requests per minute) and login keeps its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- orphaned storage/trait_def.rs, never compiled into the crate
- repository field-rename migration and file reference methods with no
  callers
- read-only search index mode, now that vcms mcp stdio proxies over HTTP
- Services::new_read_only and unused backup, storage, content-type,
  error and test helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other read in every protocol uses include_drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- authentication: scope table, credential types, and the per-protocol
  site selection rule
- REST, GraphQL, gRPC and MCP guides rewritten for site-scoped reads,
  conditional updates, cursor and token paging, and supported MCP
  revisions
- files: uploads in every protocol and how references are tracked
- webhooks and deployments are dashboard-only
- queries and errors: versions, problem+json, rate limits
- fold the coverage table and known limits into the reference pages and
  drop the unlinked architecture page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md and AGENTS.md had drifted apart and both described removed
migrations, env vars, test files and the old API key model. Merge them
into an accurate AGENTS.md and import it from CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 225 files, which is 125 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Configuration used: Repository: velopulent/cms/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3d2f715f-3716-4c0b-ba9f-5b9722ef3a3c
📥 Commits

Reviewing files that changed from the base of the PR and between 8ae1f8b and 95ea5fd.

⛔ Files ignored due to path filters (11)
  • Cargo.lock is excluded by !**/*.lock
  • apps/dashboard/bun.lock is excluded by !**/*.lock
  • apps/web/public/docs/screenshots/backups-dark.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/backups-light.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/collections-dark.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/collections-light.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/dashboard-overview-dark.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/dashboard-overview-light.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/posts-dark.png is excluded by !**/*.png
  • apps/web/public/docs/screenshots/posts-light.png is excluded by !**/*.png
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (225)
  • AGENTS.md
  • CLAUDE.md
  • apps/backend/Cargo.toml
  • apps/backend/migrations/postgres/20260804000000_initial_schema.sql
  • apps/backend/migrations/sqlite/20260804000000_initial_schema.sql
  • apps/backend/src/config.rs
  • apps/backend/src/error.rs
  • apps/backend/src/graphql/context.rs
  • apps/backend/src/graphql/loaders.rs
  • apps/backend/src/graphql/mod.rs
  • apps/backend/src/graphql/mutation/collection.rs
  • apps/backend/src/graphql/mutation/entry.rs
  • apps/backend/src/graphql/mutation/file.rs
  • apps/backend/src/graphql/mutation/mod.rs
  • apps/backend/src/graphql/mutation/singleton.rs
  • apps/backend/src/graphql/mutation/webhook.rs
  • apps/backend/src/graphql/query/mod.rs
  • apps/backend/src/graphql/schema.rs
  • apps/backend/src/graphql/types/collection.rs
  • apps/backend/src/graphql/types/entry.rs
  • apps/backend/src/graphql/types/file.rs
  • apps/backend/src/graphql/types/json.rs
  • apps/backend/src/graphql/types/mod.rs
  • apps/backend/src/graphql/types/site.rs
  • apps/backend/src/graphql/types/webhook.rs
  • apps/backend/src/grpc/auth.rs
  • apps/backend/src/grpc/interceptor.rs
  • apps/backend/src/grpc/mod.rs
  • apps/backend/src/grpc/server.rs
  • apps/backend/src/grpc/services/admin_site.rs
  • apps/backend/src/grpc/services/admin_webhook.rs
  • apps/backend/src/grpc/services/collection.rs
  • apps/backend/src/grpc/services/entry.rs
  • apps/backend/src/grpc/services/file.rs
  • apps/backend/src/grpc/services/mod.rs
  • apps/backend/src/grpc/services/singleton.rs
  • apps/backend/src/handlers/collection_handler.rs
  • apps/backend/src/handlers/entry_handler.rs
  • apps/backend/src/handlers/file_handler.rs
  • apps/backend/src/handlers/singleton_handler.rs
  • apps/backend/src/handlers/site_handler.rs
  • apps/backend/src/handlers/webhook_handler.rs
  • apps/backend/src/main.rs
  • apps/backend/src/mcp/auth.rs
  • apps/backend/src/mcp/resources/site_schema.rs
  • apps/backend/src/mcp/server.rs
  • apps/backend/src/mcp/tools/collection.rs
  • apps/backend/src/mcp/tools/entry.rs
  • apps/backend/src/mcp/tools/file.rs
  • apps/backend/src/mcp/tools/mod.rs
  • apps/backend/src/mcp/tools/singleton.rs
  • apps/backend/src/mcp/tools/site.rs
  • apps/backend/src/mcp/tools/webhook.rs
  • apps/backend/src/mcp/transports/http.rs
  • apps/backend/src/mcp/transports/stdio.rs
  • apps/backend/src/middleware/api_auth.rs
  • apps/backend/src/middleware/auth.rs
  • apps/backend/src/middleware/error.rs
  • apps/backend/src/middleware/rate_limit.rs
  • apps/backend/src/middleware/site_resolver.rs
  • apps/backend/src/models/access_token.rs
  • apps/backend/src/models/authorization.rs
  • apps/backend/src/models/collection.rs
  • apps/backend/src/models/entry.rs
  • apps/backend/src/repository/error.rs
  • apps/backend/src/repository/postgres/collection.rs
  • apps/backend/src/repository/postgres/entry.rs
  • apps/backend/src/repository/postgres/file.rs
  • apps/backend/src/repository/postgres/webhook.rs
  • apps/backend/src/repository/sqlite/collection.rs
  • apps/backend/src/repository/sqlite/entry.rs
  • apps/backend/src/repository/sqlite/file.rs
  • apps/backend/src/repository/sqlite/site.rs
  • apps/backend/src/repository/sqlite/webhook.rs
  • apps/backend/src/repository/traits.rs
  • apps/backend/src/router/collections.rs
  • apps/backend/src/router/docs.rs
  • apps/backend/src/router/entry.rs
  • apps/backend/src/router/files.rs
  • apps/backend/src/router/graphql.rs
  • apps/backend/src/router/mod.rs
  • apps/backend/src/router/openapi.rs
  • apps/backend/src/router/singleton.rs
  • apps/backend/src/router/webhooks.rs
  • apps/backend/src/server.rs
  • apps/backend/src/services/access_token.rs
  • apps/backend/src/services/authorization.rs
  • apps/backend/src/services/backup/meta.rs
  • apps/backend/src/services/backup/mod.rs
  • apps/backend/src/services/backup/schema.rs
  • apps/backend/src/services/collection.rs
  • apps/backend/src/services/deployment.rs
  • apps/backend/src/services/entry.rs
  • apps/backend/src/services/error.rs
  • apps/backend/src/services/file.rs
  • apps/backend/src/services/mod.rs
  • apps/backend/src/services/search/mod.rs
  • apps/backend/src/services/search/schema.rs
  • apps/backend/src/services/singleton.rs
  • apps/backend/src/services/site.rs
  • apps/backend/src/services/storage_profile.rs
  • apps/backend/src/services/webhook.rs
  • apps/backend/src/signed_upload.rs
  • apps/backend/src/storage/filesystem.rs
  • apps/backend/src/storage/mod.rs
  • apps/backend/src/storage/s3.rs
  • apps/backend/src/storage/trait_def.rs
  • apps/backend/src/test_helpers.rs
  • apps/backend/src/tracing.rs
  • apps/backend/src/utils/content_types.rs
  • apps/backend/src/utils/cursor.rs
  • apps/backend/src/utils/file_references.rs
  • apps/backend/src/utils/mod.rs
  • apps/backend/src/utils/timestamp.rs
  • apps/backend/tests/common/fixtures.rs
  • apps/backend/tests/common/grpc.rs
  • apps/backend/tests/common/mcp.rs
  • apps/backend/tests/graphql/auth_tests.rs
  • apps/backend/tests/graphql/collections_tests.rs
  • apps/backend/tests/graphql/contract_tests.rs
  • apps/backend/tests/graphql/entries_tests.rs
  • apps/backend/tests/graphql/files_tests.rs
  • apps/backend/tests/graphql/main.rs
  • apps/backend/tests/graphql/sites_tests.rs
  • apps/backend/tests/graphql/webhooks_tests.rs
  • apps/backend/tests/grpc/collections_tests.rs
  • apps/backend/tests/grpc/entries_tests.rs
  • apps/backend/tests/grpc/files_tests.rs
  • apps/backend/tests/grpc/main.rs
  • apps/backend/tests/grpc/singletons_tests.rs
  • apps/backend/tests/grpc/sites_tests.rs
  • apps/backend/tests/grpc/webhooks_tests.rs
  • apps/backend/tests/mcp/collection_tests.rs
  • apps/backend/tests/mcp/contract_tests.rs
  • apps/backend/tests/mcp/entry_tests.rs
  • apps/backend/tests/mcp/file_tests.rs
  • apps/backend/tests/mcp/main.rs
  • apps/backend/tests/mcp/protocol_tests.rs
  • apps/backend/tests/mcp/singleton_tests.rs
  • apps/backend/tests/mcp/site_tests.rs
  • apps/backend/tests/mcp/stdio_proxy_tests.rs
  • apps/backend/tests/mcp/webhook_tests.rs
  • apps/backend/tests/rest/access_tokens_tests.rs
  • apps/backend/tests/rest/collections_tests.rs
  • apps/backend/tests/rest/files_tests.rs
  • apps/backend/tests/rest/main.rs
  • apps/backend/tests/rest/public_contract_tests.rs
  • apps/dashboard/package.json
  • apps/dashboard/src/components/account/personal-tokens-card.tsx
  • apps/dashboard/src/components/entries/columns.tsx
  • apps/dashboard/src/components/ui/data-table.tsx
  • apps/dashboard/src/integrations/tanstack-query/devtools.tsx
  • apps/dashboard/src/lib/api.ts
  • apps/dashboard/src/routeTree.gen.ts
  • apps/dashboard/src/routes/__root.tsx
  • apps/dashboard/src/styles.css
  • apps/dashboard/vite.config.ts
  • apps/web/content/docs/(cms)/explanation/content-lifecycle.mdx
  • apps/web/content/docs/(cms)/explanation/content-model.mdx
  • apps/web/content/docs/(cms)/explanation/meta.json
  • apps/web/content/docs/(cms)/explanation/sites-and-roles.mdx
  • apps/web/content/docs/(cms)/guides/account.mdx
  • apps/web/content/docs/(cms)/guides/backups.mdx
  • apps/web/content/docs/(cms)/guides/content-modeling.mdx
  • apps/web/content/docs/(cms)/guides/entries.mdx
  • apps/web/content/docs/(cms)/guides/files.mdx
  • apps/web/content/docs/(cms)/guides/meta.json
  • apps/web/content/docs/(cms)/guides/team.mdx
  • apps/web/content/docs/(cms)/guides/webhooks-deployments.mdx
  • apps/web/content/docs/(cms)/index.mdx
  • apps/web/content/docs/(cms)/meta.json
  • apps/web/content/docs/(cms)/reference/dashboard.mdx
  • apps/web/content/docs/(cms)/reference/fields.mdx
  • apps/web/content/docs/(cms)/reference/meta.json
  • apps/web/content/docs/(cms)/reference/permissions.mdx
  • apps/web/content/docs/(cms)/tutorials/editorial-site.mdx
  • apps/web/content/docs/(cms)/tutorials/meta.json
  • apps/web/content/docs/api/authentication.mdx
  • apps/web/content/docs/api/guides/files.mdx
  • apps/web/content/docs/api/guides/graphql.mdx
  • apps/web/content/docs/api/guides/grpc.mdx
  • apps/web/content/docs/api/guides/mcp.mdx
  • apps/web/content/docs/api/guides/meta.json
  • apps/web/content/docs/api/guides/rest.mdx
  • apps/web/content/docs/api/guides/static-sites.mdx
  • apps/web/content/docs/api/guides/webhooks-deployments.mdx
  • apps/web/content/docs/api/index.mdx
  • apps/web/content/docs/api/meta.json
  • apps/web/content/docs/api/quickstart.mdx
  • apps/web/content/docs/api/reference/meta.json
  • apps/web/content/docs/api/reference/operations.mdx
  • apps/web/content/docs/api/reference/protocols.mdx
  • apps/web/content/docs/api/reference/queries-errors.mdx
  • apps/web/content/docs/index.mdx
  • apps/web/content/docs/meta.json
  • apps/web/content/docs/server/first-start.mdx
  • apps/web/content/docs/server/guides/backups.mdx
  • apps/web/content/docs/server/guides/meta.json
  • apps/web/content/docs/server/guides/production.mdx
  • apps/web/content/docs/server/guides/recovery.mdx
  • apps/web/content/docs/server/guides/services.mdx
  • apps/web/content/docs/server/guides/storage.mdx
  • apps/web/content/docs/server/guides/troubleshooting.mdx
  • apps/web/content/docs/server/guides/upgrades.mdx
  • apps/web/content/docs/server/index.mdx
  • apps/web/content/docs/server/install.mdx
  • apps/web/content/docs/server/meta.json
  • apps/web/content/docs/server/reference/cli.mdx
  • apps/web/content/docs/server/reference/configuration.mdx
  • apps/web/content/docs/server/reference/data-layout.mdx
  • apps/web/content/docs/server/reference/health-security.mdx
  • apps/web/content/docs/server/reference/meta.json
  • apps/web/package.json
  • apps/web/src/app/docs/docs.css
  • apps/web/src/components/adaptive-screenshot.tsx
  • apps/web/src/components/landing/how-it-works-section.tsx
  • apps/web/src/components/landing/testimonials-section.tsx
  • apps/web/src/components/mdx.tsx
  • apps/web/src/components/search.tsx
  • apps/web/src/lib/source.ts
  • deny.toml
  • libs/proto/cms.proto
  • libs/proto/google/protobuf/struct.proto
  • libs/proto/google/protobuf/timestamp.proto
  • package.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

krishna-santosh and others added 2 commits October 8, 2026 18:39
…-0285

Run cargo update across the workspace. Notably h2 0.4.15 -> 0.4.20
(unbounded empty DATA frames) and rustls 0.23.43 -> 0.23.45 with
rustls-webpki 0.103.15 (TLS 1.3 handshake messages accepted across
encryption levels). rmcp 3.5 renames ServerInfo to ServerConfig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
croner 4 rejects shortcut step syntax such as 5/5; write 5-59/5. The
dashboard presets are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
krishna-santosh and others added 3 commits October 8, 2026 18:49
Parameters are RefOr values in utoipa 6. Also mark the injected site_id
path parameter as required, which OpenAPI demands of path parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
object_store 0.14.2 depends on quick-xml 0.41, which fixes
RUSTSEC-2026-0194 and RUSTSEC-2026-0195.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clears the open Next.js advisories (image optimizer and next/og RCE,
server action SSRF and DoS, cache confusion). The site is a static
export, so production was not exposed, but next dev was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Deploying cms with  Cloudflare Pages  Cloudflare Pages

Latest commit: 95ea5fd
Status: ✅  Deploy successful!
Preview URL: https://6be05a4d.cms-8f5.pages.dev
Branch Preview URL: https://api.cms-8f5.pages.dev

View logs

@krishna-santosh
krishna-santosh merged commit cff243e into main Oct 8, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant