forked from molecule-ai/molecule-core
Merge pull request #1094 from Molecule-AI/staging
promote: CSP platform_url whitelist
This commit is contained in:
commit
5edc95e279
@ -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:|\*/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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("; ") + ";";
|
||||
|
||||
18
scripts/nuke-and-rebuild.sh
Normal file
18
scripts/nuke-and-rebuild.sh
Normal 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
|
||||
44
scripts/post-rebuild-setup.sh
Normal file
44
scripts/post-rebuild-setup.sh
Normal 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"
|
||||
Loading…
Reference in New Issue
Block a user