#!/bin/bash
# get-secret — fetch and USE secrets from Infisical via the official `infisical` CLI.
#
# To USE a secret, prefer `exec`: it wraps `infisical run`, which starts your command
# in a NEW process with the secrets in its environment — the value never crosses stdout,
# so it cannot leak into an AI chat transcript.
#
#   get-secret [--env E] exec <folder> -- <cmd> [args...]   # inject secrets into env, run cmd (AI-safe)
#   get-secret [--env E] <folder>/<NAME> [--show]           # single value: TTY -> plaintext, pipe(agent) -> [redacted]
#   get-secret [--env E] list [<folder>]                    # list folders / keys
#   get-secret [--env E] env|eval <folder>                  # KEY=VALUE / export lines (only inline / command-sub)
#
# Setup:
#   1) Install the Infisical CLI + jq:  https://infisical.com/docs/cli/overview
#   2) Create a Machine Identity (Universal Auth) in Infisical and give it read access.
#   3) Store its client_id:client_secret in the OS keychain (macOS shown):
#        security add-generic-password -s "$KEYCHAIN_SVC" -a "$USER" -U -w
#      (paste CLIENT_ID:CLIENT_SECRET at the prompt; on Linux use secret-tool/pass).
#   4) Fill INFISICAL_DOMAIN + KEYCHAIN_SVC below, drop this file in your PATH, chmod +x.
#   5) Per repo, bind it to a project once — create .infisical.json (committable, not secret):
#        printf '{"workspaceId":"<projectId>","defaultEnvironment":"prod"}' > .infisical.json
#      (`infisical init` does the same interactively, but needs a login session; with a
#       machine-identity-only setup, writing the file by hand is simplest.)
set -euo pipefail

# --- CONFIG: edit these (auth only — project/env live in .infisical.json) ---
INFISICAL_DOMAIN="https://your-infisical-instance.example.com"   # or https://app.infisical.com
KEYCHAIN_SVC="infisical-machine-identity"                        # keychain service holding CLIENT_ID:CLIENT_SECRET
# ---------------------------------------------------------------------------
CACHE_FILE="$HOME/.cache/infisical-token"
CACHE_TTL_MIN=25   # must stay below the machine identity's access-token TTL

err() { echo "get-secret: $*" >&2; exit 1; }
command -v infisical >/dev/null 2>&1 || err "missing dependency: infisical CLI"
command -v jq >/dev/null 2>&1 || err "missing dependency: jq"

# Project + environment come from the repo's .infisical.json. Machine-identity auth ALWAYS
# requires an explicit project id (the CLI does not read workspaceId from the file in this
# mode), so we read it here. No .infisical.json in cwd -> hard error.
[ -f .infisical.json ] || err "no .infisical.json in $(pwd). Bind this repo to a project once:
  printf '{\"workspaceId\":\"<projectId>\",\"defaultEnvironment\":\"prod\"}' > .infisical.json"
jq -e . .infisical.json >/dev/null 2>&1 || err ".infisical.json is not valid JSON."
PROJECT_ID=$(jq -r '.workspaceId // empty' .infisical.json)
ENVIRONMENT=$(jq -r '.defaultEnvironment // empty' .infisical.json)
[ -n "$PROJECT_ID" ] || err ".infisical.json has no workspaceId."

# leading --env overrides the file's defaultEnvironment (e.g. staging/prod)
while [ $# -gt 0 ]; do
  case "$1" in
    --env) shift; ENVIRONMENT="${1:?--env needs a value}"; shift ;;
    *) break ;;
  esac
done
[ -n "$ENVIRONMENT" ] || err "no environment — set defaultEnvironment in .infisical.json or pass --env."

# Obtain a short-lived machine-identity token (cached) via the official CLI — no interactive login.
get_token() {
  if [ -f "$CACHE_FILE" ] && [ -n "$(find "$CACHE_FILE" -mmin -${CACHE_TTL_MIN} 2>/dev/null)" ]; then
    cat "$CACHE_FILE"; return
  fi
  local creds cid csec tok
  creds=$(security find-generic-password -s "$KEYCHAIN_SVC" -w 2>/dev/null) \
    || err "no creds in keychain (service=$KEYCHAIN_SVC). See the setup notes at the top."
  cid="${creds%%:*}"; csec="${creds##*:}"
  tok=$(infisical login --method=universal-auth --client-id="$cid" --client-secret="$csec" \
        --domain="$INFISICAL_DOMAIN" --plain --silent 2>/dev/null) || err "infisical login failed"
  [ -n "$tok" ] || err "login returned empty token"
  mkdir -p "$(dirname "$CACHE_FILE")"
  ( umask 077; printf '%s' "$tok" > "$CACHE_FILE" )   # 600 from creation, no chmod race
  printf '%s' "$tok"
}

cmd_exec() {
  local folder="${1:-}"; [ -n "$folder" ] || err "usage: get-secret exec <folder> -- <cmd> [args...]"
  shift
  [ "${1:-}" = "--" ] || err "usage: get-secret exec <folder> -- <cmd> [args...] (missing '--')"
  shift
  [ "$#" -gt 0 ] || err "no command given after '--'"
  local tok; tok=$(get_token)
  # `infisical run` injects the folder's secrets as env vars, then runs the command in a new
  # process. Values flow store -> env -> child; they never touch stdout.
  exec infisical run --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
    --env "$ENVIRONMENT" --path "/$folder" --silent -- "$@"
}

cmd_get() {
  local show=0 arg="" a
  for a in "$@"; do case "$a" in
    --show) show=1 ;; -*) err "unknown flag: $a" ;; *) arg="$a" ;;
  esac; done
  [ -n "$arg" ] || err "expected <folder>/<NAME>"
  [[ "$arg" == */* ]] || err "expected <folder>/<NAME>, got: $arg"
  local folder="${arg%/*}" name="${arg##*/}" tok val
  tok=$(get_token)
  # stderr left visible so an auth/network failure surfaces instead of masquerading as "not found".
  val=$(infisical secrets get "$name" --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
    --env "$ENVIRONMENT" --path "/$folder" --plain --silent) || true
  [ -n "$val" ] || err "secret not found: /$folder/$name"
  if [ "$show" -eq 1 ] || [ -t 1 ]; then
    printf '%s\n' "$val"
  else
    printf '[redacted len=%s]\n' "${#val}"
    echo "get-secret: value hidden (stdout is not a TTY). To use it: get-secret exec $folder -- <cmd>. To reveal it in your terminal: get-secret $folder/$name --show" >&2
  fi
}

cmd_list() {
  local folder="${1:-}" tok; tok=$(get_token)
  if [ -z "$folder" ]; then
    infisical secrets folders get --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
      --env "$ENVIRONMENT" --path / -o json --silent 2>/dev/null | jq -r '.[]?.folderName // empty' | sort
  else
    infisical secrets --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
      --env "$ENVIRONMENT" --path "/$folder" -o json --silent 2>/dev/null | jq -r '.[]?.secretKey // empty' | sort
  fi
}

cmd_json() {
  local folder="$1" tok; tok=$(get_token)
  infisical secrets --token "$tok" --projectId "$PROJECT_ID" --domain "$INFISICAL_DOMAIN" \
    --env "$ENVIRONMENT" --path "/$folder" -o json --silent 2>/dev/null
}
cmd_env()  { local f="${1:?usage: get-secret env <folder>}";  cmd_json "$f" | jq -r '.[]? | "\(.secretKey)=\(.secretValue)"'; }
cmd_eval() { local f="${1:?usage: get-secret eval <folder>}"; cmd_json "$f" | jq -r '.[]? | "export \(.secretKey)=\(.secretValue | @sh)"'; }

case "${1:-}" in
  ""|-h|--help) echo "usage: get-secret [--env E] {exec <f> -- <cmd> | <f>/<NAME> [--show] | list [f] | env <f> | eval <f>}"; exit 0 ;;
  exec) shift; cmd_exec "$@" ;;
  list) shift; cmd_list "$@" ;;
  env)  shift; cmd_env  "$@" ;;
  eval) shift; cmd_eval "$@" ;;
  *)    cmd_get "$@" ;;
esac
