Add --account N flag and full credential resolution to TypeScript, Nim, Dart, Crystal SDKs
All four implementations now support: - loadAccountsCSV/loadCredentialsFromCsv helper to parse accounts.csv files - 5-tier credential resolution: -p/-k flags > --account N > env vars > ~/.unsandbox/accounts.csv > ./accounts.csv (with UNSANDBOX_ACCOUNT env var support) - --account N CLI flag to select a specific row from accounts.csv (0-based) - -p PUBLIC_KEY flag for explicit public key (separate from -k secret key)
This commit is contained in:
parent
59eb474995
commit
6860871128
4 changed files with 336 additions and 69 deletions
|
|
@ -132,21 +132,72 @@ def save_languages_cache(response : JSON::Any)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_api_keys(args_key : String?) : {String, String?}
|
def load_accounts_csv(path : String, index : Int32) : {String, String}?
|
||||||
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
return nil unless File.exists?(path)
|
||||||
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
|
begin
|
||||||
|
lines = File.read(path).split('\n').select do |l|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
t = l.strip
|
||||||
if public_key.nil? || public_key.empty? || secret_key.nil? || secret_key.empty?
|
!t.empty? && !t.starts_with?('#')
|
||||||
legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]?
|
|
||||||
if legacy_key.nil? || legacy_key.empty?
|
|
||||||
STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
|
||||||
exit 1
|
|
||||||
end
|
end
|
||||||
|
return nil if index < 0 || index >= lines.size
|
||||||
|
parts = lines[index].split(',')
|
||||||
|
return nil if parts.size < 2
|
||||||
|
pk = parts[0].strip
|
||||||
|
sk = parts[1].strip
|
||||||
|
return nil if pk.empty? || sk.empty?
|
||||||
|
{pk, sk}
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_api_keys(args_key : String?, args_public_key : String? = nil, account : Int32? = nil) : {String, String?}
|
||||||
|
# Tier 1: explicit -p/-k flags
|
||||||
|
if args_public_key && !args_public_key.empty? && args_key && !args_key.empty?
|
||||||
|
return {args_public_key, args_key}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tier 2: --account N → accounts.csv row N (bypasses env vars)
|
||||||
|
if !account.nil?
|
||||||
|
idx = account.not_nil!
|
||||||
|
home = ENV["HOME"]?
|
||||||
|
if home && !home.empty?
|
||||||
|
result = load_accounts_csv(File.join(home, ".unsandbox", "accounts.csv"), idx)
|
||||||
|
return {result[0], result[1]} if result
|
||||||
|
end
|
||||||
|
result = load_accounts_csv("accounts.csv", idx)
|
||||||
|
return {result[0], result[1]} if result
|
||||||
|
STDERR.puts "#{RED}Error: --account #{idx} not found in accounts.csv#{RESET}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tier 3: env vars
|
||||||
|
env_pk = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
||||||
|
env_sk = ENV["UNSANDBOX_SECRET_KEY"]?
|
||||||
|
if env_pk && !env_pk.empty? && env_sk && !env_sk.empty?
|
||||||
|
return {env_pk, env_sk}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
|
||||||
|
home = ENV["HOME"]?
|
||||||
|
def_index = (ENV["UNSANDBOX_ACCOUNT"]?.try(&.to_i?) || 0).to_i32
|
||||||
|
if home && !home.empty?
|
||||||
|
result = load_accounts_csv(File.join(home, ".unsandbox", "accounts.csv"), def_index)
|
||||||
|
return {result[0], result[1]} if result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tier 5: ./accounts.csv row 0
|
||||||
|
result = load_accounts_csv("accounts.csv", def_index)
|
||||||
|
return {result[0], result[1]} if result
|
||||||
|
|
||||||
|
# Legacy UNSANDBOX_API_KEY fallback
|
||||||
|
legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]?
|
||||||
|
if legacy_key && !legacy_key.empty?
|
||||||
return {legacy_key, nil}
|
return {legacy_key, nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
{public_key, secret_key}
|
STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
||||||
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_challenge_id(response_body : String) : String?
|
def extract_challenge_id(response_body : String) : String?
|
||||||
|
|
@ -394,7 +445,7 @@ def build_env_content(envs : Array(String), env_file : String?) : String
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_service_env(args)
|
def cmd_service_env(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String))
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
action = args[:env_action]?.as?(String) || ""
|
action = args[:env_action]?.as?(String) || ""
|
||||||
target = args[:env_target]?.as?(String) || ""
|
target = args[:env_target]?.as?(String) || ""
|
||||||
|
|
@ -463,7 +514,7 @@ def cmd_service_env(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_execute(args)
|
def cmd_execute(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
filename = args[:source_file].as(String)
|
filename = args[:source_file].as(String)
|
||||||
unless File.exists?(filename)
|
unless File.exists?(filename)
|
||||||
|
|
@ -553,7 +604,7 @@ def cmd_execute(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_session(args)
|
def cmd_session(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/sessions", public_key, secret_key)
|
result = api_request("/sessions", public_key, secret_key)
|
||||||
|
|
@ -661,7 +712,7 @@ def cmd_session(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_languages(args)
|
def cmd_languages(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
# Try to load from cache first
|
# Try to load from cache first
|
||||||
cached_response = load_languages_cache
|
cached_response = load_languages_cache
|
||||||
|
|
@ -694,7 +745,7 @@ def cmd_languages(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_key(args)
|
def cmd_key(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
# Validate key
|
# Validate key
|
||||||
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
||||||
|
|
@ -788,7 +839,7 @@ def cmd_key(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_image(args)
|
def cmd_image(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/images", public_key, secret_key)
|
result = api_request("/images", public_key, secret_key)
|
||||||
|
|
@ -949,7 +1000,7 @@ def cmd_image(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_snapshot(args)
|
def cmd_snapshot(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/snapshots", public_key, secret_key)
|
result = api_request("/snapshots", public_key, secret_key)
|
||||||
|
|
@ -1072,7 +1123,7 @@ def cmd_snapshot(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_logs(args)
|
def cmd_logs(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
source = args[:logs_source]?.as?(String) || "all"
|
source = args[:logs_source]?.as?(String) || "all"
|
||||||
lines = args[:logs_lines]?.as?(Int32) || 100
|
lines = args[:logs_lines]?.as?(Int32) || 100
|
||||||
|
|
@ -1187,7 +1238,7 @@ def cmd_service(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/services", public_key, secret_key)
|
result = api_request("/services", public_key, secret_key)
|
||||||
|
|
@ -1441,6 +1492,8 @@ def main
|
||||||
args = {
|
args = {
|
||||||
source_file: nil,
|
source_file: nil,
|
||||||
api_key: nil,
|
api_key: nil,
|
||||||
|
public_key: nil,
|
||||||
|
account: nil,
|
||||||
network: nil,
|
network: nil,
|
||||||
env: [] of String,
|
env: [] of String,
|
||||||
files: [] of String,
|
files: [] of String,
|
||||||
|
|
@ -1529,7 +1582,9 @@ def main
|
||||||
parser = OptionParser.new do |opts|
|
parser = OptionParser.new do |opts|
|
||||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr snapshot [options]\n un.cr image [options]\n un.cr logs [options]\n un.cr key [options]\n un.cr health\n un.cr version\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr snapshot [options]\n un.cr image [options]\n un.cr logs [options]\n un.cr key [options]\n un.cr health\n un.cr version\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
||||||
|
|
||||||
opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k }
|
opts.on("-k API_KEY", "--api-key=API_KEY", "Secret/API key") { |k| args[:api_key] = k }
|
||||||
|
opts.on("-p PUBLIC_KEY", "--public-key=PUBLIC_KEY", "Public key (use with -k for secret key)") { |k| args[:public_key] = k }
|
||||||
|
opts.on("--account=N", "Use row N from accounts.csv (0-based)") { |n| args[:account] = n.to_i32 }
|
||||||
opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
||||||
opts.on("-e ENV", "--env=ENV", "Environment variable (KEY=VALUE)") { |e|
|
opts.on("-e ENV", "--env=ENV", "Environment variable (KEY=VALUE)") { |e|
|
||||||
args[:env].as(Array(String)) << e
|
args[:env].as(Array(String)) << e
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ class Args {
|
||||||
String? command;
|
String? command;
|
||||||
String? sourceFile;
|
String? sourceFile;
|
||||||
String? apiKey;
|
String? apiKey;
|
||||||
|
String? publicKey;
|
||||||
|
int? account;
|
||||||
String? network;
|
String? network;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
List<String> env = [];
|
List<String> env = [];
|
||||||
|
|
@ -141,21 +143,73 @@ class Args {
|
||||||
bool snapshotHot = false;
|
bool snapshotHot = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String?> getApiKeys(String? argsKey) {
|
Map<String, String>? loadAccountsCSV(String path, int index) {
|
||||||
final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
|
try {
|
||||||
final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY'];
|
final file = File(path);
|
||||||
|
if (!file.existsSync()) return null;
|
||||||
|
final lines = file.readAsLinesSync().where((l) {
|
||||||
|
final t = l.trim();
|
||||||
|
return t.isNotEmpty && !t.startsWith('#');
|
||||||
|
}).toList();
|
||||||
|
if (index < 0 || index >= lines.length) return null;
|
||||||
|
final parts = lines[index].split(',');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
final pk = parts[0].trim();
|
||||||
|
final sk = parts[1].trim();
|
||||||
|
if (pk.isEmpty || sk.isEmpty) return null;
|
||||||
|
return {'pk': pk, 'sk': sk};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
List<String?> getApiKeys(String? argsKey, {String? argsPublicKey, int? account}) {
|
||||||
if (publicKey == null || publicKey.isEmpty || secretKey == null || secretKey.isEmpty) {
|
// Tier 1: explicit -p/-k flags
|
||||||
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
|
if (argsPublicKey != null && argsPublicKey.isNotEmpty && argsKey != null && argsKey.isNotEmpty) {
|
||||||
if (legacyKey == null || legacyKey.isEmpty) {
|
return [argsPublicKey, argsKey];
|
||||||
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
|
}
|
||||||
exit(1);
|
|
||||||
|
// Tier 2: --account N → accounts.csv row N (bypasses env vars)
|
||||||
|
if (account != null) {
|
||||||
|
final home = Platform.environment['HOME'] ?? '';
|
||||||
|
if (home.isNotEmpty) {
|
||||||
|
final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', account);
|
||||||
|
if (fromHome != null) return [fromHome['pk'], fromHome['sk']];
|
||||||
}
|
}
|
||||||
|
final fromLocal = loadAccountsCSV('./accounts.csv', account);
|
||||||
|
if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']];
|
||||||
|
stderr.writeln('${red}Error: --account $account not found in accounts.csv$reset');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: env vars
|
||||||
|
final envPk = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
|
||||||
|
final envSk = Platform.environment['UNSANDBOX_SECRET_KEY'];
|
||||||
|
if (envPk != null && envPk.isNotEmpty && envSk != null && envSk.isNotEmpty) {
|
||||||
|
return [envPk, envSk];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
|
||||||
|
final home = Platform.environment['HOME'] ?? '';
|
||||||
|
final defIndexStr = Platform.environment['UNSANDBOX_ACCOUNT'] ?? '0';
|
||||||
|
final defIndex = int.tryParse(defIndexStr) ?? 0;
|
||||||
|
if (home.isNotEmpty) {
|
||||||
|
final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', defIndex);
|
||||||
|
if (fromHome != null) return [fromHome['pk'], fromHome['sk']];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 5: ./accounts.csv row 0
|
||||||
|
final fromLocal = loadAccountsCSV('./accounts.csv', defIndex);
|
||||||
|
if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']];
|
||||||
|
|
||||||
|
// Legacy UNSANDBOX_API_KEY fallback
|
||||||
|
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
|
||||||
|
if (legacyKey != null && legacyKey.isNotEmpty) {
|
||||||
return [legacyKey, null];
|
return [legacyKey, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [publicKey, secretKey];
|
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String detectLanguage(String filename) {
|
String detectLanguage(String filename) {
|
||||||
|
|
@ -509,7 +563,7 @@ Future<bool> serviceEnvDelete(String serviceId, String publicKey, String? secret
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdServiceEnv(Args args) async {
|
Future<void> cmdServiceEnv(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
final action = args.envAction;
|
final action = args.envAction;
|
||||||
|
|
@ -579,7 +633,7 @@ Future<void> cmdServiceEnv(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdExecute(Args args) async {
|
Future<void> cmdExecute(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
final code = await File(args.sourceFile!).readAsString();
|
final code = await File(args.sourceFile!).readAsString();
|
||||||
|
|
@ -657,7 +711,7 @@ Future<void> cmdExecute(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdSession(Args args) async {
|
Future<void> cmdSession(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -717,7 +771,7 @@ Future<void> cmdSession(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdService(Args args) async {
|
Future<void> cmdService(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -941,7 +995,7 @@ Future<void> cmdService(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdLanguages(Args args) async {
|
Future<void> cmdLanguages(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -973,7 +1027,7 @@ Future<void> cmdLanguages(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdImage(Args args) async {
|
Future<void> cmdImage(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1115,7 +1169,7 @@ Future<void> imageTransfer(String id, String toKey, String publicKey, String? se
|
||||||
|
|
||||||
// Snapshot functions
|
// Snapshot functions
|
||||||
Future<void> cmdSnapshot(Args args) async {
|
Future<void> cmdSnapshot(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1319,7 +1373,7 @@ String sdkVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdKey(Args args) async {
|
Future<void> cmdKey(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1408,6 +1462,13 @@ Args parseArgs(List<String> argv) {
|
||||||
case '--api-key':
|
case '--api-key':
|
||||||
args.apiKey = argv[++i];
|
args.apiKey = argv[++i];
|
||||||
break;
|
break;
|
||||||
|
case '-p':
|
||||||
|
case '--public-key':
|
||||||
|
args.publicKey = argv[++i];
|
||||||
|
break;
|
||||||
|
case '--account':
|
||||||
|
args.account = int.parse(argv[++i]);
|
||||||
|
break;
|
||||||
case '-n':
|
case '-n':
|
||||||
case '--network':
|
case '--network':
|
||||||
args.network = argv[++i];
|
args.network = argv[++i];
|
||||||
|
|
@ -1658,7 +1719,9 @@ Execute options:
|
||||||
-o DIR Output directory for artifacts
|
-o DIR Output directory for artifacts
|
||||||
-n MODE Network mode (zerotrust/semitrusted)
|
-n MODE Network mode (zerotrust/semitrusted)
|
||||||
-v N vCPU count (1-8)
|
-v N vCPU count (1-8)
|
||||||
-k KEY API key
|
-p KEY Public key (use with -k for secret key)
|
||||||
|
-k KEY Secret/API key
|
||||||
|
--account N Use row N from accounts.csv (0-based)
|
||||||
|
|
||||||
Session options:
|
Session options:
|
||||||
--list List active sessions
|
--list List active sessions
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
# un.nim session --list
|
# un.nim session --list
|
||||||
# un.nim service --name web --ports 8080
|
# un.nim service --name web --ports 8080
|
||||||
|
|
||||||
import os, strutils, osproc, strformat, times
|
import os, strutils, osproc, strformat, times, sequtils
|
||||||
|
|
||||||
const
|
const
|
||||||
API_BASE = "https://api.unsandbox.com"
|
API_BASE = "https://api.unsandbox.com"
|
||||||
|
|
@ -1220,13 +1220,95 @@ proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceT
|
||||||
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer" & RESET)
|
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer" & RESET)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
proc main() =
|
proc loadCredentialsFromCsv(csvPath: string, accountIndex: int): tuple[pk: string, sk: string] =
|
||||||
var publicKey = getEnv("UNSANDBOX_PUBLIC_KEY", "")
|
## Load public_key,secret_key from CSV at given row index (0-based, skipping comments/blanks).
|
||||||
var secretKey = getEnv("UNSANDBOX_SECRET_KEY", "")
|
result = ("", "")
|
||||||
|
if not fileExists(csvPath):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
let content = readFile(csvPath)
|
||||||
|
var dataIndex = 0
|
||||||
|
for line in content.splitLines():
|
||||||
|
let trimmed = line.strip()
|
||||||
|
if trimmed.len == 0 or trimmed.startsWith("#"):
|
||||||
|
continue
|
||||||
|
if dataIndex == accountIndex:
|
||||||
|
let parts = trimmed.split(',')
|
||||||
|
if parts.len >= 2:
|
||||||
|
result = (parts[0].strip(), parts[1].strip())
|
||||||
|
return
|
||||||
|
dataIndex.inc
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
proc resolveCredentials(argPk: string, argSk: string, accountIndex: int): tuple[pk: string, sk: string] =
|
||||||
if publicKey == "":
|
## Resolve credentials using 5-tier priority:
|
||||||
publicKey = getEnv("UNSANDBOX_API_KEY", "")
|
## 1. Explicit -p/-k flags (argPk/argSk)
|
||||||
|
## 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 var)
|
||||||
|
## 5. ./accounts.csv row 0
|
||||||
|
# Tier 1: explicit key flags
|
||||||
|
if argPk != "" and argSk != "":
|
||||||
|
return (argPk, argSk)
|
||||||
|
# Tier 2: --account N bypasses env vars
|
||||||
|
if accountIndex >= 0:
|
||||||
|
let homeDir = getHomeDir()
|
||||||
|
let homeCsv = homeDir / ".unsandbox" / "accounts.csv"
|
||||||
|
var creds = loadCredentialsFromCsv(homeCsv, accountIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
creds = loadCredentialsFromCsv("accounts.csv", accountIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
stderr.writeLine(RED & fmt"Error: No credentials found for account index {accountIndex} in accounts.csv" & RESET)
|
||||||
|
quit(1)
|
||||||
|
# Tier 3: env vars
|
||||||
|
let envPk = getEnv("UNSANDBOX_PUBLIC_KEY", "")
|
||||||
|
let envSk = getEnv("UNSANDBOX_SECRET_KEY", "")
|
||||||
|
if envPk != "" and envSk != "":
|
||||||
|
return (envPk, envSk)
|
||||||
|
# Tier 4: ~/.unsandbox/accounts.csv (default row or UNSANDBOX_ACCOUNT)
|
||||||
|
let defaultIndex = parseInt(getEnv("UNSANDBOX_ACCOUNT", "0"))
|
||||||
|
let homeDir = getHomeDir()
|
||||||
|
let homeCsv = homeDir / ".unsandbox" / "accounts.csv"
|
||||||
|
var creds = loadCredentialsFromCsv(homeCsv, defaultIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
# Tier 5: ./accounts.csv
|
||||||
|
creds = loadCredentialsFromCsv("accounts.csv", defaultIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
# Legacy UNSANDBOX_API_KEY
|
||||||
|
let legacyKey = getEnv("UNSANDBOX_API_KEY", "")
|
||||||
|
if legacyKey != "":
|
||||||
|
return (legacyKey, legacyKey)
|
||||||
|
stderr.writeLine(RED & "Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY" & RESET)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
proc main() =
|
||||||
|
var argPublicKey = ""
|
||||||
|
var argSecretKey = ""
|
||||||
|
var accountIndex = -1 # -1 means not set
|
||||||
|
|
||||||
|
# Pre-scan for --account, -p, and -k before subcommand dispatch
|
||||||
|
let rawArgs = commandLineParams()
|
||||||
|
var i = 0
|
||||||
|
while i < rawArgs.len:
|
||||||
|
if rawArgs[i] == "--account" and i + 1 < rawArgs.len:
|
||||||
|
accountIndex = parseInt(rawArgs[i + 1])
|
||||||
|
i.inc
|
||||||
|
elif rawArgs[i] == "-p" and i + 1 < rawArgs.len:
|
||||||
|
argPublicKey = rawArgs[i + 1]
|
||||||
|
i.inc
|
||||||
|
elif rawArgs[i] == "-k" and i + 1 < rawArgs.len:
|
||||||
|
argSecretKey = rawArgs[i + 1]
|
||||||
|
i.inc
|
||||||
|
i.inc
|
||||||
|
|
||||||
|
let resolved = resolveCredentials(argPublicKey, argSecretKey, accountIndex)
|
||||||
|
var publicKey = resolved.pk
|
||||||
|
var secretKey = resolved.sk
|
||||||
|
|
||||||
let args = commandLineParams()
|
let args = commandLineParams()
|
||||||
|
|
||||||
|
|
@ -1265,6 +1347,11 @@ proc main() =
|
||||||
stderr.writeLine("Service options:")
|
stderr.writeLine("Service options:")
|
||||||
stderr.writeLine(" -e KEY=VALUE Set environment variable (for vault)")
|
stderr.writeLine(" -e KEY=VALUE Set environment variable (for vault)")
|
||||||
stderr.writeLine(" --env-file FILE Load env vars from file (for vault)")
|
stderr.writeLine(" --env-file FILE Load env vars from file (for vault)")
|
||||||
|
stderr.writeLine("")
|
||||||
|
stderr.writeLine("Credential options (global):")
|
||||||
|
stderr.writeLine(" -p PUBLIC_KEY Explicit public key")
|
||||||
|
stderr.writeLine(" -k SECRET_KEY Explicit secret key")
|
||||||
|
stderr.writeLine(" --account N Use row N from accounts.csv (bypasses env vars)")
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
if args[0] == "languages":
|
if args[0] == "languages":
|
||||||
|
|
@ -1506,6 +1593,8 @@ proc main() =
|
||||||
of "-n": network = args[i+1]; inc i
|
of "-n": network = args[i+1]; inc i
|
||||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||||
of "-k": publicKey = args[i+1]; inc i
|
of "-k": publicKey = args[i+1]; inc i
|
||||||
|
of "-p": publicKey = args[i+1]; inc i
|
||||||
|
of "--account": inc i # already handled in pre-scan
|
||||||
else:
|
else:
|
||||||
if args[i].startsWith("-"):
|
if args[i].startsWith("-"):
|
||||||
stderr.writeLine(RED & "Unknown option: " & args[i] & RESET)
|
stderr.writeLine(RED & "Unknown option: " & args[i] & RESET)
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,8 @@ interface Args {
|
||||||
network: string | null;
|
network: string | null;
|
||||||
vcpu: number | null;
|
vcpu: number | null;
|
||||||
apiKey: string | null;
|
apiKey: string | null;
|
||||||
|
publicKey: string | null;
|
||||||
|
account: number | null;
|
||||||
shell: string | null;
|
shell: string | null;
|
||||||
list: boolean;
|
list: boolean;
|
||||||
attach: string | null;
|
attach: string | null;
|
||||||
|
|
@ -203,23 +205,71 @@ interface ApiKeys {
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApiKeys(argsKey: string | null): ApiKeys {
|
function loadAccountsCSV(filePath: string, index: number): { pk: string; sk: string } | null {
|
||||||
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
|
try {
|
||||||
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const lines = content.split('\n').filter(l => {
|
||||||
|
const t = l.trim();
|
||||||
|
return t.length > 0 && !t.startsWith('#');
|
||||||
|
});
|
||||||
|
if (index < 0 || index >= lines.length) return null;
|
||||||
|
const parts = lines[index].split(',');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
const pk = parts[0].trim();
|
||||||
|
const sk = parts[1].trim();
|
||||||
|
if (!pk || !sk) return null;
|
||||||
|
return { pk, sk };
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!publicKey || !secretKey) {
|
function getApiKeys(argsPublicKey: string | null, argsSecretKey: string | null, account: number | null): ApiKeys {
|
||||||
const oldKey = argsKey || process.env.UNSANDBOX_API_KEY;
|
// Tier 1: explicit -p/-k flags
|
||||||
if (oldKey) {
|
if (argsPublicKey && argsSecretKey) {
|
||||||
publicKey = oldKey;
|
return { publicKey: argsPublicKey, secretKey: argsSecretKey };
|
||||||
secretKey = oldKey;
|
|
||||||
} else {
|
|
||||||
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
|
|
||||||
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { publicKey, secretKey };
|
// Tier 2: --account N → accounts.csv row N (bypasses env vars)
|
||||||
|
if (account !== null) {
|
||||||
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
|
||||||
|
const homeCsv = path.join(homeDir, '.unsandbox', 'accounts.csv');
|
||||||
|
const localCsv = './accounts.csv';
|
||||||
|
const fromHome = loadAccountsCSV(homeCsv, account);
|
||||||
|
if (fromHome) return { publicKey: fromHome.pk, secretKey: fromHome.sk };
|
||||||
|
const fromLocal = loadAccountsCSV(localCsv, account);
|
||||||
|
if (fromLocal) return { publicKey: fromLocal.pk, secretKey: fromLocal.sk };
|
||||||
|
console.error(`${RED}Error: --account ${account} not found in accounts.csv${RESET}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: env vars
|
||||||
|
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
|
||||||
|
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
|
||||||
|
if (publicKey && secretKey) {
|
||||||
|
return { publicKey, secretKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
|
||||||
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
|
||||||
|
const accountIndex = process.env.UNSANDBOX_ACCOUNT ? parseInt(process.env.UNSANDBOX_ACCOUNT) : 0;
|
||||||
|
const homeCsv = path.join(homeDir, '.unsandbox', 'accounts.csv');
|
||||||
|
const fromHome = loadAccountsCSV(homeCsv, accountIndex);
|
||||||
|
if (fromHome) return { publicKey: fromHome.pk, secretKey: fromHome.sk };
|
||||||
|
|
||||||
|
// Tier 5: ./accounts.csv row 0
|
||||||
|
const fromLocal = loadAccountsCSV('./accounts.csv', accountIndex);
|
||||||
|
if (fromLocal) return { publicKey: fromLocal.pk, secretKey: fromLocal.sk };
|
||||||
|
|
||||||
|
// Legacy UNSANDBOX_API_KEY fallback
|
||||||
|
const oldKey = process.env.UNSANDBOX_API_KEY;
|
||||||
|
if (oldKey) {
|
||||||
|
return { publicKey: oldKey, secretKey: oldKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
|
||||||
|
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectLanguage(filename: string): string {
|
function detectLanguage(filename: string): string {
|
||||||
|
|
@ -695,7 +745,7 @@ async function cmdServiceEnv(action: string, target: string, envs: string[], env
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdExecute(args: Args): Promise<void> {
|
async function cmdExecute(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
|
|
||||||
let code: string;
|
let code: string;
|
||||||
try {
|
try {
|
||||||
|
|
@ -759,7 +809,7 @@ async function cmdExecute(args: Args): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdSession(args: Args): Promise<void> {
|
async function cmdSession(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
|
|
||||||
if (args.list) {
|
if (args.list) {
|
||||||
const result = await apiRequest("/sessions", "GET", null, keys);
|
const result = await apiRequest("/sessions", "GET", null, keys);
|
||||||
|
|
@ -817,7 +867,7 @@ async function cmdSession(args: Args): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdService(args: Args): Promise<void> {
|
async function cmdService(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
|
|
||||||
if (args.list) {
|
if (args.list) {
|
||||||
const result = await apiRequest("/services", "GET", null, keys);
|
const result = await apiRequest("/services", "GET", null, keys);
|
||||||
|
|
@ -1083,7 +1133,7 @@ async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdLanguages(args: Args): Promise<void> {
|
async function cmdLanguages(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
|
|
||||||
// Try cache first
|
// Try cache first
|
||||||
let langs = loadLanguagesCache();
|
let langs = loadLanguagesCache();
|
||||||
|
|
@ -1106,7 +1156,7 @@ async function cmdLanguages(args: Args): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdImage(args: Args): Promise<void> {
|
async function cmdImage(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
|
|
||||||
if (args.list) {
|
if (args.list) {
|
||||||
const result = await apiRequest("/images", "GET", null, keys);
|
const result = await apiRequest("/images", "GET", null, keys);
|
||||||
|
|
@ -1199,7 +1249,7 @@ async function cmdImage(args: Args): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdKey(args: Args): Promise<void> {
|
async function cmdKey(args: Args): Promise<void> {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
await validateKey(keys, args.extend);
|
await validateKey(keys, args.extend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1214,6 +1264,8 @@ function parseArgs(argv: string[]): Args {
|
||||||
network: null,
|
network: null,
|
||||||
vcpu: null,
|
vcpu: null,
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
|
publicKey: null,
|
||||||
|
account: null,
|
||||||
shell: null,
|
shell: null,
|
||||||
list: false,
|
list: false,
|
||||||
attach: null,
|
attach: null,
|
||||||
|
|
@ -1291,6 +1343,12 @@ function parseArgs(argv: string[]): Args {
|
||||||
} else if (arg === '-k' && i + 1 < argv.length) {
|
} else if (arg === '-k' && i + 1 < argv.length) {
|
||||||
args.apiKey = argv[++i];
|
args.apiKey = argv[++i];
|
||||||
i++;
|
i++;
|
||||||
|
} else if (arg === '-p' && i + 1 < argv.length) {
|
||||||
|
args.publicKey = argv[++i];
|
||||||
|
i++;
|
||||||
|
} else if (arg === '--account' && i + 1 < argv.length) {
|
||||||
|
args.account = parseInt(argv[++i]);
|
||||||
|
i++;
|
||||||
} else if (arg === '-s' || arg === '--shell') {
|
} else if (arg === '-s' || arg === '--shell') {
|
||||||
args.shell = argv[++i];
|
args.shell = argv[++i];
|
||||||
i++;
|
i++;
|
||||||
|
|
@ -1453,7 +1511,7 @@ async function main(): Promise<void> {
|
||||||
} else if (args.command === 'service') {
|
} else if (args.command === 'service') {
|
||||||
// Check for "service env" subcommand
|
// Check for "service env" subcommand
|
||||||
if (args.envAction) {
|
if (args.envAction) {
|
||||||
const keys = getApiKeys(args.apiKey);
|
const keys = getApiKeys(args.publicKey, args.apiKey, args.account);
|
||||||
await cmdServiceEnv(args.envAction, args.envTarget!, args.env, args.envFile, keys);
|
await cmdServiceEnv(args.envAction, args.envTarget!, args.env, args.envFile, keys);
|
||||||
} else {
|
} else {
|
||||||
await cmdService(args);
|
await cmdService(args);
|
||||||
|
|
@ -1484,7 +1542,9 @@ Execute options:
|
||||||
-o DIR Output directory for artifacts
|
-o DIR Output directory for artifacts
|
||||||
-n MODE Network mode (zerotrust|semitrusted)
|
-n MODE Network mode (zerotrust|semitrusted)
|
||||||
-v N vCPU count (1-8)
|
-v N vCPU count (1-8)
|
||||||
-k KEY API key
|
-p KEY Public key (use with -k for secret key)
|
||||||
|
-k KEY Secret/API key
|
||||||
|
--account N Use row N from accounts.csv (0-based)
|
||||||
|
|
||||||
Session options:
|
Session options:
|
||||||
-s, --shell NAME Shell/REPL (default: bash)
|
-s, --shell NAME Shell/REPL (default: bash)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue