Last active
April 9, 2026 02:16
-
-
Save msrivastav13/2a7d237daf2c649a06cc9c1badbf3d62 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # setup_tdx_av.sh | |
| # Creates TDX_AV/ with two SFDX projects (Todo: standard, Pronto: reactexternalapp), | |
| # logs in via `sf org login web`, sets the org as default for both projects, | |
| # and runs `npm install` in each. | |
| # | |
| # Surfaces OS permission errors immediately with actionable hints. | |
| # | |
| # Requires: sf CLI, node/npm. | |
| set -eo pipefail | |
| ROOT_DIR="TDX_AV" | |
| TARGET_PARENT="${TDX_AV_DIR:-$HOME/Desktop}" # override with TDX_AV_DIR=/some/path | |
| TARGET_DIR="$TARGET_PARENT/$ROOT_DIR" | |
| STANDARD_PROJ="Todo" | |
| B2C_PROJ="Pronto" | |
| ORG_ALIAS="TDX26" | |
| # Heroku sandbox broker. Override SANDBOX_API at runtime if needed. | |
| SANDBOX_API="${SANDBOX_API:-https://tdx-av-zone-broker-663ef89ce24a.herokuapp.com}" | |
| LOGIN_URL="https://test.salesforce.com" | |
| CLIENT_UA="tdx-setup/1.0" | |
| step() { printf "\n\033[1;34m==> %s\033[0m\n" "$1"; } | |
| err() { printf "\n\033[1;31m!! %s\033[0m\n" "$1" 1>&2; } | |
| sudo_hint() { | |
| { | |
| err "Permission denied while: $1" | |
| echo " The OS rejected the operation. To proceed, re-run with sudo:" | |
| echo " sudo $0" | |
| echo " Or fix ownership of the parent directory first:" | |
| echo " sudo chown -R \"\$(whoami)\" \"$(pwd)\"" | |
| } 1>&2 | |
| } | |
| node_perm_hint() { | |
| { | |
| err "npm hit a permission error (EACCES)." | |
| echo " npm can't write to its cache, prefix, or this project folder." | |
| echo " Run ONE of the following, then re-run this script:" | |
| echo | |
| echo " 1) Fix ownership of the npm cache (recommended):" | |
| echo " sudo chown -R \"\$(whoami)\" \"\$HOME/.npm\"" | |
| echo | |
| echo " 2) Fix ownership of this project directory:" | |
| echo " sudo chown -R \"\$(whoami)\" \"$(pwd)\"" | |
| echo | |
| echo " 3) Switch to a user-space Node via nvm (no sudo needed afterwards):" | |
| echo " brew install nvm" | |
| echo " mkdir -p \"\$HOME/.nvm\"" | |
| echo " export NVM_DIR=\"\$HOME/.nvm\"" | |
| echo " . \"\$(brew --prefix nvm)/nvm.sh\"" | |
| echo " nvm install --lts && nvm use --lts" | |
| } 1>&2 | |
| } | |
| # run_check <label> <cmd> [args...] | |
| # Streams output live and, on failure, classifies the error. | |
| run_check() { | |
| local label="$1"; shift | |
| local logfile | |
| logfile=$(mktemp) | |
| if ! "$@" 2>&1 | tee "$logfile"; then | |
| if grep -qiE "permission denied|eacces|operation not permitted" "$logfile"; then | |
| case "$1" in | |
| npm|npx|node) node_perm_hint ;; | |
| *) sudo_hint "$label" ;; | |
| esac | |
| fi | |
| rm -f "$logfile" | |
| exit 1 | |
| fi | |
| rm -f "$logfile" | |
| } | |
| # ---- Sandbox broker helpers ------------------------------------------------ | |
| # All JSON parsing uses Node (already required by this script — see README). | |
| # No python anywhere. | |
| require_node() { | |
| command -v node >/dev/null 2>&1 || { err "node is required on PATH"; exit 1; } | |
| } | |
| # json_get <key> — read JSON on stdin, print value at top-level key. | |
| json_get() { | |
| node -e ' | |
| let s = ""; | |
| process.stdin.on("data", d => s += d).on("end", () => { | |
| try { | |
| const v = JSON.parse(s)[process.argv[1]]; | |
| if (v == null) process.exit(2); | |
| process.stdout.write(String(v)); | |
| } catch (e) { process.exit(1); } | |
| }); | |
| ' "$1" | |
| } | |
| # json_get_path <dot.path> — read JSON on stdin, print nested value. | |
| json_get_path() { | |
| node -e ' | |
| let s = ""; | |
| process.stdin.on("data", d => s += d).on("end", () => { | |
| try { | |
| let v = JSON.parse(s); | |
| for (const k of process.argv[1].split(".")) v = v == null ? v : v[k]; | |
| if (v == null) process.exit(2); | |
| process.stdout.write(String(v)); | |
| } catch (e) { process.exit(1); } | |
| }); | |
| ' "$1" | |
| } | |
| # claim_sandbox <pin> — POST /claim, populates CLAIMED_USER. | |
| # The broker NEVER returns a password — the attendee types their own | |
| # sandbox password in the Salesforce browser login screen. | |
| claim_sandbox() { | |
| local pin="$1" resp http | |
| resp=$(mktemp) | |
| chmod 600 "$resp" 2>/dev/null || true | |
| http=$(curl -sS --max-time 10 -A "$CLIENT_UA" \ | |
| -o "$resp" -w '%{http_code}' \ | |
| -X POST "$SANDBOX_API/claim" \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Pin: $pin" \ | |
| --data '{}') \ | |
| || { err "Network error contacting sandbox service. Check Wi-Fi and try again."; rm -f "$resp"; exit 1; } | |
| if [ "$http" != "200" ]; then | |
| # Never echo the body — it could be misleading or attacker-controlled. | |
| case "$http" in | |
| 401) err "Wrong PIN. Double-check the PIN you were given for the event." ;; | |
| 409) err "Sandbox pool is empty. Please see a TDX staffer." ;; | |
| 429) err "This laptop is temporarily rate-limited. Wait a minute and retry." ;; | |
| 503) err "Sandbox service is in safe mode. Please see a TDX staffer." ;; | |
| *) err "Sandbox claim failed (HTTP $http). Please see a TDX staffer." ;; | |
| esac | |
| rm -f "$resp"; exit 1 | |
| fi | |
| CLAIMED_USER=$(json_get username < "$resp") \ | |
| || { err "Bad response from sandbox service."; rm -f "$resp"; exit 1; } | |
| rm -f "$resp" | |
| } | |
| # release_sandbox — POST /release on EXIT if we never confirmed. | |
| release_sandbox() { | |
| [ -n "${CLAIMED_USER:-}" ] && [ -z "${CLAIMED_CONFIRMED:-}" ] || return 0 | |
| [ -n "${STAFF_PIN:-}" ] || return 0 | |
| curl -sS --max-time 10 -A "$CLIENT_UA" \ | |
| -X POST "$SANDBOX_API/release" \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Pin: $STAFF_PIN" \ | |
| --data "{\"username\":\"$CLAIMED_USER\"}" >/dev/null 2>&1 || true | |
| } | |
| # confirm_sandbox — POST /confirm after successful OAuth + username verify. | |
| confirm_sandbox() { | |
| curl -sS --max-time 10 -A "$CLIENT_UA" \ | |
| -X POST "$SANDBOX_API/confirm" \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Pin: $STAFF_PIN" \ | |
| --data "{\"username\":\"$CLAIMED_USER\"}" >/dev/null 2>&1 \ | |
| && CLAIMED_CONFIRMED=1 | |
| } | |
| # scrub_secrets — wipe PIN from this shell's memory on EXIT. | |
| scrub_secrets() { | |
| unset STAFF_PIN | |
| } | |
| if [ ! -d "$TARGET_PARENT" ]; then | |
| err "Target parent does not exist: $TARGET_PARENT" | |
| { | |
| echo " Set TDX_AV_DIR to an existing directory and re-run, e.g.:" | |
| echo " TDX_AV_DIR=\"\$HOME\" $0" | |
| } 1>&2 | |
| exit 1 | |
| fi | |
| if [ -d "$TARGET_DIR" ]; then | |
| step "Wiping existing $TARGET_DIR for a clean setup" | |
| run_check "rm -rf $TARGET_DIR" rm -rf "$TARGET_DIR" | |
| fi | |
| require_node | |
| # Always release on exit if we never confirmed, then wipe secrets from memory. | |
| trap 'release_sandbox; scrub_secrets' EXIT | |
| step "Enter the 4-digit TDX access PIN" | |
| # -s hides the PIN from the terminal; </dev/tty makes this work under | |
| # `bash <(curl ...)` where stdin would otherwise be the script body. | |
| read -r -s -p "PIN: " STAFF_PIN </dev/tty | |
| echo | |
| if ! printf '%s' "$STAFF_PIN" | grep -Eq '^[0-9]{4}$'; then | |
| err "PIN must be exactly 4 digits." | |
| exit 1 | |
| fi | |
| step "Allocating sandbox..." | |
| claim_sandbox "$STAFF_PIN" | |
| cat <<EOF | |
| ┌──────────────────────────────────────────────────────────────┐ | |
| │ Your sandbox username (use this in the browser login): │ | |
| │ │ | |
| │ Username : $CLAIMED_USER | |
| │ │ | |
| │ You will type your sandbox password directly in the browser │ | |
| │ login screen — this script does not see it. │ | |
| └──────────────────────────────────────────────────────────────┘ | |
| Keep this username visible — you'll need it in the next step. | |
| EOF | |
| step "Removing existing Salesforce auth for alias $ORG_ALIAS (if any)" | |
| sf org logout --target-org "$ORG_ALIAS" --no-prompt >/dev/null 2>&1 || true | |
| step "Creating root directory: $TARGET_DIR" | |
| run_check "mkdir $TARGET_DIR" mkdir -p "$TARGET_DIR" | |
| cd "$TARGET_DIR" | |
| step "Creating standard SFDX project: $STANDARD_PROJ" | |
| run_check "sf template generate project $STANDARD_PROJ" \ | |
| sf template generate project --name "$STANDARD_PROJ" --template standard | |
| step "Creating React (B2C / external) SFDX project: $B2C_PROJ" | |
| run_check "sf template generate project $B2C_PROJ" \ | |
| sf template generate project --name "$B2C_PROJ" --template reactexternalapp | |
| step "Salesforce sandbox login (alias: $ORG_ALIAS)" | |
| echo " A browser will open. Sign in as: $CLAIMED_USER" | |
| read -r -p "Press Enter to launch the browser login..." _ </dev/tty | |
| run_check "sf org login web" \ | |
| sf org login web --alias "$ORG_ALIAS" --instance-url "$LOGIN_URL" --set-default | |
| # Verify the user actually logged in with the issued sandbox username. | |
| # If they typed personal credentials by mistake, abort and let the EXIT trap | |
| # release the row back to the pool. | |
| ACTUAL=$(sf org display --target-org "$ORG_ALIAS" --json 2>/dev/null | json_get_path result.username) \ | |
| || { err "Could not read sf org display output."; exit 1; } | |
| if [ "$ACTUAL" != "$CLAIMED_USER" ]; then | |
| err "Logged-in username ($ACTUAL) does not match the issued sandbox ($CLAIMED_USER)." | |
| err "Releasing the sandbox and aborting. Re-run the script and sign in with $CLAIMED_USER." | |
| exit 1 | |
| fi | |
| confirm_sandbox || { err "Could not confirm sandbox claim with the broker."; exit 1; } | |
| step "Sandbox $CLAIMED_USER is yours." | |
| for proj in "$STANDARD_PROJ" "$B2C_PROJ"; do | |
| step "Configuring $proj (default org + npm install)" | |
| ( | |
| cd "$proj" | |
| run_check "set default org ($proj)" sf config set target-org="$ORG_ALIAS" | |
| echo " Running npm install for $proj — this can take several minutes; you'll see each package as it downloads." | |
| run_check "npm install ($proj)" npm install --loglevel=http --foreground-scripts | |
| ) | |
| done | |
| step "Done. Projects are under $(pwd)" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment