Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
55fd668
Add Cloudflare edge data API helpers
jnton Jul 21, 2026
0787319
Add filterable resources API
jnton Jul 21, 2026
fcd4a11
Add resource lookup API
jnton Jul 21, 2026
2eb0443
Add dataset facets API
jnton Jul 21, 2026
3106c22
Add agent API health endpoint
jnton Jul 21, 2026
40eda26
Add A2A agent discovery card
jnton Jul 21, 2026
8312cee
Document agent APIs in OpenAPI
jnton Jul 21, 2026
f5d127c
Generate agent API OpenAPI paths during build
jnton Jul 21, 2026
0c7b996
Harden agent API validation and error handling
jnton Jul 21, 2026
da858d9
Validate agent resource search requests
jnton Jul 21, 2026
6675f81
Validate agent resource identifiers
jnton Jul 21, 2026
16f4da2
Hide internal errors in facet endpoint
jnton Jul 21, 2026
41d0c48
Remove internal error details from health responses
jnton Jul 21, 2026
9f6ff5c
Document strict agent API constraints
jnton Jul 21, 2026
4de9a71
Add agent API security regression tests
jnton Jul 21, 2026
e6df3a8
Validate generated agent artifacts before deployment
jnton Jul 21, 2026
ed5a7c9
Fail closed on generated agent artifacts
jnton Jul 21, 2026
9040bf6
Publish agent API security constraints
jnton Jul 21, 2026
c7d51f0
Document required Cloudflare API protections
jnton Jul 21, 2026
e000cdf
Continuously verify agent API security invariants
jnton Jul 21, 2026
4713046
Verify public errors do not leak internals
jnton Jul 21, 2026
c0d66d7
Avoid claiming undeployed rate-limit policy
jnton Jul 21, 2026
28969db
Point agent discovery to the canonical MCP server
jnton Jul 21, 2026
61688f0
Validate the canonical MCP discovery endpoint
jnton Jul 21, 2026
8d71e33
Add MCP protocol and Origin security smoke tests
jnton Jul 21, 2026
d25959d
Audit production dependencies in agent security CI
jnton Jul 21, 2026
1b15cce
Upload production dependency audit report
jnton Jul 21, 2026
f24e68b
Run one-shot dependency and data remediation
jnton Jul 21, 2026
2a2ddc0
Apply safe dependency and data integrity fixes
github-actions[bot] Jul 21, 2026
5d663c2
Remove temporary remediation workflow
jnton Jul 21, 2026
a1f89fe
Classify build tooling as development dependencies
jnton Jul 21, 2026
906992d
Add one-shot lockfile synchronization
jnton Jul 21, 2026
596037e
Synchronize lockfile dependency classifications
github-actions[bot] Jul 21, 2026
a924812
Remove temporary lockfile workflow
jnton Jul 21, 2026
ccab4f1
Separate runtime and build-chain security gates
jnton Jul 21, 2026
7ccfa80
Add runtime dependency audit enforcement
jnton Jul 21, 2026
527b4ce
Cover audit enforcement script in security CI
jnton Jul 21, 2026
cdc156a
Disable vulnerable image optimization and external rewrites
jnton Jul 21, 2026
dd9be3f
Prevent middleware redirect cache poisoning
jnton Jul 21, 2026
d8cb4e1
Replace unused Umami handler with route handlers
jnton Jul 21, 2026
36c961f
Proxy Umami script through bounded route handler
jnton Jul 21, 2026
73ea992
Proxy Umami events through bounded route handler
jnton Jul 21, 2026
e419b40
Test Next and proxy security mitigations
jnton Jul 21, 2026
a0a8eb8
Correct middleware redirect regression assertion
jnton Jul 21, 2026
3744fe2
Cover platform mitigations in security CI
jnton Jul 21, 2026
5763428
Pin patched Node 22 build runtime
jnton Jul 21, 2026
1906f4d
Document runtime pin and Next.js migration blocker
jnton Jul 21, 2026
9176feb
Remove unused vulnerable build dependencies
jnton Jul 21, 2026
55bc2dc
Add one-shot dependency cleanup validation
jnton Jul 21, 2026
926c127
Remove temporary dependency cleanup workflow
jnton Jul 21, 2026
4df462f
Restore lockfile-compatible build dependency set
jnton Jul 21, 2026
06fbcf3
Add one-shot OpenNext migration validation
jnton Aug 3, 2026
8dc8c72
Make OpenNext migration workflow self-reporting
jnton Aug 3, 2026
6133676
Run OpenNext migration from PR synchronization
jnton Aug 3, 2026
d02d9bb
Run validated OpenNext migration from security workflow
jnton Aug 3, 2026
ea2926b
Allow draft transition to trigger OpenNext migration
jnton Aug 3, 2026
adc5278
Reduce migration trigger to diagnostic workflow
jnton Aug 3, 2026
e34aa56
Run validated OpenNext migration
jnton Aug 3, 2026
345161c
Fix Edge Runtime migration step
jnton Aug 3, 2026
cd4698c
Pin patched Next transitive dependencies
jnton Aug 4, 2026
6c249c6
Migrate deployment to OpenNext Workers
github-actions[bot] Aug 4, 2026
8b4bf44
Validate OpenNext Worker in read-only CI
jnton Aug 4, 2026
a152234
Remove one-shot OpenNext migration workflow
jnton Aug 4, 2026
d231fda
Document OpenNext Workers deployment and cutover
jnton Aug 4, 2026
b76b374
Add one-shot build dependency remediation
jnton Aug 4, 2026
30a9279
Apply npm safe transitive remediation
jnton Aug 4, 2026
d27cf0b
Remediate build dependency vulnerabilities
github-actions[bot] Aug 4, 2026
3304f06
Remove one-shot dependency remediation workflow
jnton Aug 4, 2026
61cabea
Enforce clean high-severity build dependency audit
jnton Aug 4, 2026
1705c66
Make Worker preview endpoints explicit
jnton Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NEXTJS_ENV="development"
RESEND_API_KEY="replace-with-local-secret"
MAILCHIMP_API_KEY="replace-with-local-secret"
MAILCHIMP_AUDIENCE_ID="replace-with-local-secret"
MAILCHIMP_SERVER_PREFIX="replace-with-local-secret"
148 changes: 148 additions & 0 deletions .github/workflows/agent-api-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: OpenNext security

