Merge pull request #1094 from Molecule-AI/staging

promote: CSP platform_url whitelist
This commit is contained in:
Hongming Wang 2026-04-20 12:55:15 -07:00 committed by GitHub
commit 5edc95e279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 104 additions and 3 deletions

View File

@ -58,6 +58,21 @@ describe("buildCsp — production", () => {
expect(connectSrc).toContain("wss:");
});
it("adds NEXT_PUBLIC_PLATFORM_URL origin to connect-src (CP cross-origin fetches)", () => {
// Canvas calls fetch(PLATFORM_URL + "/cp/auth/me") etc. from the
// browser. The browser blocks cross-origin fetches that aren't in
// connect-src, even with CORS headers set server-side. Whitelisting
// the origin is what makes fresh login + session refresh work on
// tenant subdomains after they were broken pre-2026-04-20 by a CSP
// that only allowed 'self' and wss:.
process.env.NEXT_PUBLIC_PLATFORM_URL = "https://api.example.com";
const built = buildCsp(TEST_NONCE, false);
const connectSrc = built.match(/connect-src[^;]*/)?.[0] ?? "";
expect(connectSrc).toContain("https://api.example.com");
expect(connectSrc).toContain("wss://api.example.com");
delete process.env.NEXT_PUBLIC_PLATFORM_URL;
});
it("does NOT include bare ws: in connect-src (prod uses wss only)", () => {
const connectSrc = csp.match(/connect-src[^;]*/)?.[0] ?? "";
// ws: (without 's') is insecure — should not be in production policy
@ -95,9 +110,13 @@ describe("buildCsp — development", () => {
expect(scriptSrc()).toContain("'unsafe-eval'");
});
it("allows ws: in connect-src (HMR WebSocket uses plain ws://)", () => {
it("permits ws:// in connect-src (HMR WebSocket uses plain ws://)", () => {
const connectSrc = csp.match(/connect-src[^;]*/)?.[0] ?? "";
expect(connectSrc).toContain("ws:");
// Dev uses `connect-src *` (fully permissive) which subsumes ws:.
// Accept either the literal `ws:` scheme wildcard OR the blanket
// `*` since both let HMR through. Prod tests still enforce `ws:`
// is NOT present.
expect(connectSrc).toMatch(/\bws:|\*/);
});
});

View File

@ -35,6 +35,26 @@ export function buildCsp(nonce: string, isDev: boolean): string {
].join("; ") + ";";
}
// Canvas makes cross-origin fetches to the control plane for
// /cp/auth/*, /cp/orgs/*, /cp/billing/* — PLATFORM_URL points at
// it (baked in at build time via NEXT_PUBLIC_PLATFORM_URL). CSP
// has to whitelist that origin in connect-src or the browser
// refuses the fetch with "Refused to connect because it violates
// the document's Content Security Policy."
//
// Self-hosted deployments override PLATFORM_URL at build time and
// the CSP adjusts automatically — no hardcoded hostname here.
const platformURL = process.env.NEXT_PUBLIC_PLATFORM_URL ?? "";
const connectSrcParts = ["'self'", "wss:"];
if (platformURL) {
connectSrcParts.push(platformURL);
// Also allow the wss:// sibling of PLATFORM_URL explicitly.
// `wss:` scheme-wildcard covers it today but making the exact
// origin explicit survives a future CSP tightening without
// silently breaking auth.
connectSrcParts.push(platformURL.replace(/^http/, "ws"));
}
return [
"default-src 'self'",
// Nonce-based: no unsafe-inline, no unsafe-eval.
@ -49,7 +69,7 @@ export function buildCsp(nonce: string, isDev: boolean): string {
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"connect-src 'self' wss:",
`connect-src ${connectSrcParts.join(" ")}`,
"worker-src 'self' blob:",
"upgrade-insecure-requests",
].join("; ") + ";";

View File

@ -0,0 +1,18 @@
#!/bin/bash
# Full nuke + rebuild — one command to reset everything
# Usage: bash scripts/nuke-and-rebuild.sh
set -euo pipefail
echo "=== NUKE ==="
docker compose down -v 2>/dev/null || true
docker ps -a --format "{{.Names}}" | grep "^ws-" | xargs -r docker rm -f 2>/dev/null || true
docker volume ls --format "{{.Name}}" | grep "^ws-" | xargs -r docker volume rm 2>/dev/null || true
docker network rm molecule-monorepo-net 2>/dev/null || true
echo " cleaned"
echo "=== REBUILD ==="
docker compose up -d --build
echo " platform + canvas up"
echo "=== POST-REBUILD SETUP ==="
bash scripts/post-rebuild-setup.sh

View File

@ -0,0 +1,44 @@
#!/bin/bash
# Post-rebuild setup — run after docker compose up -d --build
# Inserts global secrets that the provisioner injects into every workspace container.
# Without these, agents can't call MiniMax or push to GitHub.
set -euo pipefail
DB_CONTAINER="${DB_CONTAINER:-molecule-monorepo-postgres-1}"
DB_USER="${DB_USER:-dev}"
DB_NAME="${DB_NAME:-molecule}"
PLATFORM_URL="${PLATFORM_URL:-http://127.0.0.1:8080}"
ADMIN_TOKEN="${ADMIN_TOKEN:-HlgeMb8LjQLXg/B4y8hYzhbCQlg5LNu0oEa4IjShARE=}"
echo "=== Waiting for platform health ==="
until curl -s --max-time 5 "$PLATFORM_URL/health" >/dev/null 2>&1; do
echo " waiting..."
sleep 3
done
echo " platform up"
echo "=== Inserting global secrets ==="
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c "
INSERT INTO global_secrets (key, encrypted_value, encryption_version) VALUES
('ANTHROPIC_BASE_URL', 'https://api.minimax.io/anthropic', 0),
('ANTHROPIC_AUTH_TOKEN', '${MINIMAX_API_KEY:-sk-cp-lHt-QFSyZwZxeo_fMbmLUX3VgHOwbKGMXUZb6PS2U15D3fqjDB2qPh1OVEzvfvWs9CgcrUpyU7C682uVT_8GBy9RFLaFzBcdLkKdVcPX4yj9UaXNTH82KVw}', 0),
('ANTHROPIC_MODEL', 'MiniMax-M2.7', 0),
('ANTHROPIC_SMALL_FAST_MODEL', 'MiniMax-M2.7', 0),
('API_TIMEOUT_MS', '3000000', 0),
('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', '1', 0),
('GITHUB_TOKEN', '${GITHUB_PAT:-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}', 0)
ON CONFLICT (key) DO UPDATE SET encrypted_value = EXCLUDED.encrypted_value;
"
echo " 7 global secrets set"
echo "=== Importing org template ==="
curl -s --max-time 600 -X POST "$PLATFORM_URL/org/import" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"dir":"molecule-dev"}' | head -1
echo ""
echo " import complete"
echo "=== Done ==="
echo "Run: http://127.0.0.1:3000 for canvas"