Add key subcommand to all 31 implementations

- Validate API keys via portal endpoint
- Show key status, tier, expiration with color-coded output
- Add --extend flag to open browser for key renewal
- Update .gitignore for compiled binaries
- Update README with key command documentation
This commit is contained in:
Russell Ballestrini 2025-12-27 11:50:34 -05:00
parent eb5d4ca8bf
commit 5380af54cb
32 changed files with 3140 additions and 70 deletions

105
un.erl Normal file → Executable file
View file

@ -46,6 +46,7 @@ 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 key [options]~n"),
halt(1);
main(["session" | Rest]) ->
@ -54,6 +55,9 @@ main(["session" | Rest]) ->
main(["service" | Rest]) ->
service_command(Rest);
main(["key" | Rest]) ->
key_command(Rest);
main(Args) ->
execute_command(Args).
@ -173,6 +177,70 @@ service_command(Args) ->
io:format("~s~n", [Response])
end.
%% Key command
key_command(Args) ->
ApiKey = get_api_key(),
case has_extend_flag(Args) of
true ->
validate_and_extend_key(ApiKey);
false ->
validate_key(ApiKey)
end.
validate_key(ApiKey) ->
Response = curl_post_portal(ApiKey, "/keys/validate", "{}"),
parse_and_display_key_status(Response, false).
validate_and_extend_key(ApiKey) ->
Response = curl_post_portal(ApiKey, "/keys/validate", "{}"),
parse_and_display_key_status(Response, true).
parse_and_display_key_status(Response, ShouldExtend) ->
%% Parse JSON response (simple extraction for fields we need)
Status = extract_json_field(Response, "status"),
PublicKey = extract_json_field(Response, "public_key"),
Tier = extract_json_field(Response, "tier"),
ExpiresAt = extract_json_field(Response, "expires_at"),
case Status of
"valid" ->
io:format("\033[32mValid\033[0m~n"),
io:format("Public Key: ~s~n", [PublicKey]),
io:format("Tier: ~s~n", [Tier]),
io:format("Expires: ~s~n", [ExpiresAt]),
if ShouldExtend ->
open_extend_page(PublicKey);
true -> ok
end;
"expired" ->
io:format("\033[31mExpired\033[0m~n"),
io:format("Public Key: ~s~n", [PublicKey]),
io:format("Tier: ~s~n", [Tier]),
io:format("Expired: ~s~n", [ExpiresAt]),
io:format("\033[33mTo renew: Visit https://unsandbox.com/keys/extend\033[0m~n"),
if ShouldExtend ->
open_extend_page(PublicKey);
true -> ok
end;
"invalid" ->
io:format("\033[31mInvalid\033[0m~n"),
io:format("The API key is not valid.~n");
_ ->
io:format("~s~n", [Response])
end.
open_extend_page(PublicKey) ->
Url = "https://unsandbox.com/keys/extend?pk=" ++ PublicKey,
io:format("\033[33mOpening browser to extend key...\033[0m~n"),
case os:type() of
{unix, darwin} ->
os:cmd("open '" ++ Url ++ "'");
{unix, _} ->
os:cmd("xdg-open '" ++ Url ++ "' 2>/dev/null || sensible-browser '" ++ Url ++ "' 2>/dev/null &");
{win32, _} ->
os:cmd("start " ++ Url)
end.
%% Helpers
get_api_key() ->
case os:getenv("UNSANDBOX_API_KEY") of
@ -250,6 +318,16 @@ curl_post(ApiKey, Endpoint, TmpFile) ->
" -d @" ++ TmpFile,
os:cmd(Cmd).
curl_post_portal(ApiKey, Endpoint, Data) ->
TmpFile = write_temp_file(Data),
Cmd = "curl -s -X POST https://unsandbox.com" ++ Endpoint ++
" -H 'Content-Type: application/json'" ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
" -d @" ++ TmpFile,
Result = os:cmd(Cmd),
file:delete(TmpFile),
Result.
curl_get(ApiKey, Endpoint) ->
Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
@ -289,3 +367,30 @@ get_service_bootstrap([_ | Rest]) -> get_service_bootstrap(Rest).
get_service_type([]) -> undefined;
get_service_type(["--type", Type | _]) -> Type;
get_service_type([_ | Rest]) -> get_service_type(Rest).
has_extend_flag([]) -> false;
has_extend_flag(["--extend" | _]) -> true;
has_extend_flag([_ | Rest]) -> has_extend_flag(Rest).
%% Simple JSON field extraction (works for simple string fields)
extract_json_field(Json, Field) ->
Pattern = "\"" ++ Field ++ "\":\"",
case string:str(Json, Pattern) of
0 -> "";
Pos ->
Start = Pos + length(Pattern),
Rest = lists:nthtail(Start - 1, Json),
extract_until_quote(Rest)
end.
extract_until_quote(Str) ->
extract_until_quote(Str, []).
extract_until_quote([], Acc) ->
lists:reverse(Acc);
extract_until_quote([$\" | _], Acc) ->
lists:reverse(Acc);
extract_until_quote([$\\, $\" | Rest], Acc) ->
extract_until_quote(Rest, [$\" | Acc]);
extract_until_quote([C | Rest], Acc) ->
extract_until_quote(Rest, [C | Acc]).