on:
pull_request:
paths:
- 'src/**'
- 'scripts/**'
- 'public/**'
- 'test/**'
- 'next.config.mjs'
- 'open-next.config.ts'
- 'wrangler.jsonc'
- 'package.json'
- 'package-lock.json'
- '.node-version'
- '.github/workflows/agent-api-security.yml'
push:
branches: [main]
paths:
- 'src/**'
- 'scripts/**'
- 'public/**'
- 'test/**'
- 'next.config.mjs'
- 'open-next.config.ts'
- 'wrangler.jsonc'
- 'package.json'
- 'package-lock.json'
- '.node-version'
- '.github/workflows/agent-api-security.yml'

permissions:
contents: read

jobs:
validate-worker:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '22.23.1'
package-manager-cache: false

- name: Install locked dependencies
run: npm ci

Check warning on line 51 in .github/workflows/agent-api-security.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=yourselftoscience_yourselftoscience.org&issues=AZ_KGuAJKItL4neU0TfD&open=AZ_KGuAJKItL4neU0TfD&pullRequest=35

- name: Validate migration invariants
shell: bash
run: |
set -euo pipefail
node --check next.config.mjs
node --check src/middleware.js
node --check src/lib/agentApi.js
node --check scripts/extendAgentOpenApi.js
node --check scripts/validateAgentArtifacts.js
node --check scripts/enforceRuntimeAudit.js

if grep -RInE --include='*.js' --include='*.jsx' --include='*.ts' --include='*.tsx' \
"export const runtime[[:space:]]*=[[:space:]]*['\"]edge['\"]" src; then
echo 'OpenNext does not support route-level Edge Runtime declarations.' >&2
exit 1
fi

if grep -q '"@cloudflare/next-on-pages"' package.json package-lock.json; then
echo 'Archived next-on-pages dependency remains in the repository.' >&2
exit 1
fi

- name: Run security regression tests
run: npm test

- name: Regenerate and validate agent contracts
run: |
npm run update-openapi
npm run extend-openapi
npm run validate-agent-artifacts

- name: Build OpenNext Worker
env:
NEXTJS_ENV: production
NEXT_TELEMETRY_DISABLED: '1'
run: npx opennextjs-cloudflare build

Check warning on line 88 in .github/workflows/agent-api-security.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define exact package version to avoid installing unverified releases.

See more on https://sonarcloud.io/project/issues?id=yourselftoscience_yourselftoscience.org&issues=AZ_KGuAJKItL4neU0TfF&open=AZ_KGuAJKItL4neU0TfF&pullRequest=35

Check warning on line 88 in .github/workflows/agent-api-security.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"npx" can install packages on-demand and run their lifecycle scripts.

See more on https://sonarcloud.io/project/issues?id=yourselftoscience_yourselftoscience.org&issues=AZ_KGuAJKItL4neU0TfE&open=AZ_KGuAJKItL4neU0TfE&pullRequest=35

- name: Validate Worker bundle and Free-plan size
shell: bash
run: |
set -euo pipefail
npx wrangler deploy --dry-run --outdir .wrangler-dry-run 2>&1 | tee wrangler-dry-run.log

Check warning on line 94 in .github/workflows/agent-api-security.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"npx" can install packages on-demand and run their lifecycle scripts.

See more on https://sonarcloud.io/project/issues?id=yourselftoscience_yourselftoscience.org&issues=AZ_KGuAJKItL4neU0TfG&open=AZ_KGuAJKItL4neU0TfG&pullRequest=35

Check warning on line 94 in .github/workflows/agent-api-security.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define exact package version to avoid installing unverified releases.

See more on https://sonarcloud.io/project/issues?id=yourselftoscience_yourselftoscience.org&issues=AZ_KGuAJKItL4neU0TfH&open=AZ_KGuAJKItL4neU0TfH&pullRequest=35

node <<'NODE'
import fs from 'node:fs';

const log = fs.readFileSync('wrangler-dry-run.log', 'utf8');
const match = log.match(/gzip:\s*([0-9.]+)\s*(KiB|MiB)/i);
if (!match) {
throw new Error('Could not determine compressed Worker size from Wrangler output.');
}

const value = Number(match[1]);
const kib = match[2].toLowerCase() === 'mib' ? value * 1024 : value;
console.log(`Compressed Worker size: ${kib.toFixed(2)} KiB`);

if (kib > 3072) {
throw new Error(`Compressed Worker exceeds the Workers Free 3 MiB limit: ${kib.toFixed(2)} KiB`);
}
NODE

- name: Generate dependency audit reports
run: |
npm audit --omit=dev --json > npm-audit-runtime.json || true
npm audit --json > npm-audit-full.json || true

- name: Upload validation reports
uses: actions/upload-artifact@v4
with:
name: open-next-validation
path: |
npm-audit-runtime.json
npm-audit-full.json
wrangler-dry-run.log
if-no-files-found: error
retention-days: 14

- name: Enforce runtime dependency audit
run: node scripts/enforceRuntimeAudit.js npm-audit-runtime.json npm-audit-full.json

- name: Enforce build dependency audit
run: |
node <<'NODE'
import fs from 'node:fs';

const audit = JSON.parse(fs.readFileSync('npm-audit-full.json', 'utf8'));
const counts = audit.metadata?.vulnerabilities || {};
const high = Number(counts.high || 0);
const critical = Number(counts.critical || 0);

if (high > 0 || critical > 0) {
throw new Error(`Build dependency audit contains ${high} high and ${critical} critical findings.`);
}

console.log('Build dependency audit has no high or critical findings.');
NODE
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ next-env.d.ts
*.swp

mcp/key.pem
/.open-next/
.dev.vars
cloudflare-env.d.ts
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.23.1
219 changes: 219 additions & 0 deletions docs/cloudflare-agent-api-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Cloudflare Workers deployment and agent API security

This application is deployed as a full-stack Next.js Worker through `@opennextjs/cloudflare`. The former Cloudflare Pages and `@cloudflare/next-on-pages` deployment path is retained only as a temporary rollback target until the Worker passes live verification and the custom-domain cutover is complete.

