Fix --account N flag and credential priority in R, Raku, Julia, Groovy SDKs

All four implementations had the wrong credential priority order: env vars
were checked before accounts.csv even when an explicit --account N index
was provided.

Correct priority order implemented in all four:
1. Explicit -p/-k flags (function arguments)
2. --account N => accounts.csv row N (bypasses env vars)
3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
5. ./accounts.csv row 0

Changes per file:
- un.r: rewrote get_credentials() with correct priority, added account_index
  param, added --account N to parse_args(), replaced get_api_keys() calls in
  all cmd_* functions with get_credentials(account_index=args$account_index)
- un.raku: rewrote get-credentials() with correct priority, pre-parse
  --account N in MAIN before dispatch, added Int :$account-index param to
  all cmd-* functions and thread account-index through get-credentials calls
- un.jl: added load_accounts_csv() and get_credentials() functions with full
  5-tier priority, added --account to all ArgParse subcommand tables, wired
  account_index through all cmd function get_api_keys calls
- un.groovy: rewrote getCredentials() and getCredentialsStatic() with correct
  priority (added loadAccountsFromCsv/loadCsvAccounts helpers), rewrote
  getApiKeys() to delegate to getCredentials(), added accountIndex field to
  Args class, added --account N to parseArgs(), wired accountIndex through
  all cmdXxx function calls
This commit is contained in:
russell@unturf.com 2026-03-23 15:17:19 -04:00
parent 5373da4108
commit 422985c6db
6 changed files with 5906 additions and 163 deletions

View file

