Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 20 additions & 2 deletions scripts/rotate-db-secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import argparse
import json
import os
import shutil
import stat
import subprocess
import sys
Expand Down Expand Up @@ -54,7 +55,12 @@ def op_get(path):
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
body = r.read()
try:
return json.loads(body)
except json.JSONDecodeError:
print(f"ERROR: 1Password Connect returned non-JSON: {body[:200]}", file=sys.stderr)
sys.exit(1)
Comment on lines +58 to +63
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the JSONDecodeError path, body is bytes and the error message prints body[:200] directly, which ends up as a b'...' repr and may include non-printable characters. For consistency with the HTTPError branch (which decodes with errors="replace") and to keep logs readable/safe, decode/sanitize the snippet (and consider including response status/content-type instead of dumping raw bytes).

Copilot uses AI. Check for mistakes.
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:200]
print(f"ERROR: 1Password Connect returned HTTP {e.code}: {body}", file=sys.stderr)
Expand All @@ -76,7 +82,12 @@ def neon_post(path, api_key):
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
body = r.read()
try:
return json.loads(body)
except json.JSONDecodeError:
print(f"ERROR: Neon API returned non-JSON: {body[:200]}", file=sys.stderr)
sys.exit(1)
Comment on lines +85 to +90
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as op_get(): this JSONDecodeError branch prints a raw bytes slice in the error message. Decode/sanitize the snippet (e.g., utf-8 with errors="replace") to keep output readable and avoid logging arbitrary bytes.

Copilot uses AI. Check for mistakes.
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:200]
print(f"ERROR: Neon API returned HTTP {e.code}: {body}", file=sys.stderr)
Expand Down Expand Up @@ -143,6 +154,13 @@ def deploy_secret(database_url, wrangler_config, env_name):
print("ERROR: OP_CONNECT_TOKEN environment variable is required", file=sys.stderr)
sys.exit(1)

# Pre-flight: verify npx/wrangler available BEFORE rotating the password.
# If wrangler is missing, rotating would leave a new password that can't be deployed.
if not shutil.which("npx"):
print("ERROR: npx not found on PATH. Cannot deploy secrets to Cloudflare Workers.", file=sys.stderr)
print("ABORTING before password rotation to avoid partial failure.", file=sys.stderr)
sys.exit(1)

Comment on lines +157 to +163
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pre-flight comment says to verify "npx/wrangler" availability, but the code only checks npx is on PATH. Either adjust the comment to match what’s actually being validated, or extend the pre-flight to also validate that npx wrangler can run (e.g., npx wrangler --version with a short timeout) before rotating the password.

Suggested change
# Pre-flight: verify npx/wrangler available BEFORE rotating the password.
# If wrangler is missing, rotating would leave a new password that can't be deployed.
if not shutil.which("npx"):
print("ERROR: npx not found on PATH. Cannot deploy secrets to Cloudflare Workers.", file=sys.stderr)
print("ABORTING before password rotation to avoid partial failure.", file=sys.stderr)
sys.exit(1)
# Pre-flight: verify npx and wrangler are available BEFORE rotating the password.
# If wrangler is missing, rotating would leave a new password that can't be deployed.
if not shutil.which("npx"):
print("ERROR: npx not found on PATH. Cannot deploy secrets to Cloudflare Workers.", file=sys.stderr)
print("ABORTING before password rotation to avoid partial failure.", file=sys.stderr)
sys.exit(1)
try:
# Ensure that `npx wrangler` is runnable before proceeding.
subprocess.run(
["npx", "wrangler", "--version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
timeout=10,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
print("ERROR: 'npx wrangler' is not available or failed to run. Cannot deploy secrets to Cloudflare Workers.", file=sys.stderr)
print("ABORTING before password rotation to avoid partial failure.", file=sys.stderr)
sys.exit(1)

Copilot uses AI. Check for mistakes.
# ── Step 1: retrieve Neon API key from 1Password ──────────────────────────────

print("[1] Retrieving Neon API key from 1Password Connect...", file=sys.stderr)
Expand Down
2 changes: 1 addition & 1 deletion server/routes/wave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ waveCallbackRoute.get('/api/integrations/wave/callback', async (c) => {
return c.redirect(`${baseUrl}/connections?wave=connected`);
} catch (err) {
console.error('Wave callback error:', err);
return c.redirect(`${baseUrl}/connections?wave=error`);
return c.redirect(`${baseUrl}/connections?wave=error&reason=callback_failed`);
}
});

Expand Down
Loading