## Validated repository configuration

The repository currently uses:

- Worker name: `yourselftoscience-web`
- Node.js: `22.23.1`
- Next.js: `15.5.22`
- React and React DOM: `19.2.8`
- OpenNext Cloudflare adapter: `1.20.2`
- Wrangler: `4.114.0`
- Worker entry point: `.open-next/worker.js`
- Static assets: `.open-next/assets`, exposed through the `ASSETS` binding
- Compatibility date: `2026-08-04`
- Compatibility flags: `nodejs_compat`, `global_fetch_strictly_public`
- OpenNext static-assets incremental cache; no R2, D1, KV, Durable Objects, Queues, Browser Rendering, or Images binding

All route-level `export const runtime = 'edge'` declarations have been removed because OpenNext uses the Next.js Node.js runtime on Cloudflare Workers.

GitHub Actions must remain read-only. It installs the locked dependencies, runs regression tests, regenerates and validates agent artifacts, builds the OpenNext Worker, performs a Wrangler dry run, enforces the Workers Free compressed-size limit of 3 MiB, and blocks high or critical runtime dependency findings.

## Create the Worker from GitHub

In Cloudflare, choose **Workers & Pages → Create application → Continue with GitHub** and configure:

| Setting | Value |
| --- | --- |
| Repository | `yourselftoscience/yourselftoscience.org` |
| Worker name | `yourselftoscience-web` |
| Production branch during migration | `feat/agent-ready-cloudflare` |
| Root directory | Leave blank |
| Build command | `npx @opennextjs/cloudflare build` |
| Deploy command | `npx @opennextjs/cloudflare deploy -- --keep-vars` |
| Non-production branch deploy command | `npx @opennextjs/cloudflare upload -- --keep-vars` |
| Build cache | Enabled |
| Non-production branch builds | Enabled |

The Cloudflare Worker name must exactly match the `name` in `wrangler.jsonc`.

Add this **build variable**:

| Type | Name | Value |
| --- | --- | --- |
| Plaintext | `NODE_VERSION` | `22.23.1` |

`NEXTJS_ENV` does not need to be configured for production because OpenNext defaults it to `production`.

Keep the generated Workers API token. Do not create a custom token unless the automatically generated token fails or organizational policy requires one.

## Runtime secrets

After the Worker exists, open **Worker → Settings → Variables and Secrets** and add each item as type **Secret**:

- `RESEND_API_KEY`
- `MAILCHIMP_API_KEY`
- `MAILCHIMP_AUDIENCE_ID`
- `MAILCHIMP_SERVER_PREFIX`

Select **Deploy** after saving the secrets. They are consumed at runtime through `process.env` and must not be added as plaintext variables or committed to Git.

For local development, copy `.dev.vars.example` to `.dev.vars`, replace the placeholders, and never commit that file.

## Runtime settings

Use the following settings:

| Setting | Value |
| --- | --- |
| Workers.dev | Enabled during migration |
| Preview URLs | Enabled |
| Placement | Default |
| Observability | Enabled |
| Log sampling | 100% during migration; reduce later if volume warrants it |
| Fail open | Not applicable to the Worker deployment |
| R2/D1/KV/Durable Objects/Queues | None |
| Images binding | None |

The site intentionally keeps `images.unoptimized: true`, so no Cloudflare Images binding is needed.

## Free-plan rate limiting

The Cloudflare Free plan provides one zone-level rate-limiting rule. Use it for all dynamic API and analytics-event routes rather than static pages.

**Name:** `API and dynamic endpoint protection`

**Expression:**

```text
(starts_with(http.request.uri.path, "/api/") or http.request.uri.path eq "/umami/api/send")
```

**Counting characteristic:** IP address

**Threshold:** 30 requests per 10 seconds

**Mitigation timeout:** 10 seconds

**Action:** Block

**Status:** Active

This rule does not apply to static pages, images, JavaScript, CSS, datasets, or other static assets. Keep it at the zone level so it follows the `yourselftoscience.org` hostname after the Pages-to-Workers cutover.

## Preview verification

Do not attach the production domain immediately. First test the provided Workers preview or `workers.dev` hostname.

