From 0b6ede47878bd6c1ea67bca9dbc2ad3021acf818 Mon Sep 17 00:00:00 2001 From: Mateusz Gawlik Date: Mon, 27 Jul 2026 21:59:28 +0200 Subject: [PATCH 01/30] Complete Git-native visual CMS product flows --- .env.example | 15 + .github/workflows/cms-schedules.yml | 30 + apps/docs/src/content/docs/acceptance.md | 65 + apps/docs/src/content/docs/adapters.md | 43 +- apps/docs/src/content/docs/astro.md | 26 +- .../content/docs/changes-and-publishing.md | 25 +- .../docs/src/content/docs/content-modeling.md | 21 +- .../src/content/docs/doctor-and-upgrades.md | 24 +- apps/docs/src/content/docs/getting-started.md | 80 +- apps/docs/src/content/docs/mcp.md | 23 +- apps/docs/src/content/docs/nextjs.md | 46 +- apps/docs/src/content/docs/sandbox.md | 70 +- apps/docs/src/content/docs/security.md | 36 +- apps/playground-astro/package.json | 1 + apps/playground-astro/src/cms-fixture.ts | 22 + apps/playground-astro/src/cms-registry.tsx | 33 +- apps/playground-astro/src/cms-runtime.ts | 17 + .../src/components/CmsDemo.tsx | 57 +- .../src/components/PreviewDemo.tsx | 25 + .../src/pages/[...slug].astro | 57 +- .../pages/[cmsRoot]/preview/[...slug].astro | 16 + .../src/pages/cms/[...path].astro | 2 +- apps/playground-astro/src/pages/index.astro | 22 +- apps/playground-astro/src/public-content.ts | 102 +- apps/playground-astro/vercel.json | 41 +- .../app/[locale]/[[...slug]]/page.tsx | 78 + .../app/cms/[[...path]]/page.tsx | 10 +- apps/playground-next/app/layout.tsx | 7 +- apps/playground-next/app/page.tsx | 39 +- apps/playground-next/app/site.css | 22 + apps/playground-next/cms.fixture.ts | 24 + apps/playground-next/cms.registry.tsx | 48 +- apps/playground-next/cms.runtime.ts | 17 + apps/playground-next/components/cms-demo.tsx | 61 +- .../components/preview-demo.tsx | 25 + apps/playground-next/package.json | 1 + apps/playground-next/proxy.ts | 12 + apps/playground-next/public-content.ts | 102 +- apps/playground-next/vercel.json | 41 +- package.json | 5 + packages/adapter-kit/package.json | 1 + packages/adapter-kit/src/all-ports.test.ts | 263 +++ packages/adapter-kit/src/contracts.test.ts | 208 +++ packages/adapter-kit/src/contracts.ts | 592 ++++++ packages/adapter-kit/src/index.ts | 2 + packages/application/package.json | 5 +- packages/application/src/index.ts | 1397 +++++++++++++- packages/application/src/ports.ts | 62 + packages/assets/package.json | 4 + packages/assets/src/index.test.ts | 73 +- packages/assets/src/index.ts | 312 +++- packages/assets/src/s3.integration.test.ts | 87 + packages/astro-renderer/src/index.ts | 94 +- packages/astro/package.json | 14 +- packages/astro/src/index.ts | 7 + packages/astro/src/registry.ts | 6 + packages/astro/styles.css | 1 + packages/cli/src/index.test.ts | 99 + packages/cli/src/index.ts | 531 +++++- packages/content-codecs/src/index.test.ts | 14 + packages/content-codecs/src/index.ts | 59 +- packages/delivery/package.json | 3 + packages/delivery/src/index.test.ts | 11 + packages/delivery/src/index.ts | 112 +- packages/delivery/src/s3.integration.test.ts | 36 +- packages/editor-bridge/src/index.ts | 111 +- packages/editor-ui/src/styles.css | 943 +++++++++- packages/editor/package.json | 7 + packages/editor/src/index.tsx | 1650 ++++++++++++++++- packages/editor/src/styles.d.ts | 1 + packages/github/package.json | 3 + packages/github/src/index.test.ts | 169 ++ packages/github/src/index.ts | 28 +- packages/hosted-runtime/package.json | 20 +- packages/hosted-runtime/src/client.ts | 359 +++- packages/hosted-runtime/src/index.ts | 1176 ++++++++++-- packages/hosted-runtime/src/react.tsx | 307 +++ .../hosted-runtime/src/runtime-state.test.ts | 36 + packages/hosted-runtime/src/runtime-state.ts | 120 ++ packages/hosted-runtime/tsconfig.json | 2 +- packages/image-pipeline/package.json | 2 + packages/image-pipeline/src/index.ts | 69 +- packages/integrations/src/index.test.ts | 78 +- packages/integrations/src/index.ts | 281 ++- packages/localization/src/index.test.ts | 42 +- packages/localization/src/index.ts | 54 + packages/mcp/package.json | 4 + packages/mcp/src/index.test.ts | 94 + packages/mcp/src/index.ts | 256 ++- packages/migrations/src/index.test.ts | 26 + packages/next/package.json | 13 +- packages/next/src/editor.tsx | 2 + packages/next/src/registry.ts | 6 + packages/next/src/server.ts | 6 + packages/next/styles.css | 1 + packages/protocol/src/index.test.ts | 34 + packages/protocol/src/index.ts | 30 +- packages/protocol/src/preview.ts | 128 +- packages/react/src/index.test.ts | 78 + packages/react/src/index.tsx | 95 +- packages/release-builder/src/index.test.ts | 25 +- packages/release-builder/src/index.ts | 52 +- packages/search/src/index.test.ts | 28 +- packages/search/src/index.ts | 79 + packages/server/src/index.test.ts | 195 ++ packages/server/src/index.ts | 560 +++++- packages/sessions/src/index.test.ts | 24 + packages/testing/package.json | 1 + packages/testing/src/application.test.ts | 313 +++- packages/testing/src/index.ts | 59 +- playwright.config.ts | 3 +- pnpm-lock.yaml | 220 +++ pnpm-workspace.yaml | 4 + tests/e2e/editor.spec.ts | 109 ++ .../snapshots/astro-chromium/editor-dark.png | Bin 0 -> 125946 bytes .../snapshots/astro-chromium/editor-light.png | Bin 0 -> 127073 bytes .../snapshots/next-chromium/editor-dark.png | Bin 0 -> 133902 bytes .../snapshots/next-chromium/editor-light.png | Bin 0 -> 134256 bytes tooling/scripts/bootstrap-sandbox-release.ts | 12 +- tooling/scripts/check-bundle-budgets.mjs | 53 +- tooling/scripts/configure-r2.mjs | 60 +- tooling/scripts/configure-vercel-runtime.mjs | 73 + tooling/scripts/e2e-live.ts | 362 ++++ tooling/scripts/registry-digest.ts | 39 + tooling/scripts/smoke-live.mjs | 109 ++ turbo.json | 9 + vitest.config.ts | 6 +- 127 files changed, 13278 insertions(+), 732 deletions(-) create mode 100644 .github/workflows/cms-schedules.yml create mode 100644 apps/docs/src/content/docs/acceptance.md create mode 100644 apps/playground-astro/src/pages/[cmsRoot]/preview/[...slug].astro create mode 100644 apps/playground-next/app/[locale]/[[...slug]]/page.tsx create mode 100644 apps/playground-next/proxy.ts create mode 100644 packages/adapter-kit/src/all-ports.test.ts create mode 100644 packages/adapter-kit/src/contracts.test.ts create mode 100644 packages/adapter-kit/src/contracts.ts create mode 100644 packages/assets/src/s3.integration.test.ts create mode 100644 packages/astro/src/registry.ts create mode 100644 packages/astro/styles.css create mode 100644 packages/cli/src/index.test.ts create mode 100644 packages/delivery/src/index.test.ts create mode 100644 packages/editor/src/styles.d.ts create mode 100644 packages/github/src/index.test.ts create mode 100644 packages/hosted-runtime/src/react.tsx create mode 100644 packages/hosted-runtime/src/runtime-state.test.ts create mode 100644 packages/mcp/src/index.test.ts create mode 100644 packages/migrations/src/index.test.ts create mode 100644 packages/next/src/registry.ts create mode 100644 packages/next/styles.css create mode 100644 packages/react/src/index.test.ts create mode 100644 packages/server/src/index.test.ts create mode 100644 tests/e2e/snapshots/astro-chromium/editor-dark.png create mode 100644 tests/e2e/snapshots/astro-chromium/editor-light.png create mode 100644 tests/e2e/snapshots/next-chromium/editor-dark.png create mode 100644 tests/e2e/snapshots/next-chromium/editor-light.png create mode 100644 tooling/scripts/configure-vercel-runtime.mjs create mode 100644 tooling/scripts/e2e-live.ts create mode 100644 tooling/scripts/registry-digest.ts create mode 100644 tooling/scripts/smoke-live.mjs diff --git a/.env.example b/.env.example index 1742768..2512f80 100644 --- a/.env.example +++ b/.env.example @@ -17,8 +17,23 @@ CMS_S3_ACCESS_KEY_ID= CMS_S3_SECRET_ACCESS_KEY= CMS_ASSETS_BUCKET=git-native-cms-sandbox-assets CMS_RELEASES_BUCKET=git-native-cms-sandbox-releases +CMS_STATE_BUCKET=git-native-cms-sandbox-state CMS_PUBLIC_ASSETS_URL= CMS_PUBLIC_RELEASES_URL= +CMS_REGISTRY_DIGEST= + +# Machine actors (use independent random values, 32+ characters) +CMS_SCHEDULE_TOKEN= +CMS_MCP_TOKEN= + +# Optional deployment and revalidation hooks (configure both) +CMS_DEPLOYMENT_HOOK_URL= +CMS_REVALIDATION_URL= +CMS_INTEGRATION_TOKEN= + +# Optional translation provider API +CMS_TRANSLATION_PROVIDER_URL= +CMS_TRANSLATION_PROVIDER_TOKEN= # Opt-in, read-only R2 contract smoke test (`pnpm test:integration`) CMS_R2_SMOKE=false diff --git a/.github/workflows/cms-schedules.yml b/.github/workflows/cms-schedules.yml new file mode 100644 index 0000000..79cebd8 --- /dev/null +++ b/.github/workflows/cms-schedules.yml @@ -0,0 +1,30 @@ +name: CMS schedule executor + +on: + schedule: + - cron: "*/5 * * * *" + workflow_dispatch: + +concurrency: + group: cms-schedule-executor + cancel-in-progress: false + +permissions: + contents: read + +jobs: + execute: + runs-on: ubuntu-latest + steps: + - name: Execute due schedules + run: | + curl --fail-with-body --silent --show-error \ + --request POST \ + --header "authorization: Bearer $CMS_SCHEDULE_TOKEN" \ + --header "content-type: application/json" \ + --header "idempotency-key: schedule-${{ github.run_id }}-${{ github.run_attempt }}" \ + --data '{"configVersion":1,"schemaVersion":1}' \ + "$CMS_SCHEDULE_ENDPOINT" + env: + CMS_SCHEDULE_ENDPOINT: ${{ secrets.CMS_SCHEDULE_ENDPOINT }} + CMS_SCHEDULE_TOKEN: ${{ secrets.CMS_SCHEDULE_TOKEN }} diff --git a/apps/docs/src/content/docs/acceptance.md b/apps/docs/src/content/docs/acceptance.md new file mode 100644 index 0000000..01d4e0d --- /dev/null +++ b/apps/docs/src/content/docs/acceptance.md @@ -0,0 +1,65 @@ +--- +title: Product acceptance +description: Executable evidence for the 25 product Definition of Done requirements. +--- + +The product Definition of Done is enforced by code, shared contracts and the sandbox acceptance +run. A requirement is not considered complete when only a mock or a document exists. + +| # | Requirement | Executable evidence | +| --: | ------------------------------------------ | ---------------------------------------------------------------------------------------- | +| 1 | Next.js and Astro installation | framework package builds, playground builds and `next-*` / `astro-*` Playwright projects | +| 2 | GitHub login without personal tokens | GitHub App OAuth with PKCE, rotating JWE session and live OAuth smoke test | +| 3 | editor creates a Change, page and sections | application command suite and editor Playwright flow | +| 4 | globals and pages share one Change | logical repository/application suite and sandbox acceptance flow | +| 5 | coherent full preview | MessageChannel handshake, registry renderer and no-reload Playwright assertion | +| 6 | semantic and visual review | diff unit suite, baseline/current preview panes and review UI flow | +| 7 | approval enters staging | independent-reviewer rule, required checks and application transition suite | +| 8 | staging immutable release | deterministic builder, immutable store and staging publish command | +| 9 | staging to main production release | merge-commit promotion, back-merge and production publish command | +| 10 | production CDN JSON | delivery client, fallback tests and live R2 manifest smoke test | +| 11 | atomic rollback | release-store CAS contract and pointer-first rollback command | +| 12 | all content models | schema compiler and fixtures for pages, posts, collections, globals, settings and blocks | +| 13 | SEO, locales, redirects, assets, schedules | package suites plus localized Playwright and integration tests | +| 14 | MCP parity | MCP delegates to application handlers and has permission/confirmation tests | +| 15 | zero editor runtime in public bundle | bundle budget and public Playwright assertion | +| 16 | every capability port has a contract | `@git-native-cms/adapter-kit` all-port contract suites | +| 17 | Next.js and Astro E2E | Chromium, Firefox and WebKit matrix for both frameworks | +| 18 | WCAG 2.2 AA | axe light/dark checks, keyboard focus and reduced-motion configuration | +| 19 | `cms doctor` | CLI environment, security, registry, storage and installation checks | +| 20 | `cms upgrade` | migration/codemod package and CLI test suite | +| 21 | install-from-zero docs | Getting started, Next.js, Astro, adapters, security and sandbox guides | +| 22 | application layer cannot be bypassed | dependency-cruiser architecture gate | +| 23 | thin routes and presentation components | framework route adapters delegate to Web API/application contracts | +| 24 | reproducible release | deterministic identity/checksum tests for equal SHA and config | +| 25 | tested security | CSRF, fixation, XSS, YAML, SVG, traversal, webhook replay, origin and permission suites | + +Run the local evidence: + +```bash +pnpm check +pnpm test:e2e +``` + +With Docker available, run adapter integrations: + +```bash +CMS_CONTAINER_TESTS=true pnpm test:integration +``` + +After deploying the stable sandbox origins, run: + +```bash +pnpm test:live +pnpm test:live:flow -- --session-secret-file /secure/path/to/session-secret +``` + +The live smoke test initializes both hosted runtimes and verifies their public/editor split, +OAuth+PKCE redirect, scheduler and MCP authentication, the production pointer and its immutable +manifest with the exact deployed registry digest. + +The opt-in live flow creates a real Change with a page plus pricing/navigation globals, updates +the server-rendered preview, opens and independently approves a GitHub review, squash-merges to +Staging, promotes with a release PR, verifies CDN delivery, atomically rolls back, and restores +the verified release. It requires a production session secret supplied through a local +permission-restricted file; the value is never printed. diff --git a/apps/docs/src/content/docs/adapters.md b/apps/docs/src/content/docs/adapters.md index b810039..500efa3 100644 --- a/apps/docs/src/content/docs/adapters.md +++ b/apps/docs/src/content/docs/adapters.md @@ -1,7 +1,44 @@ --- title: Adapter authoring +description: Implement stable ports and run the shared contracts. --- -Implement the stable ports exported by `@git-native-cms/application/ports`, then run the shared -contract harness from `@git-native-cms/testing`. Adapters must support abort signals, typed errors, -optimistic concurrency and idempotent retries. +Implement ports from `@git-native-cms/application`; application code never imports an adapter. +Adapters must preserve abort signals, typed errors, optimistic concurrency and idempotent retry +semantics. + +`@git-native-cms/adapter-kit` exports executable common suites: + +```ts +import { + GitProviderContract, + ContentRepositoryContract, + ReviewPortContract, + AssetStoreContract, + AssetProcessorPortContract, + AssetUsagePortContract, + ReleaseBuilderPortContract, + ReleaseStoreContract, + SessionStoreContract, + DeploymentPortContract, + RevalidationPortContract, + PublicationNotifierPortContract, + TranslationProviderContract, + WebhookReplayStoreContract, + RateLimitPortContract, + SchedulerPortContract, + IdempotencyStoreContract, + AuditSinkContract, + FrameworkAdapterContract, + RendererContract, + contractPassed, +} from "@git-native-cms/adapter-kit"; +``` + +The repository runs all capability suites against memory fixtures, the sanitized GitHub Git Data +fixture and S3-compatible MinIO. A new adapter is not complete until every applicable shared suite +passes. + +External deployment and revalidation adapters receive deterministic idempotency keys. +Translation providers implement `createJob` and `readJob`; the returned XLIFF still passes through +the application import command, permissions, revision check and audit trail. diff --git a/apps/docs/src/content/docs/astro.md b/apps/docs/src/content/docs/astro.md index 37c5da0..5a925b1 100644 --- a/apps/docs/src/content/docs/astro.md +++ b/apps/docs/src/content/docs/astro.md @@ -1,7 +1,25 @@ --- -title: Astro +title: Astro 7 SSR +description: Server output, React editor island and Astro renderer. --- -Full editing requires Astro server output. Static projects can consume build-time or CDN content -and use the editor locally, but authentication, GitHub callbacks and mutations require a server -adapter. +Full editing requires Astro server output and the React integration: + +```bash +pnpm astro add react +pnpm add @git-native-cms/astro +pnpm add -D @git-native-cms/cli +pnpm cms init +``` + +The generator creates an `ALL` API route, an SSR `/cms/[...path]` page, the preview route and a +registered component library. `CmsHostedApp` is the only hydrated editor island; published Astro +pages use the lightweight renderer and CDN delivery data. + +Configure a server adapter such as `@astrojs/vercel` and keep `output: "server"`. The API, +GitHub OAuth callback, signed webhook, MCP endpoint and schedule executor all share the same Web +API handler. + +Astro static output can read production content at build time, but it cannot host authentication +or mutations. Keep the editor in a separate SSR deployment or use local editor mode as described +in [Astro static](/astro-static/). diff --git a/apps/docs/src/content/docs/changes-and-publishing.md b/apps/docs/src/content/docs/changes-and-publishing.md index fd017bf..a85f5c8 100644 --- a/apps/docs/src/content/docs/changes-and-publishing.md +++ b/apps/docs/src/content/docs/changes-and-publishing.md @@ -1,10 +1,25 @@ --- title: Changes and publishing +description: Branches, review, staging batches, releases and rollback. --- -A Change is an isolated editorial workspace backed by a branch. Sending it for review opens a -pull request to staging. Approved Changes are squash-merged into staging; a release pull request -moves the complete staging batch to Production. +A Change starts from the exact `main` SHA and gets an isolated `cms//` branch. +Every save uses Git Data compare-and-swap and includes `Change-ID` metadata. -Publication builds a deterministic immutable JSON release, verifies checksums, then changes one -environment pointer with optimistic concurrency. +Sending for review opens a pull request. Semantic field changes, live visual baseline, comments, +required checks and merge conflicts are shown in the workspace. Approval records the reviewer; +adding to staging squash-merges the Change, deletes its branch and keeps the audit record. + +Staging is a batch. Publication: + +1. opens and merge-commits a staging → main release PR; +2. merges main back into staging; +3. builds a deterministic release from the exact Git SHA and registry digest; +4. writes immutable files and verifies checksums; +5. atomically switches the environment pointer; +6. calls deployment and revalidation hooks with retry-safe keys. + +Production reads `environments/production/current.json`, then immutable JSON/XML/TXT files from +that release. Rollback switches the pointer first with compare-and-swap, notifies integrations, +and then opens an auditable revert PR. A retry after any intermediate network failure resumes +without duplicating publication. diff --git a/apps/docs/src/content/docs/content-modeling.md b/apps/docs/src/content/docs/content-modeling.md index 5456e8c..6bbf95b 100644 --- a/apps/docs/src/content/docs/content-modeling.md +++ b/apps/docs/src/content/docs/content-modeling.md @@ -1,7 +1,22 @@ --- title: Content modeling +description: Schemas, references, reusable blocks, localization and search. --- -Fields and content types are explicitly registered with `@git-native-cms/schema`. The schema -compiler produces JSON Schema, editor manifests, TypeScript declarations, validation rules and -MCP descriptions without inspecting arbitrary components. +Use `fields.*`, `defineSection`, `defineCollection`, `definePageType`, `defineGlobal` and +`defineSettings` from the framework registry export. Compilation produces a stable AST, JSON +Schema, editor/MCP manifest, TypeScript declarations and validators. + +The content tree supports Pages, Posts, Collections, Globals, Settings and Reusable Blocks. +Sections can bind to a filtered collection query or reference a reusable block with instance +overrides; detached sections stay local to the page. + +Portable rich text is a validated JSON AST, never stored HTML. References and asset IDs build a +release graph used by “find usages”, broken-reference checks and deletion safety. + +Locale documents use `en-US` as the source and `pl-PL` as the first fallback demonstration. +Editors can export/import XLIFF or create an external translation job. Translation import records +the source revision and status inside the same Change. + +Release artifacts include `content-index.json`, `content-graph.json`, search index, redirects, +sitemap, canonical metadata and hreflang. Slug changes preserve redirect history. diff --git a/apps/docs/src/content/docs/doctor-and-upgrades.md b/apps/docs/src/content/docs/doctor-and-upgrades.md index 61528f5..e1a53e6 100644 --- a/apps/docs/src/content/docs/doctor-and-upgrades.md +++ b/apps/docs/src/content/docs/doctor-and-upgrades.md @@ -1,7 +1,25 @@ --- title: Doctor and upgrades +description: Diagnose installations and perform recoverable migrations. --- -`cms doctor` checks runtime, routes, configuration, GitHub permissions, registry locks, storage, -Actions and security headers. `cms upgrade` creates a backup branch and performs config codemods -and content migrations through a reviewable Change. +`cms doctor` verifies Node, framework package, config, API/editor/preview routes, registry, +server-only GitHub variables, session and machine-token strength, complete S3/R2 configuration, +full registry digest, paired publication hooks, translation configuration and generated Actions. + +```bash +pnpm cms doctor +``` + +Each failed check prints a repair action and exits non-zero, so the same command is safe in CI. +Adapter runtimes may append live checks for GitHub permissions, storage access and deployment +headers. + +`cms upgrade` creates: + +- a filesystem backup under `.cms/backups/`; +- a recoverable Git branch named `cms/backup/upgrade-`; +- updated config through the ordered migration chain; +- generated route/config codemods without overwriting user-owned files. + +Review the resulting diff and keep the backup branch until production verification succeeds. diff --git a/apps/docs/src/content/docs/getting-started.md b/apps/docs/src/content/docs/getting-started.md index 412f56b..3bec805 100644 --- a/apps/docs/src/content/docs/getting-started.md +++ b/apps/docs/src/content/docs/getting-started.md @@ -1,19 +1,89 @@ --- title: Getting started -description: Install the CMS and open the editor. +description: Install the CMS, connect GitHub and R2, and open the editor. --- ## Requirements -- Node.js 22.12 or newer -- Next.js 16 App Router or Astro 7 with server output -- a GitHub App installation and a separate content repository +- Node.js 22.12 or newer and pnpm 11 +- Next.js 16 App Router or Astro 7 in SSR mode +- a GitHub organization where you can create and install a private GitHub App +- a content repository with protected `main` and `staging` branches +- three S3-compatible buckets: assets, immutable releases and a private runtime-state bucket + +## 1. Install and generate the integration + +Next.js: ```bash pnpm add @git-native-cms/next +pnpm add -D @git-native-cms/cli +pnpm cms init +``` + +Astro: + +```bash +pnpm add @git-native-cms/astro @astrojs/react +pnpm add -D @git-native-cms/cli +pnpm astro add react pnpm cms init +``` + +`cms init` creates the server API, `/cms`, `/__cms/preview`, the component registry, +`.cms/project.yaml`, validation Actions and the schedule executor. Generated files are never +overwritten. + +## 2. Prepare the content repository + +Create `main`, branch `staging` from it, and commit the generated `.cms` directory plus your +`content/` documents. Protect both branches. Editors do not need personal access tokens: the +server uses a GitHub App installation and users sign in through GitHub OAuth with PKCE. + +The GitHub App needs: + +| Repository permission | Access | +| --------------------- | -------------- | +| Contents | Read and write | +| Pull requests | Read and write | +| Issues | Read and write | +| Checks | Read | +| Metadata | Read | + +Subscribe to `check_run`, `pull_request`, and `push`. Set the OAuth callback to +`https://YOUR_ORIGIN/api/cms/auth/github/callback` and the webhook to +`https://YOUR_ORIGIN/api/cms/webhooks/github`. Install the App only on the content repository. + +## 3. Configure server-only environment variables + +Copy `.env.example`. At minimum configure the GitHub App/OAuth values, a 32+ character +`CMS_SESSION_SECRET`, S3 credentials, both bucket names and public bucket URLs. Also set separate +32+ character `CMS_SCHEDULE_TOKEN` and `CMS_MCP_TOKEN`. + +Keep `CMS_STATE_BUCKET` private; unlike immutable delivery and asset buckets it contains +idempotency, audit, replay and rate-limit state and must never have an `r2.dev` or public custom +domain. + +`CMS_REGISTRY_DIGEST` is `sha256:` followed by the full SHA-256 digest of the deployed component +registry. Recompute it whenever registered components or schemas change. + +```bash +pnpm registry:digest src/cms/registry.tsx +``` + +Never prefix these values with `NEXT_PUBLIC_` or expose them through Astro `PUBLIC_` variables. + +## 4. Verify before starting + +```bash pnpm cms doctor +pnpm build pnpm dev ``` -Open `/cms`. The generated framework routes are thin mounts around the shared CMS server. +Open `/cms`, sign in with GitHub, create a Change, edit a page and publish it through Review → +Staging → Live. A failed `doctor` check includes the exact repair action. + +For production, add `CMS_SCHEDULE_ENDPOINT=https://YOUR_ORIGIN/api/cms/schedules/execute` and +`CMS_SCHEDULE_TOKEN` as GitHub Actions secrets. The generated workflow calls the same +permission-aware application commands as the UI. diff --git a/apps/docs/src/content/docs/mcp.md b/apps/docs/src/content/docs/mcp.md index 646b2aa..1f1e2c6 100644 --- a/apps/docs/src/content/docs/mcp.md +++ b/apps/docs/src/content/docs/mcp.md @@ -1,7 +1,24 @@ --- title: MCP and AI safety +description: Stdio/HTTP transport, permissions and confirmations. --- -MCP tools use the current actor, application commands and permissions. Content is edited through -typed patches rather than raw YAML. Publication and rollback require a scoped confirmation token -in addition to the actor permission. +The MCP server exposes project, Change, document and release resources plus tools for creating a +Change, applying typed patches, requesting review, previewing, publishing and rollback. Stdio and +Streamable HTTP call the same application handlers as UI, HTTP and CLI. + +For hosted HTTP, send: + +```http +Authorization: Bearer +Content-Type: application/json +``` + +The machine actor is intentionally mapped to the `editor` role. It can create a Change and obtain +a preview, but cannot approve, stage, publish or rollback by token alone. + +Destructive tools require both the normal actor permission and a short-lived confirmation token +for the exact action. Confirmation tokens are encrypted, actor-bound, expire quickly and are +single-use. Every command audit event records `source: mcp`. + +Never pass GitHub, R2 or session secrets through MCP resources, prompts or tool arguments. diff --git a/apps/docs/src/content/docs/nextjs.md b/apps/docs/src/content/docs/nextjs.md index 8effe30..7b1d03b 100644 --- a/apps/docs/src/content/docs/nextjs.md +++ b/apps/docs/src/content/docs/nextjs.md @@ -1,7 +1,45 @@ --- -title: Next.js +title: Next.js 16 +description: App Router, SSR editor and zero-editor-runtime delivery. --- -Mount `CmsEditorPage`, the preview renderer and `createNextCmsRouteHandlers` in App Router catch-all -routes. Keep the editor stylesheet inside the `/cms` layout so the public application receives no -CMS client runtime. +Run `pnpm cms init` in a Next.js App Router project. It generates: + +- `src/cms/runtime.ts` — the server-only GitHub/R2 runtime; +- `src/cms/registry.tsx` — explicitly registered visual sections; +- `src/cms/preview.tsx` — the MessageChannel preview bridge; +- `app/api/cms/[[...path]]/route.ts` — thin Web API mount; +- `app/cms/[[...path]]` — editor shell and its scoped stylesheet; +- `app/%5F%5Fcms/preview/[[...slug]]` — full-page preview. + +If the project uses `src/app`, every route is generated under it automatically. + +The public page must import only the delivery client and renderer. Keep +`@git-native-cms/next/styles.css` inside the `/cms` layout. The production bundle gate verifies +that the public route loads 0 bytes of editor runtime. + +Use the CDN client in a server component: + +```bash +pnpm add @git-native-cms/delivery @git-native-cms/react +``` + +```tsx +import { cdnSource, createContentClient, loadContentGraph } from "@git-native-cms/delivery"; +import { CmsPageRenderer } from "@git-native-cms/react"; +import { cmsRegistry } from "@/cms/registry"; + +const content = createContentClient({ + environment: "production", + source: cdnSource({ baseUrl: process.env.CMS_PUBLIC_RELEASES_URL! }), +}); + +export default async function Page() { + const graph = await loadContentGraph(content); + const page = graph.find((document) => document.id === "doc_home"); + return ; +} +``` + +Do not cache `environments/production/current.json` permanently. Release files are immutable and +may use a one-year cache; the pointer must revalidate. diff --git a/apps/docs/src/content/docs/sandbox.md b/apps/docs/src/content/docs/sandbox.md index e5e5e40..a7b7048 100644 --- a/apps/docs/src/content/docs/sandbox.md +++ b/apps/docs/src/content/docs/sandbox.md @@ -1,22 +1,62 @@ --- title: Sandbox deployment -description: External services and secrets required by the public demonstration. +description: Deploy the Next.js and Astro playgrounds with GitHub, Vercel and Cloudflare R2. --- -The public sandbox uses two Vercel projects (`git-native-cms-next` and -`git-native-cms-astro`), separate GitHub Apps and two Cloudflare R2 buckets. Keep every secret in -GitHub Environments or Vercel Environment Variables; the browser bundle and content repository -must never receive credentials. +The reference sandbox uses: -## Provisioning checklist +- source: `DMTcorp/git-native-cms`; +- content: `DMTcorp/git-native-cms-sandbox-content`; +- Vercel: `git-native-cms-next` and `git-native-cms-astro`; +- R2: `git-native-cms-sandbox-assets`, `git-native-cms-sandbox-releases` and private + `git-native-cms-sandbox-state`; +- one private GitHub App per playground. -1. Create `DMTcorp/git-native-cms` and `DMTcorp/git-native-cms-sandbox-content`. -2. Create one GitHub App per playground with its own callback and webhook URL. -3. Create the `git-native-cms-sandbox-assets` and `git-native-cms-sandbox-releases` buckets. -4. Configure the variables documented in `.env.example` for each deployment environment. -5. Run `pnpm cms doctor`, `pnpm check`, `pnpm test:integration` and `pnpm test:e2e`. -6. Deploy both SSR playgrounds, then exercise publish, delivery and rollback against the sandbox - content repository. +## R2 -Set `CMS_R2_SMOKE=true` only in a protected integration environment. The smoke suite is read-only -and validates R2 through the same S3 adapter used by production delivery. +Create separate buckets and an API token limited to those buckets. Keep the state bucket private; +only assets and releases receive public development URLs. Use the S3 endpoint +`https://ACCOUNT_ID.r2.cloudflarestorage.com`, region `auto`, and public development URLs for +`CMS_PUBLIC_ASSETS_URL` and `CMS_PUBLIC_RELEASES_URL`. + +The assets bucket CORS policy allows `PUT` and `HEAD` only from the two stable Vercel origins, +including `content-type` and signed `x-amz-*` headers. `tooling/scripts/configure-r2.mjs` applies +that policy after its credential smoke test. + +Set immutable release and asset objects to long-lived cache. Configure +`environments/*/current.json` for revalidation/no-cache. The adapter uses conditional writes for +immutable files and compare-and-swap for pointers. + +## Vercel + +Set every variable from `.env.example` in Production and Preview. `CMS_ORIGIN` must match the +stable deployment origin used by the GitHub App. Set `CMS_HOSTED_RUNTIME=true`. + +The two playgrounds must use different App IDs, private keys, OAuth client secrets and webhook +secrets. They may share the content repository and R2 buckets. + +## GitHub Actions + +Add repository secrets: + +```text +CMS_SCHEDULE_ENDPOINT=https://YOUR_STABLE_ORIGIN/api/cms/schedules/execute +CMS_SCHEDULE_TOKEN= +``` + +The five-minute executor is concurrency-locked and safe to retry. Publication hooks and +translation providers are optional; configure both deployment/revalidation URLs together. + +## Acceptance run + +```bash +pnpm cms doctor +pnpm check +CMS_CONTAINER_TESTS=true pnpm test:integration +pnpm test:e2e +pnpm test:live +``` + +Then verify GitHub login, a page plus global navigation/pricing in one Change, preview, review, +staging, production CDN delivery, rollback, asset deletion safety, `en-US`/`pl-PL`, scheduling and +an editor-only MCP actor that cannot publish. diff --git a/apps/docs/src/content/docs/security.md b/apps/docs/src/content/docs/security.md index f379f46..db5a0f5 100644 --- a/apps/docs/src/content/docs/security.md +++ b/apps/docs/src/content/docs/security.md @@ -1,7 +1,35 @@ --- -title: Security +title: Security model +description: Authentication, untrusted content, machine actors and storage boundaries. --- -OAuth uses state and PKCE. Sessions are encrypted JWE cookies with absolute and idle expiration. -Mutations require a session-bound CSRF token. Webhook signatures, delivery IDs, preview origins, -asset MIME types and every protocol message are validated before domain commands run. +GitHub OAuth uses state and PKCE. The access token is stored only inside an encrypted, rotating +JWE cookie and is revoked during logout. Cookies are `Secure`, `HttpOnly`, `SameSite=Lax`, have +absolute and idle expiry, and each session carries an independent CSRF secret. + +All mutations require JSON, a same-origin request, CSRF, an idempotency key and an exact expected +Git revision. Publication and rollback additionally require a short-lived, actor/action-scoped +JWE confirmation token whose JTI can be claimed only once. + +GitHub webhooks require SHA-256 HMAC verification and a one-time delivery ID. MCP machine access +uses a separate bearer token and maps to an editor-only actor; it cannot stage or publish without +permission and a confirmation token. + +Untrusted boundaries enforce: + +- 1 MiB HTTP bodies, depth/node limits and protected-key rejection; +- deterministic JSON/YAML with alias limits and no custom object types; +- RFC 6901 patch paths with prototype-pollution protection; +- sanitized portable rich text and blocked `javascript:` links; +- non-SVG allowlisted uploads, declared/actual size and MIME checks, SHA-256 addressing; +- Sharp EXIF removal and a 40-megapixel decode limit; +- safe release paths, full Git/registry digests and immutable checksum verification; +- exact preview origin/session handshake plus Ajv validation in both MessageChannel directions; +- HTTPS-only external integration URLs and redirect rejection. + +Assets and immutable releases may use public R2 delivery URLs. `CMS_STATE_BUCKET` must remain +private because it contains audit, idempotency, webhook replay, one-time confirmation and +distributed rate-limit records. + +The Vercel configs add CSP, HSTS, `nosniff`, frame, referrer, permissions, COOP and CORP headers. +Run unit security tests, MinIO/R2 contracts, three-browser E2E and axe WCAG checks before release. diff --git a/apps/playground-astro/package.json b/apps/playground-astro/package.json index 4b34a64..0f6a1cd 100644 --- a/apps/playground-astro/package.json +++ b/apps/playground-astro/package.json @@ -18,6 +18,7 @@ "@git-native-cms/editor-bridge": "workspace:*", "@git-native-cms/editor-ui": "workspace:*", "@git-native-cms/hosted-runtime": "workspace:*", + "@git-native-cms/localization": "workspace:*", "@git-native-cms/permissions": "workspace:*", "@git-native-cms/react": "workspace:*", "@git-native-cms/schema": "workspace:*", diff --git a/apps/playground-astro/src/cms-fixture.ts b/apps/playground-astro/src/cms-fixture.ts index 4cea117..3b3b6a2 100644 --- a/apps/playground-astro/src/cms-fixture.ts +++ b/apps/playground-astro/src/cms-fixture.ts @@ -34,6 +34,11 @@ export const change: Change = { export const document: ContentDocument<{ readonly title: string; readonly route: { readonly path: string }; + readonly redirectFrom: readonly string[]; + readonly seo: { readonly title: string; readonly description: string }; + readonly locales: Readonly< + Record> }> + >; readonly sections: readonly { readonly id: string; readonly type: string; @@ -49,6 +54,23 @@ export const document: ContentDocument<{ data: { title: "Astro homepage", route: { path: "/" }, + redirectFrom: ["/welcome"], + seo: { + title: "Fieldnotes · Astro Git-native CMS", + description: "A live Astro demonstration of Git-native visual publishing.", + }, + locales: { + "pl-PL": { + status: "translated", + fields: { + "/title": "Strona główna Astro", + "/sections/0/heading": "Jeden model treści. Dwa prawdziwe renderery.", + "/sections/0/description": "Ten sam proces obsługuje Next.js i Astro.", + "/sections/1/heading": "Edycja renderowana na serwerze", + "/sections/1/description": "Astro uruchamia pełny CMS przez adapter serwerowy.", + }, + }, + }, sections: [ { id: "sec_astro_hero", diff --git a/apps/playground-astro/src/cms-registry.tsx b/apps/playground-astro/src/cms-registry.tsx index 75a9825..91a6c7a 100644 --- a/apps/playground-astro/src/cms-registry.tsx +++ b/apps/playground-astro/src/cms-registry.tsx @@ -12,14 +12,18 @@ const sectionDefinition = (name: string, label: string) => }, }); +function text(value: unknown, fallback: string): string { + return typeof value === "string" || typeof value === "number" ? String(value) : fallback; +} + export const sandboxRegistry = createReactRegistry({ sections: [ registerReactSection(sectionDefinition("hero", "Hero"), ({ section }) => (
Built with Astro -

{String(section.heading)}

-

{String(section.description)}

+

{String(section.heading)}

+

{String(section.description)}