Fix --account N credential priority across all SDKs

--account N was silently ignored when UNSANDBOX_PUBLIC_KEY/SECRET_KEY
env vars were set. The credential resolution checked env vars at tier 2
before ever reaching the CSV lookup, so the explicit flag had no effect.

Correct priority order (all 8 SDKs):
  1. CLI -p/-k flags (explicit key args)
  2. --account N → direct CSV row lookup (bypasses env vars)
  3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
  4. ~/.unsandbox/accounts.csv default (row 0 or UNSANDBOX_ACCOUNT)

SDKs updated: C, Python, Go, JavaScript, Ruby, PHP, Java, Rust

Also adds:
- --account N flag to CLI parsers in all 8 SDKs (Go, PHP, Java, Rust
  previously had no flag at all; Python/JS/Ruby had the parameter but
  never wired it to the CLI)
- test_account_flag.sh integration test for each SDK verifying the
  priority behavior with real and garbage credentials
- test-integration Makefile target for the C SDK
This commit is contained in:
russell@unturf.com 2026-03-23 14:56:00 -04:00
parent 8093386568
commit ed6c52b001
20 changed files with 1243 additions and 174 deletions

View file

@ -331,10 +331,11 @@ function loadCredentialsFromStorage() {
*
* Priority:
* 1. Function arguments
* 2. Environment variables (Node.js)
* 3. localStorage (Browser)
* 4. ~/.unsandbox/accounts.csv (Node.js)
* 5. ./accounts.csv (Node.js)
* 2. accountIndex >= 0 → load from accounts.csv row N
* 3. Environment variables (Node.js)
* 4. localStorage (Browser)
* 5. ~/.unsandbox/accounts.csv (default row, Node.js)
* 6. ./accounts.csv (default row, Node.js)
*/
function resolveCredentials(publicKey, secretKey, accountIndex) {
// Tier 1: Function arguments
@ -342,7 +343,26 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
return [publicKey, secretKey];
}
// Tier 2: Environment variables (Node.js only)
// Tier 2: Explicit accountIndex → load from accounts.csv row N (Node.js only)
if (IS_NODE && fs && path && accountIndex !== undefined && accountIndex >= 0) {
// ~/.unsandbox/accounts.csv first
try {
const unsandboxDir = getUnsandboxDir();
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
if (creds) {
return creds;
}
} catch (e) {
// Continue to next location
}
// ./accounts.csv fallback
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
if (creds) {
return creds;
}
}
// Tier 3: Environment variables (Node.js only)
if (IS_NODE) {
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
const envSk = process.env.UNSANDBOX_SECRET_KEY;
@ -351,7 +371,7 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
}
}
// Tier 3: localStorage (Browser only)
// Tier 4: localStorage (Browser only)
if (IS_BROWSER) {
const storageCreds = loadCredentialsFromStorage();
if (storageCreds) {
@ -359,17 +379,14 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
}
}
// Tier 4 & 5: File-based credentials (Node.js only)
// Tier 5 & 6: File-based credentials with default index (Node.js only)
if (IS_NODE && fs && path) {
// Determine account index
if (accountIndex === undefined) {
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
}
const defaultIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
// Tier 4: ~/.unsandbox/accounts.csv
// Tier 5: ~/.unsandbox/accounts.csv
try {
const unsandboxDir = getUnsandboxDir();
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), defaultIndex);
if (creds) {
return creds;
}
@ -377,8 +394,8 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
// Continue to next tier
}
// Tier 5: ./accounts.csv
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
// Tier 6: ./accounts.csv
const creds = loadCredentialsFromCsv('accounts.csv', defaultIndex);
if (creds) {
return creds;
}
@ -2193,6 +2210,7 @@ function parseArgs(args) {
output: null,
publicKey: null,
secretKey: null,
accountIndex: undefined,
network: 'zerotrust',
vcpu: 1,
yes: false,
@ -2322,6 +2340,9 @@ function parseArgs(args) {
} else if (arg === '-k' || arg === '--secret-key') {
result.secretKey = args[++i];
i++;
} else if (arg === '--account') {
result.accountIndex = parseInt(args[++i], 10);
i++;
} else if (arg === '-n' || arg === '--network') {
result.network = args[++i];
i++;
@ -2509,8 +2530,7 @@ function formatTable(items, columns) {
* Handle session commands.
*/
async function handleSession(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
// List sessions
if (opts.list) {
@ -2593,8 +2613,7 @@ async function handleSession(opts) {
* Handle service commands.
*/
async function handleService(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
// Handle env subcommand
if (opts.subcommand === 'env') {
@ -2829,8 +2848,7 @@ async function handleService(opts) {
* Handle snapshot commands.
*/
async function handleSnapshot(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
// List snapshots
if (opts.list) {
@ -2898,8 +2916,7 @@ async function handleSnapshot(opts) {
* Handle image command.
*/
async function handleImage(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
// List images
if (opts.list) {
@ -3002,13 +3019,13 @@ async function handleImage(opts) {
* Handle key command.
*/
async function handleKey(opts) {
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
try {
const result = await validateKeys(opts.publicKey, opts.secretKey);
const result = await validateKeys(pk, sk);
console.log('API Key Status:');
console.log(JSON.stringify(result, null, 2));
} catch (err) {
// If validate endpoint doesn't exist, just show that credentials were resolved
const [pk] = resolveCredentials(opts.publicKey, opts.secretKey);
console.log(`Public Key: ${pk}`);
console.log('Key validation endpoint returned error - key may still be valid.');
}
@ -3018,7 +3035,8 @@ async function handleKey(opts) {
* Handle languages command.
*/
async function handleLanguages(opts) {
const languages = await getLanguages(opts.publicKey, opts.secretKey);
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
const languages = await getLanguages(pk, sk);
if (opts.json) {
// Output as JSON array
@ -3035,8 +3053,7 @@ async function handleLanguages(opts) {
* Handle execute command (default).
*/
async function handleExecute(opts) {
const pk = opts.publicKey;
const sk = opts.secretKey;
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
let code;
let language;

View file

@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Integration test for --account N credential selection in un.js.
#
# Tests that --account N selects the correct row from accounts.csv,
# taking priority over environment variables.
#
# Requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY to be set.
# Skips if credentials are not available.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UN_JS="$SCRIPT_DIR/../src/un.js"
PASS=0
FAIL=0
SKIP=0
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
skip() { echo "SKIP: $1"; SKIP=$((SKIP + 1)); }
# Require real credentials from environment
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set"
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
exit 0
fi
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
REAL_SK="$UNSANDBOX_SECRET_KEY"
GARBAGE_PK="unsb-pk-0000-0000-0000-garbage"
GARBAGE_SK="unsb-sk-00000-00000-00000-garbage"
# Build a temporary HOME with accounts.csv: row 0 = garbage, row 1 = real creds
TMPHOME="$(mktemp -d)"
mkdir -p "$TMPHOME/.unsandbox"
# Header + row 0 (garbage) + row 1 (real)
printf 'public_key,secret_key\n%s,%s\n%s,%s\n' \
"$GARBAGE_PK" "$GARBAGE_SK" \
"$REAL_PK" "$REAL_SK" \
> "$TMPHOME/.unsandbox/accounts.csv"
cleanup() {
rm -rf "$TMPHOME"
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# Test 1: HOME=TMPHOME, env vars = garbage, --account 1 => row 1 = real creds
# Expect: key command succeeds (no auth error)
# ---------------------------------------------------------------------------
if HOME="$TMPHOME" \
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
node "$UN_JS" --account 1 key 2>&1 | grep -qiE 'Public Key|key_id|account|status'; then
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
else
# Also accept a successful JSON response (key validates)
output="$(HOME="$TMPHOME" \
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
node "$UN_JS" --account 1 key 2>&1 || true)"
if echo "$output" | grep -qiE '401|invalid|unauthorized|authentication'; then
fail "Test 1: --account 1 selected row 1 but auth failed (real creds may be invalid)"
else
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
fi
fi
# ---------------------------------------------------------------------------
# Test 2: HOME=TMPHOME, env vars = real, --account 0 => row 0 = garbage creds
# Expect: 401 / auth error (garbage creds used despite real env vars)
# ---------------------------------------------------------------------------
output2="$(HOME="$TMPHOME" \
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
UNSANDBOX_SECRET_KEY="$REAL_SK" \
node "$UN_JS" --account 0 key 2>&1 || true)"
if echo "$output2" | grep -qiE '401|invalid|unauthorized|authentication|error'; then
pass "Test 2: --account 0 selects row 0 (garbage) over env real creds (expected auth failure)"
else
fail "Test 2: --account 0 should use garbage creds and fail auth, but got: $output2"
fi
# ---------------------------------------------------------------------------
# Test 3: HOME=TMPHOME, env vars = real, no --account => env vars win (real creds)
# Expect: key command succeeds
# ---------------------------------------------------------------------------
output3="$(HOME="$TMPHOME" \
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
UNSANDBOX_SECRET_KEY="$REAL_SK" \
node "$UN_JS" key 2>&1 || true)"
if echo "$output3" | grep -qiE '401|invalid|unauthorized|authentication failed'; then
fail "Test 3: without --account, env real creds should succeed but got auth error: $output3"
else
pass "Test 3: without --account, env vars (real creds) are used"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0