Replace `<preview-host>` below with the actual host:

```bash
curl --fail-with-body https://<preview-host>/
curl --fail-with-body https://<preview-host>/.well-known/agent-card.json
curl --fail-with-body https://<preview-host>/api/health
curl --fail-with-body 'https://<preview-host>/api/resources?limit=1'
curl --fail-with-body 'https://<preview-host>/api/resources?activelyRecruiting=false&limit=1'
curl --fail-with-body https://<preview-host>/api/facets
curl --fail-with-body https://<preview-host>/openapi.json
curl --fail-with-body https://<preview-host>/llms.txt
curl --fail-with-body https://<preview-host>/resources.json
curl --fail-with-body https://<preview-host>/umami/script.js
```

The following malformed requests must return HTTP 400 without exposing an exception or stack trace:

```bash
curl -i 'https://<preview-host>/api/resources?activelyRecruiting=yes'
curl -i 'https://<preview-host>/api/resources?limit=1.5'
curl -i 'https://<preview-host>/api/resources?q=a&q=b'
curl -i 'https://<preview-host>/api/resources/%252Fetc'
```

The analytics event proxy must reject unsupported and oversized events:

```bash
curl -i -X POST 'https://<preview-host>/umami/api/send' \
-H 'Content-Type: text/plain' \
--data 'not-json'

python - <<'PY' | curl -i -X POST 'https://<preview-host>/umami/api/send' \
-H 'Content-Type: application/json' \
--data-binary @-
import json
print(json.dumps({'payload': 'x' * (70 * 1024)}))
PY
```

Require HTTP 415 for the first analytics request and HTTP 413 for the second. Confirm middleware redirects include `Cache-Control: private, no-store`.

Also test:

- a real contact-form submission through Resend;
- a Mailchimp subscription that enters the pending/double-opt-in state;
- a valid JSON analytics event;
- representative resource and data-type pages;
- browser navigation, CSS, icons, and downloadable data files;
- Worker logs for uncaught exceptions, CPU-limit errors, or missing bindings.

## MCP verification

The MCP service remains a separate Worker and must not be moved or captured by the website Worker:

```text
https://mcp.yourselftoscience.org/mcp
```

Verify initialization:

```bash
curl -i -X POST 'https://mcp.yourselftoscience.org/mcp' \
-H 'Origin: https://yourselftoscience.org' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"deployment-smoke-test","version":"1.0.0"}}}'
```

Require a valid JSON-RPC or SSE response, preserve any returned `Mcp-Session-Id`, and verify `tools/list` through the same session.

A hostile browser origin must be rejected:

```bash
curl -i -X POST 'https://mcp.yourselftoscience.org/mcp' \
-H 'Origin: https://attacker.invalid' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"origin-security-test","version":"1.0.0"}}}'
```

Treat acceptance of the hostile origin as a deployment blocker.

## Custom-domain cutover

Only after the Worker preview passes:

1. Record the existing Pages custom domains and current DNS configuration.
2. Remove `yourselftoscience.org` from the `yourselftoscience-org` Pages project.
3. Add `yourselftoscience.org` as a custom domain on `yourselftoscience-web`.
4. Move `www.yourselftoscience.org` as well if it is currently attached to Pages.
5. Do not add a wildcard route or wildcard custom domain.
6. Leave `mcp.yourselftoscience.org` attached exclusively to `yts-mcp`.
7. Repeat all production smoke tests and confirm the rate-limiting rule records events.
8. Keep the Pages project undeleted until the Worker has operated correctly in production and rollback is no longer needed.

After the cutover is verified, change the Worker production branch from `feat/agent-ready-cloudflare` to `main`, merge the pull request, and verify the resulting `main` deployment before retiring Pages.

## Operational monitoring

Monitor:

- `/api/*` and `/umami/api/send` request rates and 429 responses;
- Worker CPU time, exceptions, and invocation counts;
- `/api/health` 503 responses;
- upstream `resources.json` fetch failures;
- contact and subscription provider failures;
- MCP initialization failures, Origin rejections, and tool-call latency.

Do not expose exception messages, stack traces, secret values, bindings, session identifiers, or deployment metadata in public responses.
Loading
Loading