[object Object]

← back to Wallco Ai

fix(scripts): DB password from DATABASE_URL (.env), not empty PGPASSWORD env

4e68e65e18d8e01de45ee9c71cc5532312015070 · 2026-06-09 17:27:27 -0700 · Steve Abrams

The secret-scrub left these reading os.environ PGPASSWORD (not exported) → empty
pw → PG auth fail. _pg_password() now parses DATABASE_URL from .env (mirrors
edges-batch-scan.py); PGPASSWORD env still overrides. SELECT 1 verified all 4 paths.

Files touched

Diff

commit 4e68e65e18d8e01de45ee9c71cc5532312015070
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 9 17:27:27 2026 -0700

    fix(scripts): DB password from DATABASE_URL (.env), not empty PGPASSWORD env
    
    The secret-scrub left these reading os.environ PGPASSWORD (not exported) → empty
    pw → PG auth fail. _pg_password() now parses DATABASE_URL from .env (mirrors
    edges-batch-scan.py); PGPASSWORD env still overrides. SELECT 1 verified all 4 paths.
---
 scripts/build-tif.py               | 19 ++++++++++++++++++-
 scripts/fix-seam.py                | 21 +++++++++++++++++++--
 scripts/monterey-top12-manifest.py | 19 ++++++++++++++++++-
 scripts/regen-id-collisions.py     | 23 ++++++++++++++++++++++-
 4 files changed, 77 insertions(+), 5 deletions(-)

diff --git a/scripts/build-tif.py b/scripts/build-tif.py
index c3c063e..c7895d3 100755
--- a/scripts/build-tif.py
+++ b/scripts/build-tif.py
@@ -40,9 +40,26 @@ import shutil
 import subprocess
 from pathlib import Path
 from collections import Counter
+from urllib.parse import urlparse
 
 import psycopg2
 from psycopg2.extras import RealDictCursor
+
+
+def _pg_password():
+    """Resolve Postgres password: PGPASSWORD env var takes precedence, then
+    parse it out of DATABASE_URL in .env.  Never hard-code the secret."""
+    if os.environ.get("PGPASSWORD"):
+        return os.environ["PGPASSWORD"]
+    env_file = ROOT / ".env"
+    if env_file.exists():
+        for line in env_file.read_text().splitlines():
+            if line.startswith("DATABASE_URL="):
+                url = line.split("=", 1)[1].strip().strip('"')
+                parsed = urlparse(url)
+                if parsed.password:
+                    return parsed.password
+    return ""
 from PIL import Image, ImageStat
 
 ROOT = Path(__file__).resolve().parents[1]
@@ -70,7 +87,7 @@ VTRACER_BIN = shutil.which("vtracer")  # optional real vectorizer
 def db():
     return psycopg2.connect(
         host="127.0.0.1", port=5432,
-        user="dw_admin", password="DW2024SecurePass",
+        user="dw_admin", password=_pg_password(),
         dbname="dw_unified",
     )
 
diff --git a/scripts/fix-seam.py b/scripts/fix-seam.py
index b74f85b..9f253a2 100644
--- a/scripts/fix-seam.py
+++ b/scripts/fix-seam.py
@@ -14,10 +14,27 @@ Usage:
   python3 scripts/fix-seam.py --scan-flagged
 """
 import os, sys, argparse, json, subprocess
+from urllib.parse import urlparse
 from PIL import Image, ImageFilter
 
 ROOT = os.path.expanduser('~/Projects/wallco-ai')
 OUT = os.path.join(ROOT, 'data', 'seam-fix')
+
+
+def _pg_password():
+    """Resolve Postgres password: PGPASSWORD env var takes precedence, then
+    parse it out of DATABASE_URL in .env.  Never hard-code the secret."""
+    if os.environ.get("PGPASSWORD"):
+        return os.environ["PGPASSWORD"]
+    env_file = os.path.join(ROOT, ".env")
+    if os.path.exists(env_file):
+        for line in open(env_file):
+            if line.startswith("DATABASE_URL="):
+                url = line.split("=", 1)[1].strip().strip('"')
+                parsed = urlparse(url)
+                if parsed.password:
+                    return parsed.password
+    return ""
 LEDGER = os.path.join(OUT, 'fix-log.jsonl')
 SAMPLE_STEP = 20
 THRESHOLD = 20
@@ -62,7 +79,7 @@ def find_disk_path(design_id):
     # 1. PG canonical
     sql = f"SELECT local_path FROM spoon_all_designs WHERE id={design_id} LIMIT 1;"
     try:
-        env = {**os.environ, 'PGPASSWORD':'DW2024SecurePass'}
+        env = {**os.environ, 'PGPASSWORD': _pg_password()}
         r = subprocess.run(['psql','-h','127.0.0.1','-U','dw_admin','-d','dw_unified','-At','-c',sql],
                          capture_output=True, text=True, env=env, timeout=10)
         lp = r.stdout.strip()
@@ -143,7 +160,7 @@ def main():
         a,b = args.range.split('-')
         ids += list(range(int(a), int(b)+1))
     if args.scan_flagged:
-        env = {**os.environ, 'PGPASSWORD':'DW2024SecurePass'}
+        env = {**os.environ, 'PGPASSWORD': _pg_password()}
         r = subprocess.run(['psql','-h','127.0.0.1','-U','dw_admin','-d','dw_unified','-At','-c',
             "SELECT id FROM spoon_all_designs WHERE id BETWEEN 40000 AND 50000 AND is_published AND user_removed IS NOT TRUE ORDER BY id;"],
             capture_output=True, text=True, env=env, timeout=30)
diff --git a/scripts/monterey-top12-manifest.py b/scripts/monterey-top12-manifest.py
index 73aa8c4..cfb1508 100644
--- a/scripts/monterey-top12-manifest.py
+++ b/scripts/monterey-top12-manifest.py
@@ -14,10 +14,27 @@ Usage:
 """
 import argparse, json, os, subprocess, sys
 from pathlib import Path
