#!/usr/bin/env bash
# CO-019 lint-substitute (Carve-Outs Register).
#
# Fails (exit 1) if any server-side .ts file contains a literal-string fallback
# for a process.env-sourced CRYPTOGRAPHIC SECRET. Pattern:
#
#   const X = process.env.X_SECRET || "literal-default";
#   const X = process.env.X_KEY    ?? "literal-default";
#
# Only flags env names whose suffix or substring matches:
#   SECRET, TOKEN, SALT, PASSWORD, _PW, _KEY (with _ delimiter to avoid
#   false-positive matches like APIKEY_NAME)
# OR a sentinel literal known to belong to the FIND-H2 cluster:
#   aegis-cyber-secret-key, DEFAULT-CHANGE-IN-PROD, wave10-dev-pdf-secret-change-me
#
# Does NOT flag non-secret data defaults (PORT, NODE_ENV, BACKUP_DIR, paths,
# URLs, etc.) — those are not credential material and are out of CO-019 scope.
#
# The CO-019 standard is: source-side fail-closed for secrets — if the env var
# is unset or under-length, throw at runtime. The boot guard at
# server/lib/secret-boot-guard.ts catches the missing-secret case at startup;
# this lint is defence-in-depth so a regression bypassing the boot guard (e.g.,
# a test harness importing a module before guard runs, or a new module whose
# secret is not yet in the guard REQUIRED list) cannot silently use a
# predictable string.
#
# Run manually:
#   bash scripts/check-no-secret-fallback.sh
#
# Excluded:
#   - server/lib/ghost-api.ts  (FAKE_TRAP deception assets — never real credentials)
#   - test/spec/mock files
#   - comment-only lines (starting with // after trim)
#   - lines marked "// CO-019: intentional" or "// CO-019: not a secret" above

set -u

mapfile -t FILES < <(find server -type f -name "*.ts" \
  ! -path "*/ghost-api.ts" \
  ! -name "*.test.ts" \
  ! -name "*.spec.ts" \
  | sort)

violations=0
violation_lines=""

# Credential-shaped env name patterns (case-sensitive — env vars conventionally UPPER):
#   *SECRET, *TOKEN, *SALT, *PASSWORD, *_PW (as suffix), *_KEY (as suffix)
ENV_CRED_RE='process\.env\.[A-Z_][A-Z0-9_]*(SECRET|TOKEN|SALT|PASSWORD|_PW|_KEY)[A-Z0-9_]*[[:space:]]*(\|\||\?\?)[[:space:]]*"[^"]+"'
# Sentinel literals from the H2 cluster:
SENTINEL_RE='"[^"]*(aegis-cyber-secret-key|DEFAULT-CHANGE-IN-PROD|wave10-dev-pdf-secret-change-me)[^"]*"'

for f in "${FILES[@]}"; do
  while IFS= read -r match_line; do
    [ -z "$match_line" ] && continue
    n="${match_line%%:*}"

    line_content=$(sed -n "${n}p" "$f")

    # Skip pure comment lines.
    trimmed=$(echo "$line_content" | sed 's/^[[:space:]]*//')
    if [[ "$trimmed" == //* ]] || [[ "$trimmed" == \** ]]; then
      continue
    fi

    # Skip empty-string fallback (different category, downstream code throws).
    if echo "$line_content" | grep -Eq '(\|\||\?\?)[[:space:]]*""'; then
      continue
    fi

    # Allow if a CO-019 carve-out marker appears in any of the 10 lines
    # immediately above the violation site. The 10-line window lets a single
    # multi-line carve-out comment cover a small cluster of credential-shaped
    # lines (e.g., the admin/risk/auditor triplet inside a Record literal,
    # which is separated from its preceding comment block by the variable
    # declaration line itself). The marker strings ("CO-019: intentional",
    # "CO-019: not a secret") are distinctive enough that contiguity is not
    # required for false-acceptance safety — no production code would contain
    # those substrings except as a deliberate carve-out annotation.
    window_start=$((n - 10))
    if [ "$window_start" -lt 1 ]; then window_start=1; fi
    if sed -n "${window_start},$((n - 1))p" "$f" 2>/dev/null \
        | grep -Eq 'CO-019: not a secret|CO-019: intentional'; then
      continue
    fi

    violations=$((violations + 1))
    violation_lines="$violation_lines\n$f:$n: $trimmed"
  done < <(grep -nE "$ENV_CRED_RE|$SENTINEL_RE" "$f" 2>/dev/null || true)
done

if [ "$violations" -gt 0 ]; then
  echo "CO-019 regression detected — $violations literal-fallback site(s) for env-sourced credentials in server/:"
  echo
  printf "%b\n" "$violation_lines"
  echo
  echo "Replace with explicit-throw-if-unset, e.g.:"
  echo "  const X = (() => {"
  echo "    const v = process.env.X_SECRET;"
  echo "    if (!v || v.length < 32) throw new Error(\"X_SECRET required (>=32 chars). CO-019.\");"
  echo "    return v;"
  echo "  })();"
  echo
  echo "Or use a requireSecret(name, minLen) helper local to the file."
  echo
  echo "If the literal is an intentional ephemeral test fixture (not a real production"
  echo "credential), mark the line above with:"
  echo "  // CO-019: intentional — <reason, e.g. ephemeral RBAC test user, deleted in same-function finally block>"
  exit 1
fi

echo "CO-019 check passed — no literal-string fallback for env-sourced credentials in server/."
