diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f79a011..4acaa9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '24' + node-version: "24" - run: npm install --global pnpm@11.19.0 - run: pnpm install --frozen-lockfile - name: Configure disposable test environment @@ -40,6 +40,71 @@ jobs: node --input-type=module -e "for(let i=0;i<60;i++){try{const r=await fetch('http://localhost:4317/health/ready');if(r.ok)process.exit(0);}catch{}await new Promise(r=>setTimeout(r,1000));}process.exit(1);" pnpm test:browser - run: pnpm scan:secrets - - run: pnpm audit --audit-level=moderate + - run: pnpm audit --audit-level=low - run: docker build -t cove:ci . - run: pnpm build + - name: Verify the compiled Sites browser journey + run: | + pnpm exec wrangler d1 execute DB --local --config .sites-runtime/wrangler.json --file drizzle/0000_sticky_juggernaut.sql + pnpm exec wrangler d1 execute DB --local --config .sites-runtime/wrangler.json --file drizzle/0001_sites_auth.sql + pnpm exec wrangler d1 execute DB --local --config .sites-runtime/wrangler.json --file drizzle/0002_integrity.sql + pnpm exec wrangler dev --local --config .sites-runtime/wrangler.json --ip 127.0.0.1 --port 4318 --inspector-port 0 > /tmp/cove-sites.log 2>&1 & + preview_pid=$! + trap 'kill "$preview_pid" || true' EXIT + node --input-type=module -e "for(let i=0;i<60;i++){try{const r=await fetch('http://localhost:4318');if(r.ok)process.exit(0);}catch{}await new Promise(r=>setTimeout(r,1000));}process.exit(1);" + node scripts/verify-sites-browser.mjs + node scripts/verify-sites-mcp.mjs + - uses: actions/upload-artifact@v4 + if: always() + with: + name: cove-verification + path: | + docs/sites-browser-verification.json + docs/design/ + test-results/ + recovery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + - run: npm install --global pnpm@11.19.0 + - run: pnpm install --frozen-lockfile + - run: node scripts/setup-env.mjs + - run: docker compose up -d --wait db mail + - run: pnpm db:migrate + - run: pnpm build:local + - name: Create fictional saved context and an assistant grant + run: | + node --input-type=module <<'NODE' + import { randomUUID } from 'node:crypto'; + import { Pool } from 'pg'; + import { CoveService } from './dist/packages/domain/service.js'; + import { emptyContext } from './dist/packages/shared/context.js'; + process.loadEnvFile('.env'); + const pool = new Pool({connectionString:process.env.DATABASE_URL}); + try { + const userId = randomUUID(); + await pool.query('INSERT INTO "user"(id,name,email,"emailVerified") VALUES($1,$2,$3,true)',[userId,'Recovery fixture',`${userId}@example.test`]); + const service = new CoveService(pool,{MAX_PROJECTS:50,MAX_REVISIONS:1000,MAX_STORAGE_BYTES:104857600}); + const actor = {userId}; + const context = {...emptyContext(),goal:'Preserve this fictional project through backup and restart.'}; + const project = await service.createProject(actor,{name:'Recovery fixture',context}); + await service.createHandoff(actor,project.id,{expectedVersion:1,requestKey:randomUUID()}); + await service.saveConnection(actor,{clientId:'recovery-fixture',label:'Recovery fixture',grants:[{projectId:project.id,capabilities:['read']}]}); + } finally { await pool.end(); } + NODE + - run: node scripts/backup-verify.mjs + - run: docker build -t cove:0.1.0 . + - run: node scripts/verify-container.mjs + - uses: actions/upload-artifact@v4 + if: always() + with: + name: cove-recovery-verification + path: | + docs/recovery-verification.json + docs/container-verification.json + - name: Remove this disposable CI stack + if: always() + run: docker compose down --volumes --remove-orphans diff --git a/README.md b/README.md index 1a7b6ff..5339b4f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Local prerequisites are Node.js 24, pnpm 11.19.0, Git, and Docker with Compose. | `packages/mcp` and `packages/shared` | MCP tools, validation, comparisons, and handoff formatting | | `tests` and `scripts` | Automated checks, setup, builds, and recovery tools | -See [GitHub Actions](https://github.com/agammann/cove/actions/workflows/ci.yml) for current automated results and the [documentation index](docs/README.md) for dated verification records and known limits. +See [GitHub Actions](https://github.com/agammann/cove/actions/workflows/ci.yml) for current automated results, [October 2 verification](docs/verification-2026-10-02.md) for the compiled browser and HTTP MCP checks, and the [documentation index](docs/README.md) for dated records and known limits. ## License diff --git a/docs/README.md b/docs/README.md index 3273afb..924859b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ Current automated results are in [GitHub Actions](https://github.com/agammann/co | Record | Scope | | --- | --- | +| [October 2 verification](verification-2026-10-02.md) | Dependency updates, compiled browser journey and actual HTTP OAuth/MCP checks | | [September 19 live acceptance](live-acceptance-2026-09-19.md) | Public browser and mobile journeys, live OAuth/MCP, reproduced issues, fixes, and remaining boundaries | | [Sites verification](sites.md#verification-on-september-13-2026) | Hosted browser verification, local Worker tests, and remaining external host checks | | [Original verification](verification.md) | September 10 local PostgreSQL release checks | diff --git a/docs/development.md b/docs/development.md index eed258f..3becbba 100644 --- a/docs/development.md +++ b/docs/development.md @@ -107,6 +107,8 @@ node scripts/verify-sites-browser.mjs This script exercises desktop/mobile rendering, saving context, handoff copying, and deletion of its synthetic account. It updates `docs/sites-browser-verification.json` and the screenshots in `docs/design`; review those changes before committing. The fixture uses synthetic identity headers and a development secret. Keep it bound to loopback, and never expose it through a tunnel or use its sign in flow against production. +Run `node scripts/verify-sites-mcp.mjs` against the same local Worker to verify all seven tools over actual HTTP. Two independent SDK clients complete PKCE, save and retrieve a pinned handoff, and check read-only grants and live revocation. The check creates and removes its own fictional local account and writes `test-results/cove-sites-mcp.json`. Repeated OAuth registrations are subject to the application's rate limits; wait for the normal cooldown before rerunning. This protocol test does not establish a ChatGPT or Codex host connection. + ## Troubleshooting | Symptom | Resolution | @@ -122,7 +124,7 @@ This script exercises desktop/mobile rendering, saving context, handoff copying, ## Maintainer checks -The [CI workflow](../.github/workflows/ci.yml) also builds the Docker image and builds Sites after the local browser journey. Recovery and container exercises are separate: +The [CI workflow](../.github/workflows/ci.yml) builds both distributions and runs the compiled Sites browser/MCP checks. A separate disposable Compose job verifies backup restoration and production container startup. You can repeat those operational checks locally: ```sh node scripts/backup-verify.mjs diff --git a/docs/verification-2026-10-02.md b/docs/verification-2026-10-02.md new file mode 100644 index 0000000..e310179 --- /dev/null +++ b/docs/verification-2026-10-02.md @@ -0,0 +1,16 @@ +# October 2, 2026 UTC verification + +These checks use fictional data. Local environment: Windows, Node 24.19.0, pnpm 11.19.0 and Playwright Chromium 153.0.8010.12. + +- Frozen dependency installation, lint, TypeScript and the Sites production build passed. +- Twelve domain, security regression and Sites tests passed. The Sites suite includes two official SDK clients against an in-process Worker fixture. +- The compiled Worker and actual local D1 passed the browser journey: project creation, editing, saved context, concise/full handoff copying, account cleanup and desktop/mobile layout. +- Two independent official MCP SDK 2.0.0 clients completed actual HTTP registration, S256 PKCE, consent and separate project grants against that compiled Worker. All seven tools returned the expected project, revision, handoff, search and change data. A read-only client could not write; the saved revision stayed at version 2. Revoking one live connection rejected further calls without disabling the other client. +- The initial dependency audit reported 24 advisories. Updating Wrangler, Fastify, Nodemailer and compatible transitive packages reduced the audit to zero advisories at all severity levels. The release-age exceptions name only the patched Wrangler and its exact Miniflare dependency. +- The source credential-pattern check passed. Its bounded patterns do not guarantee the absence of secrets. + +Run `node scripts/verify-sites-mcp.mjs` against the documented local Sites fixture to repeat the compiled HTTP check. It creates and deletes only its own fictional account and records results in `test-results/cove-sites-mcp.json`. OAuth registration rate limits remain enabled; a cooldown was required after repeated exploratory runs. + +GitHub Actions passed 21 tests including actual PostgreSQL and OAuth integration, the email sign-in and recovery browser journey, and the compiled Sites browser and HTTP MCP checks. A separate disposable Compose job restored a fictional saved revision and pinned handoff, compared complete project/history contents, revoked restored grants, and verified persistence after a database restart. The production container passed fresh migration, health and frontend checks, Host validation, non-root/read-only startup, restart, and rejection of insecure demo settings. See [the verified run](https://github.com/agammann/cove/actions/runs/36962954567) and [the workflow](https://github.com/agammann/cove/actions/workflows/ci.yml). + +The local sign-in uses synthetic identity headers on loopback. Live ChatGPT sign-in and a native Codex or ChatGPT assistant interaction still require the owner's account connection. SDK protocol checks do not establish those host integrations. Production recovery, off-host backups and participant usability are outside this evidence. diff --git a/package.json b/package.json index c300684..901b4fe 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "@modelcontextprotocol/server": "2.0.0", "better-auth": "1.7.3", "drizzle-orm": "0.45.2", - "fastify": "5.12.3", + "fastify": "5.12.5", "lucide-react": "1.42.0", - "nodemailer": "10.0.1", + "nodemailer": "10.0.9", "pg": "8.23.0", "react": "19.2.8", "react-dom": "19.2.8", @@ -62,6 +62,6 @@ "typescript-eslint": "8.70.0", "vite": "8.2.2", "vitest": "4.1.11", - "wrangler": "4.131.1" + "wrangler": "4.146.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7ce559..9a3829c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,14 +42,14 @@ importers: specifier: 0.45.2 version: 0.45.2(@cloudflare/workers-types@4.20260515.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0) fastify: - specifier: 5.12.3 - version: 5.12.3 + specifier: 5.12.5 + version: 5.12.5 lucide-react: specifier: 1.42.0 version: 1.42.0(react@19.2.8) nodemailer: - specifier: 10.0.1 - version: 10.0.1 + specifier: 10.0.9 + version: 10.0.9 pg: specifier: 8.23.0 version: 8.23.0 @@ -121,8 +121,8 @@ importers: specifier: 4.1.11 version: 4.1.11(@types/node@26.5.0)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.25.12)(tsx@4.23.13)) wrangler: - specifier: 4.131.1 - version: 4.131.1(@cloudflare/workers-types@4.20260515.1)(@types/node@26.5.0) + specifier: 4.146.0 + version: 4.146.0(@cloudflare/workers-types@4.20260515.1)(@types/node@26.5.0) packages: @@ -242,8 +242,8 @@ packages: resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} - '@cloudflare/unenv-preset@2.16.1': - resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + '@cloudflare/unenv-preset@2.16.2': + resolution: {integrity: sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==} peerDependencies: unenv: 2.0.0-rc.24 workerd: '>1.20260305.0 <2.0.0-0' @@ -251,32 +251,32 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260911.1': - resolution: {integrity: sha512-785eaY1bkR1cm4Z/PCUeteZYmTMe6lre2zz63/GdGGimsoMsKxgl4brFPRukim8iv28EyD1XoCB/VPYF20BERA==} + '@cloudflare/workerd-darwin-64@1.20261001.1': + resolution: {integrity: sha512-4cgSgDf28JSw/P5Dj5GCS59hzVqS5XnmGAWNkvYHLI6ODU9idGaMMNEuhJXDkEG/lsABmlVlnCgsdh2VbKWepw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260911.1': - resolution: {integrity: sha512-WU4bFqEN0H7ndGWxoedegv95DmNVBtv0ncXcHG9nYFTUI78sxEb0qoT3U6Ga4hyBkzsJFBX/zvVBIGX3qKldGA==} + '@cloudflare/workerd-darwin-arm64@1.20261001.1': + resolution: {integrity: sha512-8ulAWruEVouNmEIsQsy9WSSCS9zkLu93W2MTwp5esiFyoPp05BNSVFIFsYSPi1pkFmBBd7fsnwMpbLkaJLaVPQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260911.1': - resolution: {integrity: sha512-0Y2gy62oxQxWa38qinSPE6zNL5+JmumJtDY9AWW1HB8KHuATxN71o5MGzmVFfB8PwZsiHfUd2Sv7O22krCOrhw==} + '@cloudflare/workerd-linux-64@1.20261001.1': + resolution: {integrity: sha512-kZbTZJGrhsMOdqZ2BIybjaBRLZjYsRLWj7mcNw/6Y3hodOhp5MrNzcz4iWGwNVWwyIV+7Pl+/LX5VcgnRqdHOg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260911.1': - resolution: {integrity: sha512-kttNPnx1r2lCqFUoMH62z7CqGV+j4QBbw5fdtaz4pzOrzBv0AWkNATt7onFUe+SwP8zhcepMtbm2F4kKzTf6VA==} + '@cloudflare/workerd-linux-arm64@1.20261001.1': + resolution: {integrity: sha512-oOk3Zj6k/8oP0FJgZBWDn7+BqbsqIMEMaV95pulHPVbwVu4wYoLfQq2hp0vkbCNsCFLqZb7cqixXdrwD54ZIow==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260911.1': - resolution: {integrity: sha512-5iO/YfoBDOgO3CrHdkiiVP8SL3O2jC+c6Ux3d378TSPKLhU5+CgHjtE/ZSodWQrzr4FzFRqdW8S7n5nbyD1MHQ==} + '@cloudflare/workerd-windows-64@1.20261001.1': + resolution: {integrity: sha512-uRxm5W4VyBkoSaoP1BfOuH0873tE+vxsknF4sab6/5YIRvIAufChq3QDp+zlAn67PuGPGcn7W8QraA0t4ucmOQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -1451,8 +1451,8 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} buffer-from@1.1.2: @@ -1709,17 +1709,17 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-uri@3.1.7: - resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} - fast-uri@4.1.4: - resolution: {integrity: sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==} + fast-uri@4.2.1: + resolution: {integrity: sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==} fastify-plugin@6.0.0: resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} - fastify@5.12.3: - resolution: {integrity: sha512-reZ8wce5VNCcufIt9AVtzZa3L4u1j8esikn7OEgHWLVpRpL5R7Y2+Xzj70OUkv5zDfzUAxXZT6cu4Rt0zr3EKA==} + fastify@5.12.5: + resolution: {integrity: sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==} fastq@1.20.3: resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} @@ -1799,8 +1799,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ip-address@10.7.0: - resolution: {integrity: sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==} + ip-address@10.7.2: + resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} engines: {node: '>= 12'} ipaddr.js@2.5.0: @@ -1946,8 +1946,8 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - miniflare@5.20260911.0-alpha: - resolution: {integrity: sha512-CRieJmvHx+7rNqnA5SKdsYsER6rfkUIE/jruIUw+fLhsQ4sORfuMtr3+FQzsQ9/y8lhk061V4Fl1DFdHiyBB6g==} + miniflare@5.20261001.0-alpha: + resolution: {integrity: sha512-GaimS5mSIOMyvd16ga+e1/QkI8cmp3z35QPHFex0DktqNQf2RVZQwMPQXdyoUreUiFP/0Gpe2MoQInTyMAD5xA==} engines: {node: '>=22.0.0'} minimatch@10.2.6: @@ -1973,8 +1973,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nodemailer@10.0.1: - resolution: {integrity: sha512-c+gU9cL9HLDax3vjxL88kW+6NOgdtEUWaZ+AUtxdJR6LLhf0kGdCLExof7yiKW7zdO9EfXCSIgmhGyFmUM0mYQ==} + nodemailer@10.0.9: + resolution: {integrity: sha512-BF0qcyplCwp+jMk6HCjFykBz/YhhZSsxrARhOldLwFWH+8kGjQsd2WIMIZhqEuyXRoGFi0ONbDeWDMDoL8MhLw==} engines: {node: '>=20.0.0'} obug@2.1.4: @@ -2325,8 +2325,8 @@ packages: undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - undici@7.29.0: - resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -2433,17 +2433,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260911.1: - resolution: {integrity: sha512-vRr8QdBxueQOZJO1hRCI73EZlix87IAyBAcSyI3rA1VB+6oxjw3oaqzYnIV8C4IOPtUgihbdMAgzkb5GM4V7DQ==} + workerd@1.20261001.1: + resolution: {integrity: sha512-d/SIYHFO0PT/wiFZg8in4NpRIxYuFwslX1HdylOtWkBIIUmSpkGFhK820cV84XACFylwJ48xuRoWW/8DWDPsPQ==} engines: {node: '>=16'} hasBin: true - wrangler@4.131.1: - resolution: {integrity: sha512-1u5FMdJAn6UOcL02cVsIITcnHrk6mC7N+RF10EkVhPL18R/o9g5BZb4PCjByL+3AsRP5wQpppCIPHhYPRmIJwg==} + wrangler@4.146.0: + resolution: {integrity: sha512-c27eHUH0Isr8HTmgZ6cLtLr/0cxtiBoLren6ywBeTW8ZIzj/dINVNBcqsbZc+iETnI5jrzO7E7Z8mLgLZ/l9iw==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^5.20260911.1 + '@cloudflare/workers-types': ^5.20261001.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -2580,25 +2580,25 @@ snapshots: '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1)': + '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261001.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260911.1 + workerd: 1.20261001.1 - '@cloudflare/workerd-darwin-64@1.20260911.1': + '@cloudflare/workerd-darwin-64@1.20261001.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260911.1': + '@cloudflare/workerd-darwin-arm64@1.20261001.1': optional: true - '@cloudflare/workerd-linux-64@1.20260911.1': + '@cloudflare/workerd-linux-64@1.20261001.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260911.1': + '@cloudflare/workerd-linux-arm64@1.20261001.1': optional: true - '@cloudflare/workerd-windows-64@1.20260911.1': + '@cloudflare/workerd-windows-64@1.20261001.1': optional: true '@cloudflare/workers-types@4.20260515.1': {} @@ -2898,7 +2898,7 @@ snapshots: dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 4.1.4 + fast-uri: 4.2.1 '@fastify/error@4.2.0': {} @@ -2926,7 +2926,7 @@ snapshots: dependencies: '@lukeed/ms': 2.0.2 fastify-plugin: 6.0.0 - ip-address: 10.7.0 + ip-address: 10.7.2 toad-cache: 3.7.4 '@fastify/send@4.1.1': @@ -3380,7 +3380,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.7 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -3436,7 +3436,7 @@ snapshots: blake3-wasm@2.1.5: {} - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -3678,7 +3678,7 @@ snapshots: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 4.1.4 + fast-uri: 4.2.1 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -3688,13 +3688,13 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-uri@3.1.7: {} + fast-uri@3.1.8: {} - fast-uri@4.1.4: {} + fast-uri@4.2.1: {} fastify-plugin@6.0.0: {} - fastify@5.12.3: + fastify@5.12.5: dependencies: '@fastify/ajv-compiler': 4.0.6 '@fastify/error': 4.2.0 @@ -3786,7 +3786,7 @@ snapshots: inherits@2.0.4: {} - ip-address@10.7.0: {} + ip-address@10.7.2: {} ipaddr.js@2.5.0: {} @@ -3894,12 +3894,12 @@ snapshots: mime@3.0.0: {} - miniflare@5.20260911.0-alpha(@types/node@26.5.0): + miniflare@5.20261001.0-alpha(@types/node@26.5.0): dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.4(@types/node@26.5.0) - undici: 7.29.0 - workerd: 1.20260911.1 + undici: 7.29.1 + workerd: 1.20261001.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -3909,7 +3909,7 @@ snapshots: minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minipass@7.1.3: {} @@ -3921,7 +3921,7 @@ snapshots: natural-compare@1.4.0: {} - nodemailer@10.0.1: {} + nodemailer@10.0.9: {} obug@2.1.4: {} @@ -4247,7 +4247,7 @@ snapshots: undici-types@8.9.0: {} - undici@7.29.0: {} + undici@7.29.1: {} unenv@2.0.0-rc.24: dependencies: @@ -4308,24 +4308,24 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260911.1: + workerd@1.20261001.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260911.1 - '@cloudflare/workerd-darwin-arm64': 1.20260911.1 - '@cloudflare/workerd-linux-64': 1.20260911.1 - '@cloudflare/workerd-linux-arm64': 1.20260911.1 - '@cloudflare/workerd-windows-64': 1.20260911.1 + '@cloudflare/workerd-darwin-64': 1.20261001.1 + '@cloudflare/workerd-darwin-arm64': 1.20261001.1 + '@cloudflare/workerd-linux-64': 1.20261001.1 + '@cloudflare/workerd-linux-arm64': 1.20261001.1 + '@cloudflare/workerd-windows-64': 1.20261001.1 - wrangler@4.131.1(@cloudflare/workers-types@4.20260515.1)(@types/node@26.5.0): + wrangler@4.146.0(@cloudflare/workers-types@4.20260515.1)(@types/node@26.5.0): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260911.1) + '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261001.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 5.20260911.0-alpha(@types/node@26.5.0) + miniflare: 5.20261001.0-alpha(@types/node@26.5.0) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260911.1 + workerd: 1.20261001.1 optionalDependencies: '@cloudflare/workers-types': 4.20260515.1 fsevents: 2.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7b3082..3042827 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,5 +2,8 @@ allowBuilds: esbuild: true sharp: false workerd: true +minimumReleaseAgeExclude: + - miniflare@5.20261001.0-alpha + - wrangler@4.146.0 overrides: esbuild@<0.24.3: 0.25.12 diff --git a/scripts/backup-verify.mjs b/scripts/backup-verify.mjs index 4294dc3..39408eb 100644 --- a/scripts/backup-verify.mjs +++ b/scripts/backup-verify.mjs @@ -34,8 +34,19 @@ const run = (args, input) => }); let target; let result; +let created = false; +const history = async (pool) => { + const rows = {}; + for (const table of ["projects", "revisions", "handoffs", "events"]) { + rows[table] = ( + await pool.query(`SELECT to_jsonb(t) AS row FROM ${table} t ORDER BY id`) + ).rows; + } + return JSON.stringify(rows); +}; try { const before = await admin.query("SELECT count(*)::int n FROM revisions"); + const savedHistory = await history(admin); const dump = await run([ "compose", "exec", @@ -51,6 +62,7 @@ try { await mkdir("backups", { recursive: true }); await writeFile("backups/verification.dump", dump); await admin.query(`CREATE DATABASE ${dbName}`); + created = true; await run( [ "compose", @@ -74,6 +86,8 @@ try { const restored = await target.query("SELECT count(*)::int n FROM revisions"); if (restored.rows[0].n !== before.rows[0].n) throw new Error("Restored revision count mismatch"); + if ((await history(target)) !== savedHistory) + throw new Error("Restored project history content mismatch"); // Exercise reconciliation using a synthetic deleted account that exists only in the restored copy. const synthetic = randomUUID(); await target.query( @@ -159,11 +173,19 @@ try { restored.rows[0].n ) throw new Error("Project history changed after database restart"); + if ((await history(target)) !== savedHistory) + throw new Error("Project history content changed after database restart"); + if ( + (await target.query("SELECT 1 FROM connections WHERE status <> 'revoked'")) + .rowCount + ) + throw new Error("Restored connections were not revoked"); result = { date: new Date().toISOString(), backupFormat: "PostgreSQL custom", restoredRevisions: restored.rows[0].n, restore: "passed", + projectHistoryContent: "passed", deletedAccountReconciliation: "passed", allAccessRevokedOnRecovery: "passed", databaseRestartPersistence: "passed", @@ -171,7 +193,7 @@ try { }; } finally { if (target) await target.end(); - await admin.query(`DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`); + if (created) await admin.query(`DROP DATABASE ${dbName} WITH (FORCE)`); await admin.end(); } await writeFile( diff --git a/scripts/verify-sites-mcp.mjs b/scripts/verify-sites-mcp.mjs new file mode 100644 index 0000000..133e3de --- /dev/null +++ b/scripts/verify-sites-mcp.mjs @@ -0,0 +1,236 @@ +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { randomUUID, randomBytes, createHash } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import { URL, URLSearchParams } from "node:url"; +import assert from "node:assert/strict"; +const { fetch } = globalThis; +const base = "http://localhost:4318"; +await mkdir("test-results", { recursive: true }); +const auth = await fetch(base + "/api/auth/chatgpt", { + headers: { + "oai-authenticated-user-id": "mcp-review-" + randomUUID(), + "oai-authenticated-user-email": "mcp-" + randomUUID() + "@example.test", + }, + redirect: "manual", +}); +assert.equal(auth.status, 302); +const cookie = auth.headers + .getSetCookie() + .map((s) => s.split(";")[0]) + .join("; "); +const api = async (path, method = "GET", data) => { + const r = await fetch(base + path, { + method, + headers: { + cookie, + origin: base, + ...(data ? { "content-type": "application/json" } : {}), + }, + ...(data ? { body: JSON.stringify(data) } : {}), + }); + const value = await r.json(); + assert(r.ok, JSON.stringify({ status: r.status, value })); + return value; +}; +const project = await api("/api/projects", "POST", { + name: "Fictional MCP acceptance", +}); +const clients = [], + checks = []; +let failure; +async function authorize(label, capabilities = ["read", "write", "handoff"]) { + const registration = await fetch(base + "/api/auth/oauth2/register", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + client_name: label, + application_type: "native", + redirect_uris: ["http://127.0.0.1:3999/callback"], + token_endpoint_auth_method: "none", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + scope: "cove offline_access", + }), + }); + assert.equal(registration.status, 201); + const cid = (await registration.json()).client_id; + const connection = await api("/api/connections", "POST", { + clientId: cid, + label, + grants: [{ projectId: project.id, capabilities }], + }); + const verifier = randomBytes(48).toString("base64url"); + const query = new URLSearchParams({ + client_id: cid, + redirect_uri: "http://127.0.0.1:3999/callback", + response_type: "code", + scope: "cove offline_access", + resource: base + "/api/mcp", + code_challenge: createHash("sha256").update(verifier).digest("base64url"), + code_challenge_method: "S256", + state: randomUUID(), + }); + const authorization = await fetch( + base + "/api/auth/oauth2/authorize?" + query, + { headers: { cookie, accept: "text/html" }, redirect: "manual" }, + ); + assert.equal(authorization.status, 302); + const location = new URL(authorization.headers.get("location"), base); + const consent = await api("/api/auth/oauth2/consent", "POST", { + accept: true, + oauth_query: + location.searchParams.get("oauth_query") || + location.searchParams.toString(), + }); + const callback = new URL(consent.url || consent.redirect_uri); + const token = await fetch(base + "/api/auth/oauth2/token", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: cid, + code: callback.searchParams.get("code"), + redirect_uri: "http://127.0.0.1:3999/callback", + code_verifier: verifier, + resource: base + "/api/mcp", + }), + }); + assert.equal(token.status, 200); + const credentials = await token.json(); + const client = new Client({ name: label, version: "1.0.0" }); + clients.push(client); + await client.connect( + new StreamableHTTPClientTransport(new URL(base + "/api/mcp"), { + requestInit: { + headers: { Authorization: "Bearer " + credentials.access_token }, + }, + }), + ); + return { client, connection }; +} +try { + const one = await authorize("Fictional review A"), + two = await authorize("Fictional review B", ["read"]); + assert.equal((await one.client.listTools()).tools.length, 7); + checks.push( + "Two actual HTTP SDK clients complete S256 OAuth with separate project grants", + ); + const context = { + goal: "Across assistants", + summary: "Fictional protocol check", + constraints: [], + decisions: [], + notes: [], + completedWork: [], + nextSteps: [], + openQuestions: [], + sources: [], + artifacts: [], + }; + const update = await one.client.callTool({ + name: "cove_update_context", + arguments: { + projectId: project.id, + context, + expectedVersion: 1, + summary: "Fictional revision", + requestKey: randomUUID(), + }, + }); + assert(!update.isError, JSON.stringify(update)); + const handoff = await one.client.callTool({ + name: "cove_create_handoff", + arguments: { + projectId: project.id, + expectedVersion: 2, + requestKey: randomUUID(), + }, + }); + assert(!handoff.isError, JSON.stringify(handoff)); + const received = await two.client.callTool({ + name: "cove_get_handoff", + arguments: { + projectId: project.id, + handoffId: handoff.structuredContent.id, + }, + }); + assert.equal(received.structuredContent.snapshot.context.goal, context.goal); + checks.push( + "Actual MCP save, handoff and independent client retrieval preserve the saved revision", + ); + const retrieved = await two.client.callTool({ + name: "cove_get_context", + arguments: { projectId: project.id, detail: "full" }, + }); + assert.equal(retrieved.structuredContent.revision.context.goal, context.goal); + for (const call of [ + { name: "cove_list_projects", arguments: {} }, + { name: "cove_search", arguments: { query: context.goal } }, + { + name: "cove_get_changes", + arguments: { projectId: project.id, from: 1, to: 2 }, + }, + ]) { + const result = await two.client.callTool(call); + assert(!result.isError, JSON.stringify(result)); + assert( + JSON.stringify(result.structuredContent).includes( + call.name === "cove_get_changes" ? context.goal : project.id, + ), + `${call.name}: ${JSON.stringify(result.structuredContent)}`, + ); + } + checks.push( + "All seven advertised tools return the expected project, source revision, search and change data over HTTP", + ); + const denied = await two.client.callTool({ + name: "cove_update_context", + arguments: { + projectId: project.id, + context, + expectedVersion: 2, + summary: "Forbidden write", + requestKey: randomUUID(), + }, + }); + assert.equal(denied.isError, true); + assert.equal(denied.structuredContent.error.code, "PROJECT_UNAVAILABLE"); + const unchanged = await two.client.callTool({ + name: "cove_get_context", + arguments: { projectId: project.id, detail: "full" }, + }); + assert.equal(unchanged.structuredContent.project.version, 2); + checks.push("A read-only OAuth client cannot mutate the saved context"); + await api("/api/connections/" + one.connection.id + "/revoke", "POST", {}); + await assert.rejects(() => one.client.listTools()); + assert.equal((await two.client.listTools()).tools.length, 7); + checks.push( + "Revoking a live connection rejects further calls while the other client remains authorized", + ); +} catch (e) { + failure = e.stack; + process.exitCode = 1; + console.error(e.message); +} finally { + for (const client of clients) await client.close(); + await api("/api/account", "DELETE", { confirmation: "DELETE MY ACCOUNT" }); + await writeFile( + "test-results/cove-sites-mcp.json", + JSON.stringify( + { + date: new Date().toISOString(), + runtime: "compiled Worker and real local D1 over HTTP", + sdk: "2.0.0", + signIn: "synthetic local identity", + checks, + failure, + }, + null, + 2, + ), + ); + console.log(checks.join("\n")); +}