+from urllib.parse import urlparse
 
 ROOT = Path(__file__).resolve().parents[1]
 
 
+def _pg_password():
+    """Resolve Postgres password: PGPASSWORD env var takes precedence, then
+    parse it out of DATABASE_URL in .env.  Never hard-code the secret."""
+    if os.environ.get("PGPASSWORD"):
+        return os.environ["PGPASSWORD"]
+    env_file = ROOT / ".env"
+    if env_file.exists():
+        for line in env_file.read_text().splitlines():
+            if line.startswith("DATABASE_URL="):
+                url = line.split("=", 1)[1].strip().strip('"')
+                parsed = urlparse(url)
+                if parsed.password:
+                    return parsed.password
+    return ""
+
+
 def psql_json(sql):
     # Connect the same way the app does on this host (peer auth / DATABASE_URL=dw_unified).
     env = dict(os.environ)
@@ -25,7 +42,7 @@ def psql_json(sql):
                        capture_output=True, text=True, env=env)
     if r.returncode != 0:
         # fall back to dw_admin TCP if peer auth isn't available
-        env["PGPASSWORD"] = env.get("PGPASSWORD", "DW2024SecurePass")
+        env["PGPASSWORD"] = _pg_password()
         r = subprocess.run(["psql", "-h", "127.0.0.1", "-U", "dw_admin", "-d", "dw_unified",
                             "-At", "-c", sql], capture_output=True, text=True, env=env)
         if r.returncode != 0:
diff --git a/scripts/regen-id-collisions.py b/scripts/regen-id-collisions.py
index bdddbfa..7c1d97d 100644
--- a/scripts/regen-id-collisions.py
+++ b/scripts/regen-id-collisions.py
@@ -1,4 +1,25 @@
 import json, subprocess, os
+from pathlib import Path
+from urllib.parse import urlparse
+
+_ROOT = Path(__file__).resolve().parents[1]
+
+
+def _pg_password():
+    """Resolve Postgres password: PGPASSWORD env var takes precedence, then
+    parse it out of DATABASE_URL in .env.  Never hard-code the secret."""
+    if os.environ.get("PGPASSWORD"):
+        return os.environ["PGPASSWORD"]
+    env_file = _ROOT / ".env"
+    if env_file.exists():
+        for line in env_file.read_text().splitlines():
+            if line.startswith("DATABASE_URL="):
+                url = line.split("=", 1)[1].strip().strip('"')
+                parsed = urlparse(url)
+                if parsed.password:
+                    return parsed.password
+    return ""
+
 
 SEL = ("SELECT id, is_published::int, COALESCE(category,''), md5(COALESCE(prompt,'')), "
        "COALESCE(substr(prompt,1,100),''), COALESCE(generator,''), COALESCE(parent_design_id::text,''), "
@@ -7,7 +28,7 @@ SEL = ("SELECT id, is_published::int, COALESCE(category,''), md5(COALESCE(prompt
        "FROM all_designs WHERE id BETWEEN 1 AND 14999 ORDER BY id;")
 
 def run_mac2(sql):
-    env = dict(os.environ, PGPASSWORD="DW2024SecurePass")
+    env = dict(os.environ, PGPASSWORD=_pg_password())
     r = subprocess.run(["psql","-h","127.0.0.1","-U","dw_admin","-d","dw_unified","-tAF","\t","-c",sql],
                        capture_output=True, text=True, env=env)
     if r.returncode != 0: raise RuntimeError("mac2 psql: "+r.stderr)

← 4a2ef63 restore script: generalize via CATEGORY_REGEX, hard-exclude  ·  back to Wallco Ai  ·  security: strip DB DSN passwords from audit/digest docs (use 52ea65f →