@ -222,42 +222,67 @@ def signRequest(String secretKey, long timestamp, String method, String path, St
* @return Tuple of [publicKey, secretKey] * @return Tuple of [publicKey, secretKey]
* @throws AuthenticationError if no credentials found * @throws AuthenticationError if no credentials found
*/ */
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = 0) { def loadAccountsFromCsv(File path) {
def validAccounts = []
if (!path.exists()) return validAccounts
try {
def lines = path.text.trim().split('\n')
lines.each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore file read errors
}
return validAccounts
}
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = -1) {
// Priority 1: Function arguments // Priority 1: Function arguments
if (publicKey && secretKey) { if (publicKey && secretKey) {
return [publicKey, secretKey] return [publicKey, secretKey]
} }
// Priority 2: Environment variables // Priority 2: --account N => accounts.csv row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadAccountsFromCsv(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY') def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY') def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) { if (envPk && envSk) {
return [envPk, envSk] return [envPk, envSk]
} }
// Priority 3: Config file // Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv') def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
if (accountsPath.exists()) { def searchPaths = [
try { new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
def lines = accountsPath.text.trim().split('\n') new File('accounts.csv')
def validAccounts = [] ]
lines.each { line -> for (path in searchPaths) {
def trimmed = line.trim() def accts = loadAccountsFromCsv(path)
if (!trimmed || trimmed.startsWith('#')) return if (accts && defaultIdx < accts.size()) {
if (trimmed.contains(',')) { return accts[defaultIdx]
def parts = trimmed.split(',', 2)
def pk = parts[0]
def sk = parts[1]
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
if (validAccounts && accountIndex < validAccounts.size()) {
return validAccounts[accountIndex]
}
} catch (Exception e) {
// Ignore file read errors
} }
} }
@ -267,21 +292,14 @@ def getCredentials(String publicKey = null, String secretKey = null, int account
) )
} }
// Legacy compatibility // Legacy compatibility - now delegates to getCredentials for proper priority
def getApiKeys(argsKey) { def getApiKeys(argsKey, int accountIndex = -1) {
def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY') try {
def secretKey = System.getenv('UNSANDBOX_SECRET_KEY') return getCredentials(argsKey ?: null, null, accountIndex)
} catch (AuthenticationError e) {
if (!publicKey || !secretKey) { System.err.println("${RED}Error: ${e.message}${RESET}")
def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY') System.exit(1)
if (!legacyKey) {
System.err.println("${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}")
System.exit(1)
}
return [legacyKey, null]
} }
return [publicKey, secretKey]
} }
// ============================================================================ // ============================================================================
@ -606,7 +624,7 @@ def execute(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials( def (publicKey, secretKey) = getCredentials(
options.publicKey, options.publicKey,
options.secretKey, options.secretKey,
options.accountIndex ?: 0 options.accountIndex != null ? options.accountIndex : -1
) )
def payload = [ def payload = [
@ -656,7 +674,7 @@ def executeAsync(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials( def (publicKey, secretKey) = getCredentials(
options.publicKey, options.publicKey,
options.secretKey, options.secretKey,
options.accountIndex ?: 0 options.accountIndex != null ? options.accountIndex : -1
) )
def payload = [ def payload = [
@ -703,7 +721,7 @@ def run(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials( def (publicKey, secretKey) = getCredentials(
options.publicKey, options.publicKey,
options.secretKey, options.secretKey,
options.accountIndex ?: 0 options.accountIndex != null ? options.accountIndex : -1
) )
def ttl = options.ttl ?: DEFAULT_TTL def ttl = options.ttl ?: DEFAULT_TTL
@ -728,7 +746,7 @@ def runAsync(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials( def (publicKey, secretKey) = getCredentials(
options.publicKey, options.publicKey,
options.secretKey, options.secretKey,
options.accountIndex ?: 0 options.accountIndex != null ? options.accountIndex : -1
) )
def ttl = options.ttl ?: DEFAULT_TTL def ttl = options.ttl ?: DEFAULT_TTL
@ -1589,45 +1607,72 @@ class Client {
def creds = getCredentialsStatic( def creds = getCredentialsStatic(
options.publicKey, options.publicKey,
options.secretKey, options.secretKey,
options.accountIndex ?: 0 options.accountIndex != null ? options.accountIndex : -1
) )
this.publicKey = creds[0] this.publicKey = creds[0]
this.secretKey = creds[1] this.secretKey = creds[1]
} }
private static loadCsvAccounts(File path) {
def accounts = []
if (!path.exists()) return accounts
try {
path.text.trim().split('\n').each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
accounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore
}
return accounts
}
private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) { private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) {
// Priority 1: explicit arguments
if (publicKey && secretKey) { if (publicKey && secretKey) {
return [publicKey, secretKey] return [publicKey, secretKey]
} }
// Priority 2: --account N => CSV row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadCsvAccounts(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY') def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY') def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) { if (envPk && envSk) {
return [envPk, envSk] return [envPk, envSk]
} }
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv') // Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
if (accountsPath.exists()) { def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
try { def searchPaths = [
def lines = accountsPath.text.trim().split('\n') new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
def validAccounts = [] new File('accounts.csv')
lines.each { line -> ]
def trimmed = line.trim() for (path in searchPaths) {
if (!trimmed || trimmed.startsWith('#')) return def accts = loadCsvAccounts(path)
if (trimmed.contains(',')) { if (accts && defaultIdx < accts.size()) {
def parts = trimmed.split(',', 2) return accts[defaultIdx]
def pk = parts[0]
def sk = parts[1]
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
if (validAccounts && accountIndex < validAccounts.size()) {
return validAccounts[accountIndex]
}
} catch (Exception e) {
// Ignore
} }
} }
@ -1738,6 +1783,7 @@ class Args {
String sourceFile = null String sourceFile = null
String inlineLang = null String inlineLang = null
String apiKey = null String apiKey = null
Integer accountIndex = -1
String network = null String network = null
Integer vcpu = 0 Integer vcpu = 0
List<String> env = [] List<String> env = []
@ -1839,7 +1885,7 @@ def serviceEnvSet(serviceId, content, publicKey, secretKey) {
} }
def cmdServiceEnv(args) { def cmdServiceEnv(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
switch (args.envAction) { switch (args.envAction) {
case 'status': case 'status':
@ -1875,7 +1921,7 @@ def cmdServiceEnv(args) {
} }
def cmdExecute(args) { def cmdExecute(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
String code String code
String language String language
@ -1956,7 +2002,7 @@ def cmdExecute(args) {
} }
def cmdSession(args) { def cmdSession(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.sessionSnapshot) { if (args.sessionSnapshot) {
def payload = [:] def payload = [:]
@ -2033,7 +2079,7 @@ def openBrowser(url) {
} }
def cmdSnapshot(args) { def cmdSnapshot(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.snapshotList) { if (args.snapshotList) {
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey) def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
@ -2073,7 +2119,7 @@ def cmdSnapshot(args) {
} }
def cmdImage(args) { def cmdImage(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.imageList) { if (args.imageList) {
def output = apiRequest('/images', 'GET', null, publicKey, secretKey) def output = apiRequest('/images', 'GET', null, publicKey, secretKey)
@ -2153,7 +2199,7 @@ def cmdImage(args) {
} }
def cmdLanguages(args) { def cmdLanguages(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true]) def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true])
def langList = result.languages ?: [] def langList = result.languages ?: []
@ -2168,7 +2214,7 @@ def cmdLanguages(args) {
} }
def cmdKey(args) { def cmdKey(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate", def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
'-H', 'Content-Type: application/json'] '-H', 'Content-Type: application/json']
@ -2240,7 +2286,7 @@ def cmdKey(args) {
} }
def cmdService(args) { def cmdService(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.serviceSnapshot) { if (args.serviceSnapshot) {
def payload = [:] def payload = [:]
@ -2457,6 +2503,9 @@ def parseArgs(argv) {
case '--public-key': case '--public-key':
args.apiKey = argv[++i] // For compatibility args.apiKey = argv[++i] // For compatibility
break break
case '--account':
args.accountIndex = argv[++i].toInteger()
break
case '-n': case '-n':
case '--network': case '--network':
args.network = argv[++i] args.network = argv[++i]

View file

@ -78,28 +78,77 @@ function detect_language(filename::String)::String
return get(EXT_MAP, ext, "unknown") return get(EXT_MAP, ext, "unknown")
end end
function get_api_keys(args_key=nothing)::Tuple{String,String} function load_accounts_csv(path::String)::Vector{Tuple{String,String}}
# Try new-style keys first accounts = Tuple{String,String}[]
public_key = something(args_key, get(ENV, "UNSANDBOX_PUBLIC_KEY", "")) isfile(path) || return accounts
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "") try
for line in eachline(path)
# Fall back to old-style single key for backwards compatibility trimmed = strip(line)
if isempty(public_key) isempty(trimmed) && continue
old_key = get(ENV, "UNSANDBOX_API_KEY", "") startswith(trimmed, "#") && continue
if isempty(old_key) parts = split(trimmed, ","; limit=2)
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)") length(parts) >= 2 || continue
exit(1) pk = strip(parts[1])
sk = strip(parts[2])
if startswith(pk, "unsb-pk-") && startswith(sk, "unsb-sk-")
push!(accounts, (pk, sk))
end
end end
# Old-style: use same key for both public and secret catch
return (old_key, old_key)
end end
return accounts
end
if isempty(secret_key) function get_credentials(; account_index::Int=-1)::Tuple{String,String}
println(stderr, "$(RED)Error: UNSANDBOX_SECRET_KEY not set$(RESET)") # Priority 2: --account N => accounts.csv row N (bypasses env vars)
if account_index >= 0
for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"]
accts = load_accounts_csv(path)
if account_index < length(accts)
return accts[account_index + 1]
end
end
println(stderr, "$(RED)Error: No account at index $account_index in accounts.csv$(RESET)")
exit(1) exit(1)
end end
return (public_key, secret_key) # Priority 3: Environment variables
public_key = get(ENV, "UNSANDBOX_PUBLIC_KEY", "")
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
if !isempty(public_key) && !isempty(secret_key)
return (public_key, secret_key)
end
# Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
default_idx = tryparse(Int, get(ENV, "UNSANDBOX_ACCOUNT", "0"))
default_idx = something(default_idx, 0)
for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"]
accts = load_accounts_csv(path)
if default_idx < length(accts)
return accts[default_idx + 1]
end
end
# Legacy fallback
old_key = get(ENV, "UNSANDBOX_API_KEY", "")
if !isempty(old_key)
return (old_key, old_key)
end
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)")
exit(1)
end
function get_api_keys(args_key=nothing; account_index::Int=-1)::Tuple{String,String}
# Priority 1: explicit -k flag
if args_key !== nothing && !isempty(string(args_key))
public_key = string(args_key)
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
if !isempty(secret_key)
return (public_key, secret_key)
end
end
return get_credentials(account_index=account_index)
end end
function hmac_sha256_hex(key::String, message::String)::String function hmac_sha256_hex(key::String, message::String)::String
@ -356,7 +405,7 @@ function service_env_delete(service_id::String, public_key::String, secret_key::
end end
function cmd_service_env(args) function cmd_service_env(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
action = get(args, "env-action", nothing) action = get(args, "env-action", nothing)
target = get(args, "env-target", nothing) target = get(args, "env-target", nothing)
@ -428,7 +477,7 @@ function cmd_service_env(args)
end end
function cmd_execute(args) function cmd_execute(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
filename = args["source_file"] filename = args["source_file"]
if !isfile(filename) if !isfile(filename)
@ -518,7 +567,7 @@ function cmd_execute(args)
end end
function cmd_session(args) function cmd_session(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
if args["list"] if args["list"]
result = api_request("/sessions", public_key, secret_key) result = api_request("/sessions", public_key, secret_key)
@ -577,7 +626,7 @@ function cmd_session(args)
end end
function cmd_service(args) function cmd_service(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
# Handle env subcommand # Handle env subcommand
if get(args, "env-action", nothing) !== nothing if get(args, "env-action", nothing) !== nothing
@ -914,7 +963,7 @@ function cmd_languages(args)
if langs === nothing if langs === nothing
# Cache miss or expired, fetch from API # Cache miss or expired, fetch from API
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
result = api_request("/languages", public_key, secret_key) result = api_request("/languages", public_key, secret_key)
langs = get(result, "languages", []) langs = get(result, "languages", [])
save_languages_cache(langs) save_languages_cache(langs)
@ -930,7 +979,7 @@ function cmd_languages(args)
end end
function cmd_key(args) function cmd_key(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
# For portal validation, we still use public_key as bearer token # For portal validation, we still use public_key as bearer token
api_key = public_key api_key = public_key
@ -996,6 +1045,9 @@ function main()
required = false required = false
"--api-key", "-k" "--api-key", "-k"
help = "API key (or set UNSANDBOX_API_KEY)" help = "API key (or set UNSANDBOX_API_KEY)"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
"--network", "-n" "--network", "-n"
help = "Network mode" help = "Network mode"
arg_type = String arg_type = String
@ -1057,6 +1109,9 @@ function main()
help = "Comma-separated ports for cloned service" help = "Comma-separated ports for cloned service"
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
@add_arg_table! s["session"] begin @add_arg_table! s["session"] begin
@ -1074,6 +1129,9 @@ function main()
range_tester = x -> x in ["zerotrust", "semitrusted"] range_tester = x -> x in ["zerotrust", "semitrusted"]
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
@add_arg_table! s["service"] begin @add_arg_table! s["service"] begin
@ -1135,6 +1193,9 @@ function main()
help = "Service ID for env commands" help = "Service ID for env commands"
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
"env" "env"
help = "Manage service environment vault" help = "Manage service environment vault"
action = :command action = :command
@ -1155,6 +1216,9 @@ function main()
help = "Load vault variables from file" help = "Load vault variables from file"
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
@add_arg_table! s["key"] begin @add_arg_table! s["key"] begin
@ -1163,6 +1227,9 @@ function main()
action = :store_true action = :store_true
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
@add_arg_table! s["languages"] begin @add_arg_table! s["languages"] begin
@ -1171,6 +1238,9 @@ function main()
action = :store_true action = :store_true
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
@add_arg_table! s["image"] begin @add_arg_table! s["image"] begin
@ -1203,6 +1273,9 @@ function main()
help = "Comma-separated ports for spawned service" help = "Comma-separated ports for spawned service"
"--api-key", "-k" "--api-key", "-k"
help = "API key" help = "API key"
"--account"
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
arg_type = Int
end end
args = parse_args(ARGS, s) args = parse_args(ARGS, s)
@ -1220,6 +1293,7 @@ function main()
service_args["vault-env"] = get(env_args, "vault-env", nothing) service_args["vault-env"] = get(env_args, "vault-env", nothing)
service_args["env-file"] = get(env_args, "env-file", nothing) service_args["env-file"] = get(env_args, "env-file", nothing)
service_args["api-key"] = get(env_args, "api-key", nothing) service_args["api-key"] = get(env_args, "api-key", nothing)
service_args["account"] = get(env_args, "account", nothing)
end end
cmd_service(service_args) cmd_service(service_args)
elseif args["%COMMAND%"] == "languages" elseif args["%COMMAND%"] == "languages"
@ -1239,7 +1313,7 @@ function main()
end end
function cmd_image(args) function cmd_image(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
if args["list"] if args["list"]
result = api_request("/images", public_key, secret_key) result = api_request("/images", public_key, secret_key)
@ -1330,7 +1404,7 @@ function cmd_image(args)
end end
function cmd_snapshot(args) function cmd_snapshot(args)
(public_key, secret_key) = get_api_keys(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
if args["list"] if args["list"]
result = api_request("/snapshots", public_key, secret_key) result = api_request("/snapshots", public_key, secret_key)

View file

@ -121,29 +121,64 @@ MAX_ENV_CONTENT_SIZE <- 65536
#' creds <- get_credentials() #' creds <- get_credentials()
#' creds <- get_credentials(public_key = "unsb-pk-xxxx", secret_key = "unsb-sk-xxxx") #' creds <- get_credentials(public_key = "unsb-pk-xxxx", secret_key = "unsb-sk-xxxx")
#' } #' }
get_credentials <- function(public_key = NULL, secret_key = NULL) { get_credentials <- function(public_key = NULL, secret_key = NULL, account_index = -1) {
# Priority 1: Function arguments # Priority 1: Function arguments
if (!is.null(public_key) && !is.null(secret_key)) { if (!is.null(public_key) && !is.null(secret_key)) {
return(list(public_key = public_key, secret_key = secret_key)) return(list(public_key = public_key, secret_key = secret_key))
} }
# Priority 2: Environment variables # Priority 2: --account N => accounts.csv row N (bypasses env vars)
load_account_from_csv <- function(idx) {
search_paths <- c(
file.path(Sys.getenv("HOME"), ".unsandbox", "accounts.csv"),
"accounts.csv"
)
for (accounts_file in search_paths) {
if (file.exists(accounts_file)) {
lines <- readLines(accounts_file, warn = FALSE)
valid <- list()
for (line in lines) {
trimmed <- trimws(line)
if (nchar(trimmed) == 0 || startsWith(trimmed, "#")) next
parts <- strsplit(trimmed, ",")[[1]]
if (length(parts) >= 2 &&
startsWith(parts[1], "unsb-pk-") &&
startsWith(parts[2], "unsb-sk-")) {
valid <- c(valid, list(list(public_key = parts[1], secret_key = parts[2])))
}
}
if (length(valid) > idx) {
return(valid[[idx + 1]])
}
}
}
return(NULL)
}
if (account_index >= 0) {
result <- load_account_from_csv(account_index)
if (!is.null(result)) {
return(result)
}
stop(sprintf("No account at index %d in accounts.csv", account_index))
}
# Priority 3: Environment variables
env_public <- Sys.getenv("UNSANDBOX_PUBLIC_KEY") env_public <- Sys.getenv("UNSANDBOX_PUBLIC_KEY")
env_secret <- Sys.getenv("UNSANDBOX_SECRET_KEY") env_secret <- Sys.getenv("UNSANDBOX_SECRET_KEY")
if (env_public != "" && env_secret != "") { if (env_public != "" && env_secret != "") {
return(list(public_key = env_public, secret_key = env_secret)) return(list(public_key = env_public, secret_key = env_secret))
} }
# Priority 3: Accounts file # Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
accounts_file <- file.path(Sys.getenv("HOME"), ".unsandbox", "accounts.csv") default_idx <- 0
if (file.exists(accounts_file)) { env_account <- Sys.getenv("UNSANDBOX_ACCOUNT")
lines <- readLines(accounts_file, warn = FALSE) if (env_account != "") {
for (line in lines) { default_idx <- as.integer(env_account)
parts <- strsplit(trimws(line), ",")[[1]] }
if (length(parts) >= 2) { result <- load_account_from_csv(default_idx)
return(list(public_key = parts[1], secret_key = parts[2])) if (!is.null(result)) {
} return(result)
}
} }
# Fallback to legacy UNSANDBOX_API_KEY # Fallback to legacy UNSANDBOX_API_KEY
@ -1665,7 +1700,7 @@ build_env_content <- function(envs, env_file) {
} }
cmd_service_env <- function(args) { cmd_service_env <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -1743,7 +1778,7 @@ cmd_service_env <- function(args) {
} }
cmd_execute <- function(args) { cmd_execute <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -1837,7 +1872,7 @@ cmd_execute <- function(args) {
} }
cmd_session <- function(args) { cmd_session <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -1995,7 +2030,7 @@ cmd_languages <- function(args) {
if (is.null(langs)) { if (is.null(langs)) {
# Cache miss or expired, fetch from API # Cache miss or expired, fetch from API
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -2016,7 +2051,7 @@ cmd_languages <- function(args) {
} }
cmd_key <- function(args) { cmd_key <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -2115,7 +2150,7 @@ cmd_key <- function(args) {
} }
cmd_snapshot <- function(args) { cmd_snapshot <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -2195,7 +2230,7 @@ cmd_snapshot <- function(args) {
} }
cmd_image <- function(args) { cmd_image <- function(args) {
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -2308,7 +2343,7 @@ cmd_service <- function(args) {
return() return()
} }
keys <- get_api_keys(args$api_key) keys <- get_credentials(account_index = if (!is.null(args$account_index)) args$account_index else -1)
public_key <- keys$public_key public_key <- keys$public_key
secret_key <- keys$secret_key secret_key <- keys$secret_key
@ -2532,6 +2567,7 @@ parse_args <- function() {
result <- list( result <- list(
source_file = NULL, source_file = NULL,
api_key = NULL, api_key = NULL,
account_index = -1L,
network = NULL, network = NULL,
env = NULL, env = NULL,
files = NULL, files = NULL,
@ -2628,6 +2664,10 @@ parse_args <- function() {
i <- i + 1 i <- i + 1
result$api_key <- args[i] result$api_key <- args[i]
i <- i + 1 i <- i + 1
} else if (arg == "--account") {
i <- i + 1
result$account_index <- as.integer(args[i])
i <- i + 1
} else if (arg %in% c("-n", "--network")) { } else if (arg %in% c("-n", "--network")) {
i <- i + 1 i <- i + 1
result$network <- args[i] result$network <- args[i]

View file

@ -143,38 +143,59 @@ sub sign-request(Str $secret-key, Int $timestamp, Str $method, Str $path, Str $b
#| 1. Function arguments #| 1. Function arguments
#| 2. Environment variables #| 2. Environment variables
#| 3. ~/.unsandbox/accounts.csv #| 3. ~/.unsandbox/accounts.csv
sub get-credentials(Str :$public-key, Str :$secret-key, Int :$account-index = 0) returns List is export { sub get-credentials(Str :$public-key, Str :$secret-key, Int :$account-index = -1) returns List is export {
# Priority 1: Function arguments # Priority 1: Function arguments
if $public-key && $secret-key { if $public-key && $secret-key {
return ($public-key, $secret-key); return ($public-key, $secret-key);
} }
# Priority 2: Environment variables # Helper: load valid accounts from a CSV path
sub load-accounts-from(IO::Path $path) {
my @valid;
if $path.e {
try {
my @lines = $path.slurp.trim.split("\n");
for @lines -> $line {
my $trimmed = $line.trim;
next if !$trimmed || $trimmed.starts-with('#');
if $trimmed.contains(',') {
my ($pk, $sk) = $trimmed.split(',', 2);
if $pk.starts-with('unsb-pk-') && $sk.starts-with('unsb-sk-') {
@valid.push(($pk, $sk));
}
}
}
}
}
return @valid;
}
# Priority 2: --account N => accounts.csv row N (bypasses env vars)
if $account-index >= 0 {
for ($*HOME.add('.unsandbox').add('accounts.csv'),
'accounts.csv'.IO) -> $path {
my @accts = load-accounts-from($path);
if @accts && $account-index < @accts.elems {
return @accts[$account-index];
}
}
die AuthenticationError.new("No account at index $account-index in accounts.csv");
}
# Priority 3: Environment variables
my $env-pk = %*ENV<UNSANDBOX_PUBLIC_KEY> // ''; my $env-pk = %*ENV<UNSANDBOX_PUBLIC_KEY> // '';
my $env-sk = %*ENV<UNSANDBOX_SECRET_KEY> // ''; my $env-sk = %*ENV<UNSANDBOX_SECRET_KEY> // '';
if $env-pk && $env-sk { if $env-pk && $env-sk {
return ($env-pk, $env-sk); return ($env-pk, $env-sk);
} }
# Priority 3: Config file # Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
my $accounts-path = $*HOME.add('.unsandbox').add('accounts.csv'); my $default-idx = (%*ENV<UNSANDBOX_ACCOUNT> // '0').Int;
if $accounts-path.e { for ($*HOME.add('.unsandbox').add('accounts.csv'),
try { 'accounts.csv'.IO) -> $path {
my @lines = $accounts-path.slurp.trim.split("\n"); my @accts = load-accounts-from($path);
my @valid-accounts; if @accts && $default-idx < @accts.elems {
for @lines -> $line { return @accts[$default-idx];
my $trimmed = $line.trim;
next if !$trimmed || $trimmed.starts-with('#');
if $trimmed.contains(',') {
my ($pk, $sk) = $trimmed.split(',', 2);
if $pk.starts-with('unsb-pk-') && $sk.starts-with('unsb-sk-') {
@valid-accounts.push(($pk, $sk));
}
}
}
if @valid-accounts && $account-index < @valid-accounts.elems {
return @valid-accounts[$account-index];
}
} }
} }
@ -1366,8 +1387,8 @@ sub uri-encode(Str $s) {
return $s.subst(/<-[A-Za-z0-9\-_.~]>/, { .encode.list.map({ '%' ~ .fmt('%02X') }).join }, :g); return $s.subst(/<-[A-Za-z0-9\-_.~]>/, { .encode.list.map({ '%' ~ .fmt('%02X') }).join }, :g);
} }
sub cmd-execute(@args) { sub cmd-execute(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $source-file = ''; my $source-file = '';
my %env-vars; my %env-vars;
my @input-files; my @input-files;
@ -1490,8 +1511,8 @@ sub cmd-execute(@args) {
exit %result<exit_code> // 0; exit %result<exit_code> // 0;
} }
sub cmd-session(@args) { sub cmd-session(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $list-mode = False; my $list-mode = False;
my $kill-id = ''; my $kill-id = '';
my $shell = ''; my $shell = '';
@ -1579,8 +1600,8 @@ sub cmd-session(@args) {
say "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}"; say "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}";
} }
sub cmd-service(@args) { sub cmd-service(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $list-mode = False; my $list-mode = False;
my $info-id = ''; my $info-id = '';
my $logs-id = ''; my $logs-id = '';
@ -1813,8 +1834,8 @@ sub cmd-service(@args) {
exit 1; exit 1;
} }
sub cmd-languages(@args) { sub cmd-languages(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $json-output = False; my $json-output = False;
for @args -> $arg { for @args -> $arg {
@ -1837,8 +1858,8 @@ sub cmd-languages(@args) {
} }
} }
sub cmd-key(@args) { sub cmd-key(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $extend = False; my $extend = False;
for @args -> $arg { for @args -> $arg {
@ -1897,8 +1918,8 @@ sub cmd-key(@args) {
say "Concurrency: {%result<concurrency> // 'N/A'}"; say "Concurrency: {%result<concurrency> // 'N/A'}";
} }
sub cmd-image(@args) { sub cmd-image(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $list-mode = False; my $list-mode = False;
my $info-id = ''; my $info-id = '';
my $delete-id = ''; my $delete-id = '';
@ -2090,8 +2111,8 @@ sub cmd-image(@args) {
exit 1; exit 1;
} }
sub cmd-snapshot(@args) { sub cmd-snapshot(@args, Int :$account-index = -1) {
my ($public-key, $secret-key) = get-credentials(); my ($public-key, $secret-key) = get-credentials(:$account-index);
my $list-mode = False; my $list-mode = False;
my $info-id = ''; my $info-id = '';
my $delete-id = ''; my $delete-id = '';
@ -2224,7 +2245,19 @@ sub cmd-snapshot(@args) {
exit 1; exit 1;
} }
sub MAIN(*@args) is export { sub MAIN(*@args is copy) is export {
# Pre-parse --account N (global credential flag) before dispatching
my $account-index = -1;
my $i = 0;
while $i < @args.elems {
if @args[$i] eq '--account' && $i + 1 < @args.elems {
$account-index = @args[$i + 1].Int;
@args.splice($i, 2);
} else {
$i++;
}
}
unless @args { unless @args {
note "Usage: un.raku [options] <source_file>"; note "Usage: un.raku [options] <source_file>";
note " un.raku session [options]"; note " un.raku session [options]";
@ -2275,25 +2308,25 @@ sub MAIN(*@args) is export {
given @args[0] { given @args[0] {
when 'session' { when 'session' {
cmd-session(@args[1..*]); cmd-session(@args[1..*], :$account-index);
} }
when 'service' { when 'service' {
cmd-service(@args[1..*]); cmd-service(@args[1..*], :$account-index);
} }
when 'snapshot' { when 'snapshot' {
cmd-snapshot(@args[1..*]); cmd-snapshot(@args[1..*], :$account-index);
} }
when 'image' { when 'image' {
cmd-image(@args[1..*]); cmd-image(@args[1..*], :$account-index);
} }
when 'key' { when 'key' {
cmd-key(@args[1..*]); cmd-key(@args[1..*], :$account-index);
} }
when 'languages' { when 'languages' {
cmd-languages(@args[1..*]); cmd-languages(@args[1..*], :$account-index);
} }
default { default {
cmd-execute(@args); cmd-execute(@args, :$account-index);
} }
} }
} }

View file

@ -1 +0,0 @@
clients/groovy/sync/src/un.groovy

2806
un.groovy Normal file

File diff suppressed because it is too large Load diff

1
un.jl
View file

@ -1 +0,0 @@
clients/julia/sync/src/un.jl

2743
un.jl Executable file

File diff suppressed because it is too large Load diff