Add --account N flag and fix credential priority in Erlang, Elixir, OCaml, F#, Haskell SDKs
Correct 5-tier credential priority across all five implementations: 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 Erlang: add load_credentials_from_csv/2, extract_account_arg/3, process-dict-based account_index, dispatch/1 helper, fix get_api_keys/0. Elixir: add load_credentials_from_csv/2, extract_account_arg/3, Process.put/get-based account_index, dispatch/1 helper, fix get_api_keys/0. OCaml: add cli_account_index ref, parse_accounts_csv, load_csv_at, strip_account_arg, fix get_credentials priority ordering. F#: add AccountIndex field to Args, loadCredentialsFromCsv, fix getApiKeys signature to accept accountIndex, wire --account N in parseArgs. Haskell: add cliAccountIndex IORef (unsafePerformIO), loadCredentialsFromCsv, stripAccountArg, fix getApiKeys priority ordering.
This commit is contained in:
parent
a839ffcd12
commit
13f15c8abc
5 changed files with 512 additions and 129 deletions
|
|
@ -56,8 +56,10 @@
|
|||
%%%
|
||||
%%% Authentication Priority:
|
||||
%%% 1. Function arguments (PublicKey, SecretKey)
|
||||
%%% 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
%%% 3. Config file (~/.unsandbox/accounts.csv)
|
||||
%%% 2. --account N -> accounts.csv row N (bypasses env vars)
|
||||
%%% 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
%%% 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||
%%% 5. ./accounts.csv row 0
|
||||
|
||||
-define(API_BASE, "https://api.unsandbox.com").
|
||||
-define(PORTAL_BASE, "https://unsandbox.com").
|
||||
|
|
@ -719,37 +721,60 @@ not_contains_error(Response) ->
|
|||
%% CLI Entry Point
|
||||
%% ============================================================================
|
||||
|
||||
main([]) ->
|
||||
io:format("Usage: un.erl [options] <source_file>~n"),
|
||||
io:format(" un.erl session [options]~n"),
|
||||
io:format(" un.erl service [options]~n"),
|
||||
io:format(" un.erl snapshot [options]~n"),
|
||||
io:format(" un.erl image [options]~n"),
|
||||
io:format(" un.erl key [options]~n"),
|
||||
main(RawArgs) ->
|
||||
%% Strip --account N from args and store index in process dict before dispatch
|
||||
{AccountIndex, Args} = extract_account_arg(RawArgs, undefined, []),
|
||||
case AccountIndex of
|
||||
undefined -> ok;
|
||||
N -> erlang:put(account_index, N)
|
||||
end,
|
||||
dispatch(Args).
|
||||
|
||||
dispatch([]) ->
|
||||
io:format("Usage: un.erl [--account N] [options] <source_file>~n"),
|
||||
io:format(" un.erl [--account N] session [options]~n"),
|
||||
io:format(" un.erl [--account N] service [options]~n"),
|
||||
io:format(" un.erl [--account N] snapshot [options]~n"),
|
||||
io:format(" un.erl [--account N] image [options]~n"),
|
||||
io:format(" un.erl [--account N] key [options]~n"),
|
||||
io:format(" un.erl languages [--json]~n"),
|
||||
io:format("~nGlobal options:~n"),
|
||||
io:format(" --account N Use accounts.csv row N (bypasses env vars)~n"),
|
||||
halt(1);
|
||||
|
||||
main(["session" | Rest]) ->
|
||||
dispatch(["session" | Rest]) ->
|
||||
session_command(Rest);
|
||||
|
||||
main(["service" | Rest]) ->
|
||||
dispatch(["service" | Rest]) ->
|
||||
service_command(Rest);
|
||||
|
||||
main(["snapshot" | Rest]) ->
|
||||
dispatch(["snapshot" | Rest]) ->
|
||||
snapshot_command(Rest);
|
||||
|
||||
main(["image" | Rest]) ->
|
||||
dispatch(["image" | Rest]) ->
|
||||
image_command(Rest);
|
||||
|
||||
main(["key" | Rest]) ->
|
||||
dispatch(["key" | Rest]) ->
|
||||
key_command(Rest);
|
||||
|
||||
main(["languages" | Rest]) ->
|
||||
dispatch(["languages" | Rest]) ->
|
||||
languages_command(Rest);
|
||||
|
||||
main(Args) ->
|
||||
dispatch(Args) ->
|
||||
execute_command(Args).
|
||||
|
||||
%% Strip --account N from argument list, return {Index | undefined, RestArgs}
|
||||
extract_account_arg([], Acc, RestAcc) ->
|
||||
{Acc, lists:reverse(RestAcc)};
|
||||
extract_account_arg(["--account", NStr | Rest], _Acc, RestAcc) ->
|
||||
N = try list_to_integer(NStr) catch _:_ ->
|
||||
io:format("Error: --account requires an integer argument~n"),
|
||||
halt(1)
|
||||
end,
|
||||
extract_account_arg(Rest, N, RestAcc);
|
||||
extract_account_arg([Arg | Rest], Acc, RestAcc) ->
|
||||
extract_account_arg(Rest, Acc, [Arg | RestAcc]).
|
||||
|
||||
%% Execute command
|
||||
execute_command(Args) ->
|
||||
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
||||
|
|
@ -1452,19 +1477,96 @@ open_extend_page(PublicKey) ->
|
|||
end.
|
||||
|
||||
%% Helpers
|
||||
get_api_keys() ->
|
||||
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
||||
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
||||
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
||||
|
||||
if
|
||||
PublicKey =/= false andalso SecretKey =/= false ->
|
||||
{PublicKey, SecretKey};
|
||||
ApiKey =/= false ->
|
||||
{ApiKey, false};
|
||||
true ->
|
||||
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
||||
halt(1)
|
||||
%% @doc Load credentials from a CSV file at the given path.
|
||||
%% Skips blank lines and comment lines (#). Returns {ok, {PK, SK}} or error.
|
||||
load_credentials_from_csv(CsvPath, AccountIndex) ->
|
||||
case file:read_file(CsvPath) of
|
||||
{ok, Bin} ->
|
||||
Lines = string:split(binary_to_list(Bin), "\n", all),
|
||||
ValidAccounts = lists:filtermap(fun(Line) ->
|
||||
Trimmed = string:trim(Line),
|
||||
case Trimmed of
|
||||
"" -> false;
|
||||
[$# | _] -> false;
|
||||
_ ->
|
||||
Parts = string:split(Trimmed, ",", all),
|
||||
case Parts of
|
||||
[PK, SK | _] ->
|
||||
PKt = string:trim(PK),
|
||||
SKt = string:trim(SK),
|
||||
if
|
||||
length(PKt) > 8 andalso length(SKt) > 8 ->
|
||||
{true, {PKt, SKt}};
|
||||
true -> false
|
||||
end;
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end, Lines),
|
||||
if
|
||||
AccountIndex < length(ValidAccounts) ->
|
||||
{ok, lists:nth(AccountIndex + 1, ValidAccounts)};
|
||||
true ->
|
||||
error
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
%% @doc Resolve credentials with correct priority:
|
||||
%% 1. --account N process-dict override -> accounts.csv row N
|
||||
%% 2. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
|
||||
%% 3. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||
%% 4. ./accounts.csv row 0
|
||||
get_api_keys() ->
|
||||
Home = case os:getenv("HOME") of false -> "."; H -> H end,
|
||||
HomeCsv = filename:join([Home, ".unsandbox", "accounts.csv"]),
|
||||
%% Priority 1: explicit --account N (stored in process dict by main/1)
|
||||
case erlang:get(account_index) of
|
||||
undefined ->
|
||||
%% Priority 2: environment variables
|
||||
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
||||
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
||||
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
||||
if
|
||||
PublicKey =/= false andalso SecretKey =/= false ->
|
||||
{PublicKey, SecretKey};
|
||||
ApiKey =/= false ->
|
||||
{ApiKey, false};
|
||||
true ->
|
||||
%% Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||
DefaultIndex = case os:getenv("UNSANDBOX_ACCOUNT") of
|
||||
false -> 0;
|
||||
IdxStr -> try list_to_integer(string:trim(IdxStr)) catch _:_ -> 0 end
|
||||
end,
|
||||
case load_credentials_from_csv(HomeCsv, DefaultIndex) of
|
||||
{ok, {PK, SK}} ->
|
||||
{PK, SK};
|
||||
error ->
|
||||
%% Priority 4: ./accounts.csv
|
||||
case load_credentials_from_csv("accounts.csv", DefaultIndex) of
|
||||
{ok, {PK2, SK2}} ->
|
||||
{PK2, SK2};
|
||||
error ->
|
||||
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
||||
halt(1)
|
||||
end
|
||||
end
|
||||
end;
|
||||
AccountIndex ->
|
||||
case load_credentials_from_csv(HomeCsv, AccountIndex) of
|
||||
{ok, {PK, SK}} ->
|
||||
{PK, SK};
|
||||
error ->
|
||||
case load_credentials_from_csv("accounts.csv", AccountIndex) of
|
||||
{ok, {PK2, SK2}} ->
|
||||
{PK2, SK2};
|
||||
error ->
|
||||
io:format("Error: No credentials found for account index ~B in accounts.csv~n", [AccountIndex]),
|
||||
halt(1)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
get_api_key() ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue