diff --git a/un.awk b/un.awk index ea813be..1a07220 100644 --- a/un.awk +++ b/un.awk @@ -44,6 +44,7 @@ BEGIN { API_BASE = "https://api.unsandbox.com" + PORTAL_BASE = "https://unsandbox.com" # Extension to language map split("py:python js:javascript ts:typescript rb:ruby php:php pl:perl lua:lua sh:bash go:go rs:rust c:c cpp:cpp java:java kt:kotlin cs:csharp fs:fsharp hs:haskell ml:ocaml clj:clojure scm:scheme lisp:commonlisp erl:erlang ex:elixir jl:julia r:r cr:crystal d:d nim:nim zig:zig v:v dart:dart groovy:groovy f90:fortran cob:cobol pro:prolog forth:forth tcl:tcl raku:raku m:objc awk:awk ps1:powershell", pairs, " ") @@ -242,10 +243,97 @@ function service_create(name, ports, domains, service_type, bootstrap) { print response } +function validate_key(api_key, do_extend) { + # Call curl to validate key + cmd = "curl -s -X POST '" PORTAL_BASE "/keys/validate' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " api_key "'" + + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + + # Parse expired status (simple regex check) + if (match(response, /"expired":true/)) { + print RED "Expired" RESET + + # Extract public_key if present + if (match(response, /"public_key":"([^"]+)"/, arr)) { + public_key = arr[1] + print "Public Key: " public_key + } + + # Extract tier + if (match(response, /"tier":"([^"]+)"/, arr)) { + print "Tier: " arr[1] + } + + # Extract expires_at + if (match(response, /"expires_at":"([^"]+)"/, arr)) { + print "Expired: " arr[1] + } + + print YELLOW "To renew: Visit https://unsandbox.com/keys/extend" RESET + + if (do_extend && public_key) { + url = PORTAL_BASE "/keys/extend?pk=" public_key + print "" + print BLUE "Opening browser to: " url RESET + system("xdg-open '" url "' 2>/dev/null || open '" url "' 2>/dev/null &") + } + exit 1 + } + + # Valid key + print GREEN "Valid" RESET + + # Extract and display fields + if (match(response, /"public_key":"([^"]+)"/, arr)) { + public_key = arr[1] + print "Public Key: " public_key + } + if (match(response, /"tier":"([^"]+)"/, arr)) { + print "Tier: " arr[1] + } + if (match(response, /"status":"([^"]+)"/, arr)) { + print "Status: " arr[1] + } + if (match(response, /"expires_at":"([^"]+)"/, arr)) { + print "Expires: " arr[1] + } + if (match(response, /"time_remaining":"([^"]+)"/, arr)) { + print "Time Remaining: " arr[1] + } + if (match(response, /"rate_limit":"?([^",}]+)"?/, arr)) { + print "Rate Limit: " arr[1] + } + if (match(response, /"burst":"?([^",}]+)"?/, arr)) { + print "Burst: " arr[1] + } + if (match(response, /"concurrency":"?([^",}]+)"?/, arr)) { + print "Concurrency: " arr[1] + } + + if (do_extend && public_key) { + url = PORTAL_BASE "/keys/extend?pk=" public_key + print "" + print BLUE "Opening browser to: " url RESET + system("xdg-open '" url "' 2>/dev/null || open '" url "' 2>/dev/null &") + } +} + +function cmd_key(do_extend) { + api_key = get_api_key() + validate_key(api_key, do_extend) +} + function show_help() { print "Usage: awk -f un.awk " print " awk -f un.awk session --list" print " awk -f un.awk session --kill ID" + print " awk -f un.awk key [--extend]" print " awk -f un.awk service --list" print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD]" print " awk -f un.awk service --destroy ID" @@ -288,6 +376,15 @@ END { exit 0 } + if (ARGV[1] == "key") { + do_extend = 0 + if (ARGC >= 3 && ARGV[2] == "--extend") { + do_extend = 1 + } + cmd_key(do_extend) + exit 0 + } + if (ARGV[1] == "service") { if (ARGC >= 3 && ARGV[2] == "--list") { service_list() diff --git a/un.clj b/un.clj index 9076a6c..1ef3a04 100644 --- a/un.clj +++ b/un.clj @@ -216,27 +216,39 @@ status (extract-field "status" response) public-key (extract-field "public_key" response) tier (extract-field "tier" response) - expires-at (extract-field "expires_at" response)] + valid-through (extract-field "valid_through_datetime" response) + valid-for (extract-field "valid_for_human" response) + rate-limit (extract-field "rate_per_minute" response) + burst (extract-field "burst" response) + concurrency (extract-field "concurrency" response) + expired-at (extract-field "expired_at_datetime" response)] (cond (= status "valid") (do - (println (str green "Valid" reset)) - (when public-key (println (str "Public Key: " public-key))) - (when tier (println (str "Tier: " tier))) - (when expires-at (println (str "Expires: " expires-at))) + (println (str green "Valid" reset "\n")) + (when public-key (println (str "Public Key: " public-key))) + (when tier (println (str "Tier: " tier))) + (println "Status: valid") + (when valid-through (println (str "Expires: " valid-through))) + (when valid-for (println (str "Time Remaining: " valid-for))) + (when rate-limit (println (str "Rate Limit: " rate-limit "/min"))) + (when burst (println (str "Burst: " burst))) + (when concurrency (println (str "Concurrency: " concurrency))) (when extend? (let [url (str portal-base "/keys/extend?pk=" public-key)] + (println (str blue "Opening browser to extend key..." reset)) (sh "xdg-open" url)))) (= status "expired") (do - (println (str red "Expired" reset)) - (when public-key (println (str "Public Key: " public-key))) - (when tier (println (str "Tier: " tier))) - (when expires-at (println (str "Expired: " expires-at))) - (println (str yellow "To renew: Visit " portal-base "/keys/extend" reset)) + (println (str red "Expired" reset "\n")) + (when public-key (println (str "Public Key: " public-key))) + (when tier (println (str "Tier: " tier))) + (when expired-at (println (str "Expired: " expired-at))) + (println (str "\n" yellow "To renew:" reset " Visit " portal-base "/keys/extend")) (when extend? (let [url (str portal-base "/keys/extend?pk=" public-key)] + (println (str blue "Opening browser..." reset)) (sh "xdg-open" url)))) :else diff --git a/un.cob b/un.cob index b2e7e04..910fb05 100644 --- a/un.cob +++ b/un.cob @@ -73,6 +73,9 @@ 01 WS-DOMAINS PIC X(256). 01 WS-SERVICE-TYPE PIC X(64). 01 WS-BOOTSTRAP PIC X(2048). + 01 WS-PORTAL-BASE PIC X(256) VALUE + "https://unsandbox.com". + 01 WS-EXTEND-FLAG PIC X(8). PROCEDURE DIVISION. MAIN-PROCEDURE. @@ -98,6 +101,11 @@ STOP RUN END-IF. + IF WS-ARG1 = "key" + PERFORM HANDLE-KEY + STOP RUN + END-IF. + * Default: execute command MOVE WS-ARG1 TO WS-FILENAME. PERFORM HANDLE-EXECUTE. @@ -442,3 +450,93 @@ END-STRING. CALL "SYSTEM" USING WS-CURL-CMD. + + HANDLE-KEY. + * Get API key + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF. + + * Parse key arguments + MOVE SPACES TO WS-EXTEND-FLAG. + ACCEPT WS-ARG2 FROM ARGUMENT-VALUE. + + IF WS-ARG2 = "--extend" + MOVE "true" TO WS-EXTEND-FLAG + END-IF. + + * Validate key + PERFORM VALIDATE-KEY. + + VALIDATE-KEY. + * Build curl command to validate API key + STRING "curl -s -X POST " + FUNCTION TRIM(WS-PORTAL-BASE) + "/keys/validate " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY) + "' -o /tmp/unsandbox_key_resp.json; " + "STATUS=$?; " + "if [ $STATUS -ne 0 ]; then " + "echo -e '\x1b[31mInvalid\x1b[0m'; " + "exit 1; " + "fi; " + "EXPIRED=$(jq -r '.expired // false' " + "/tmp/unsandbox_key_resp.json); " + "PUBLIC_KEY=$(jq -r '.public_key // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + IF WS-EXTEND-FLAG = "true" + STRING FUNCTION TRIM(WS-CURL-CMD) + "xdg-open '" + FUNCTION TRIM(WS-PORTAL-BASE) + "/keys/extend?pk='\"$PUBLIC_KEY\" 2>/dev/null; " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + ELSE + STRING FUNCTION TRIM(WS-CURL-CMD) + "if [ \"$EXPIRED\" = \"true\" ]; then " + "echo -e '\x1b[31mExpired\x1b[0m'; " + "echo 'Public Key: '$PUBLIC_KEY; " + "echo 'Tier: '$(jq -r '.tier // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Expired: '$(jq -r '.expires_at // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo -e '\x1b[33mTo renew: Visit " + "https://unsandbox.com/keys/extend\x1b[0m'; " + "rm -f /tmp/unsandbox_key_resp.json; " + "exit 1; " + "else " + "echo -e '\x1b[32mValid\x1b[0m'; " + "echo 'Public Key: '$PUBLIC_KEY; " + "echo 'Tier: '$(jq -r '.tier // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Status: '$(jq -r '.status // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Expires: '$(jq -r '.expires_at // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Time Remaining: '$(jq -r " + "'.time_remaining // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Rate Limit: '$(jq -r '.rate_limit // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Burst: '$(jq -r '.burst // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "echo 'Concurrency: '$(jq -r '.concurrency // \"N/A\"' " + "/tmp/unsandbox_key_resp.json); " + "fi; " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + STRING FUNCTION TRIM(WS-CURL-CMD) + "rm -f /tmp/unsandbox_key_resp.json" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. diff --git a/un.dart b/un.dart index 2fe1b14..1470b39 100644 --- a/un.dart +++ b/un.dart @@ -357,41 +357,52 @@ Future cmdKey(Args args) async { try { final result = await apiRequestCurl('/keys/validate', 'POST', null, apiKey, baseUrl: portalBase); - final status = result['status'] as String?; - final publicKey = result['public_key'] as String?; - final tier = result['tier'] as String?; - final expiresAt = result['expires_at'] as String?; - - if (status == 'valid') { - print('${green}Valid$reset'); - if (publicKey != null) print('Public Key: $publicKey'); - if (tier != null) print('Tier: $tier'); - if (expiresAt != null) print('Expires: $expiresAt'); - } else if (status == 'expired') { - print('${red}Expired$reset'); - if (publicKey != null) print('Public Key: $publicKey'); - if (tier != null) print('Tier: $tier'); - if (expiresAt != null) print('Expired: $expiresAt'); - print('${yellow}To renew: Visit $portalBase/keys/extend$reset'); - } else { - print('${red}Invalid$reset'); - } - - if (args.keyExtend && publicKey != null) { - final url = '$portalBase/keys/extend?pk=$publicKey'; - print('${yellow}Opening: $url$reset'); - if (Platform.isMacOS) { - await Process.run('open', [url]); - } else if (Platform.isLinux) { - await Process.run('xdg-open', [url]); - } else if (Platform.isWindows) { - await Process.run('cmd', ['/c', 'start', url]); + // Handle --extend flag + if (args.keyExtend) { + final publicKey = result['public_key'] as String?; + if (publicKey != null) { + final url = '$portalBase/keys/extend?pk=$publicKey'; + print('${blue}Opening browser to extend key...$reset'); + if (Platform.isMacOS) { + await Process.run('open', [url]); + } else if (Platform.isLinux) { + await Process.run('xdg-open', [url]); + } else if (Platform.isWindows) { + await Process.run('cmd', ['/c', 'start', url]); + } else { + print('${yellow}Please open manually: $url$reset'); + } + return; } else { - print('${yellow}Please open manually: $url$reset'); + stderr.writeln('${red}Error: Could not retrieve public key$reset'); + exit(1); } } + + // Check if key is expired + final expired = result['expired'] as bool? ?? false; + if (expired) { + print('${red}Expired$reset'); + print('Public Key: ${result['public_key'] ?? 'N/A'}'); + print('Tier: ${result['tier'] ?? 'N/A'}'); + print('Expired: ${result['expires_at'] ?? 'N/A'}'); + print('${yellow}To renew: Visit $portalBase/keys/extend$reset'); + exit(1); + } + + // Valid key + print('${green}Valid$reset'); + print('Public Key: ${result['public_key'] ?? 'N/A'}'); + print('Tier: ${result['tier'] ?? 'N/A'}'); + print('Status: ${result['status'] ?? 'N/A'}'); + print('Expires: ${result['expires_at'] ?? 'N/A'}'); + print('Time Remaining: ${result['time_remaining'] ?? 'N/A'}'); + print('Rate Limit: ${result['rate_limit'] ?? 'N/A'}'); + print('Burst: ${result['burst'] ?? 'N/A'}'); + print('Concurrency: ${result['concurrency'] ?? 'N/A'}'); } catch (e) { - stderr.writeln('${red}Error validating key: $e$reset'); + print('${red}Invalid$reset'); + print('Reason: $e'); exit(1); } } diff --git a/un.erl b/un.erl index 8cac9a5..6fc0a5a 100755 --- a/un.erl +++ b/un.erl @@ -201,13 +201,22 @@ parse_and_display_key_status(Response, ShouldExtend) -> PublicKey = extract_json_field(Response, "public_key"), Tier = extract_json_field(Response, "tier"), ExpiresAt = extract_json_field(Response, "expires_at"), + TimeRemaining = extract_json_field(Response, "time_remaining"), + RateLimit = extract_json_field(Response, "rate_limit"), + Burst = extract_json_field(Response, "burst"), + Concurrency = extract_json_field(Response, "concurrency"), 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("Status: ~s~n", [Status]), io:format("Expires: ~s~n", [ExpiresAt]), + if TimeRemaining =/= "" -> io:format("Time Remaining: ~s~n", [TimeRemaining]); true -> ok end, + if RateLimit =/= "" -> io:format("Rate Limit: ~s~n", [RateLimit]); true -> ok end, + if Burst =/= "" -> io:format("Burst: ~s~n", [Burst]); true -> ok end, + if Concurrency =/= "" -> io:format("Concurrency: ~s~n", [Concurrency]); true -> ok end, if ShouldExtend -> open_extend_page(PublicKey); true -> ok diff --git a/un.ex b/un.ex index 98d558a..c103624 100755 --- a/un.ex +++ b/un.ex @@ -248,13 +248,22 @@ defmodule Un do public_key = Map.get(data, "public_key") tier = Map.get(data, "tier") expires_at = Map.get(data, "expires_at") + time_remaining = Map.get(data, "time_remaining") + rate_limit = Map.get(data, "rate_limit") + burst = Map.get(data, "burst") + concurrency = Map.get(data, "concurrency") case status do "valid" -> IO.puts("#{@green}Valid#{@reset}") IO.puts("Public Key: #{public_key}") IO.puts("Tier: #{tier}") + IO.puts("Status: #{status}") IO.puts("Expires: #{expires_at}") + if time_remaining, do: IO.puts("Time Remaining: #{time_remaining}") + if rate_limit, do: IO.puts("Rate Limit: #{rate_limit}") + if burst, do: IO.puts("Burst: #{burst}") + if concurrency, do: IO.puts("Concurrency: #{concurrency}") if extend do open_browser("#{@portal_base}/keys/extend?pk=#{public_key}") @@ -285,13 +294,22 @@ defmodule Un do public_key = extract_json_value(response, "public_key") tier = extract_json_value(response, "tier") expires_at = extract_json_value(response, "expires_at") + time_remaining = extract_json_value(response, "time_remaining") + rate_limit = extract_json_value(response, "rate_limit") + burst = extract_json_value(response, "burst") + concurrency = extract_json_value(response, "concurrency") case status do "valid" -> IO.puts("#{@green}Valid#{@reset}") IO.puts("Public Key: #{public_key}") IO.puts("Tier: #{tier}") + IO.puts("Status: #{status}") IO.puts("Expires: #{expires_at}") + if time_remaining, do: IO.puts("Time Remaining: #{time_remaining}") + if rate_limit, do: IO.puts("Rate Limit: #{rate_limit}") + if burst, do: IO.puts("Burst: #{burst}") + if concurrency, do: IO.puts("Concurrency: #{concurrency}") if extend do open_browser("#{@portal_base}/keys/extend?pk=#{public_key}") diff --git a/un.f90 b/un.f90 index 106805a..6a5f56f 100644 --- a/un.f90 +++ b/un.f90 @@ -41,13 +41,14 @@ program unsandbox_cli character(len=1024) :: filename, language, api_key, ext, arg, subcommand character(len=256) :: session_id, service_id integer :: stat, i, nargs, dot_pos - logical :: list_flag, is_session, is_service + logical :: list_flag, is_session, is_service, is_key ! Initialize subcommand = '' list_flag = .false. is_session = .false. is_service = .false. + is_key = .false. session_id = '' service_id = '' @@ -57,6 +58,7 @@ program unsandbox_cli write(0, '(A)') 'Usage: un.f90 [options] ' write(0, '(A)') ' un.f90 session [options]' write(0, '(A)') ' un.f90 service [options]' + write(0, '(A)') ' un.f90 key [--extend]' stop 1 end if @@ -70,6 +72,10 @@ program unsandbox_cli is_service = .true. call handle_service() stop 0 + else if (trim(arg) == 'key') then + is_key = .true. + call handle_key() + stop 0 else ! Default execute command filename = trim(arg) @@ -314,4 +320,104 @@ contains end if end subroutine handle_service + subroutine handle_key() + character(len=4096) :: full_cmd + character(len=256) :: arg + integer :: i, stat + logical :: extend_mode + character(len=32) :: portal_base + + portal_base = 'https://unsandbox.com' + extend_mode = .false. + + ! Check for --extend flag + do i = 2, command_argument_count() + call get_command_argument(i, arg) + if (trim(arg) == '--extend') then + extend_mode = .true. + end if + end do + + ! Get API key + call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat) + if (stat /= 0 .or. len_trim(api_key) == 0) then + write(0, '(A)') 'Error: UNSANDBOX_API_KEY not set' + stop 1 + end if + + if (extend_mode) then + ! Validate and extend + write(full_cmd, '(30A)') & + 'resp=$(curl -s -X POST ', trim(portal_base), '/keys/validate ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(api_key), '" ', & + '-d "{}"); ', & + 'status=$(echo "$resp" | jq -r ".status // empty"); ', & + 'public_key=$(echo "$resp" | jq -r ".public_key // empty"); ', & + 'tier=$(echo "$resp" | jq -r ".tier // empty"); ', & + 'expires_at=$(echo "$resp" | jq -r ".expires_at // empty"); ', & + 'time_remaining=$(echo "$resp" | jq -r ".time_remaining // empty"); ', & + 'rate_limit=$(echo "$resp" | jq -r ".rate_limit // empty"); ', & + 'burst=$(echo "$resp" | jq -r ".burst // empty"); ', & + 'concurrency=$(echo "$resp" | jq -r ".concurrency // empty"); ', & + 'if [ "$status" = "valid" ]; then ', & + 'echo -e "\x1b[32mValid\x1b[0m"; ', & + 'echo "Public Key: $public_key"; ', & + 'echo "Tier: $tier"; ', & + 'echo "Status: $status"; ', & + 'echo "Expires: $expires_at"; ', & + '[ -n "$time_remaining" ] && echo "Time Remaining: $time_remaining"; ', & + '[ -n "$rate_limit" ] && echo "Rate Limit: $rate_limit"; ', & + '[ -n "$burst" ] && echo "Burst: $burst"; ', & + '[ -n "$concurrency" ] && echo "Concurrency: $concurrency"; ', & + 'echo -e "\x1b[34mOpening browser to extend key...\x1b[0m"; ', & + 'xdg-open "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null || ', & + 'sensible-browser "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null &; ', & + 'elif [ "$status" = "expired" ]; then ', & + 'echo -e "\x1b[31mExpired\x1b[0m"; ', & + 'echo "Public Key: $public_key"; ', & + 'echo "Tier: $tier"; ', & + 'echo "Expired: $expires_at"; ', & + 'echo -e "\x1b[33mTo renew: Visit ', trim(portal_base), '/keys/extend\x1b[0m"; ', & + 'echo -e "\x1b[34mOpening browser to extend key...\x1b[0m"; ', & + 'xdg-open "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null || ', & + 'sensible-browser "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null &; ', & + 'else echo -e "\x1b[31mInvalid\x1b[0m"; fi' + else + ! Validate only + write(full_cmd, '(30A)') & + 'resp=$(curl -s -X POST ', trim(portal_base), '/keys/validate ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(api_key), '" ', & + '-d "{}"); ', & + 'status=$(echo "$resp" | jq -r ".status // empty"); ', & + 'public_key=$(echo "$resp" | jq -r ".public_key // empty"); ', & + 'tier=$(echo "$resp" | jq -r ".tier // empty"); ', & + 'expires_at=$(echo "$resp" | jq -r ".expires_at // empty"); ', & + 'time_remaining=$(echo "$resp" | jq -r ".time_remaining // empty"); ', & + 'rate_limit=$(echo "$resp" | jq -r ".rate_limit // empty"); ', & + 'burst=$(echo "$resp" | jq -r ".burst // empty"); ', & + 'concurrency=$(echo "$resp" | jq -r ".concurrency // empty"); ', & + 'if [ "$status" = "valid" ]; then ', & + 'echo -e "\x1b[32mValid\x1b[0m"; ', & + 'echo "Public Key: $public_key"; ', & + 'echo "Tier: $tier"; ', & + 'echo "Status: $status"; ', & + 'echo "Expires: $expires_at"; ', & + '[ -n "$time_remaining" ] && echo "Time Remaining: $time_remaining"; ', & + '[ -n "$rate_limit" ] && echo "Rate Limit: $rate_limit"; ', & + '[ -n "$burst" ] && echo "Burst: $burst"; ', & + '[ -n "$concurrency" ] && echo "Concurrency: $concurrency"; ', & + 'elif [ "$status" = "expired" ]; then ', & + 'echo -e "\x1b[31mExpired\x1b[0m"; ', & + 'echo "Public Key: $public_key"; ', & + 'echo "Tier: $tier"; ', & + 'echo "Expired: $expires_at"; ', & + 'echo -e "\x1b[33mTo renew: Visit ', trim(portal_base), '/keys/extend\x1b[0m"; ', & + 'else echo -e "\x1b[31mInvalid\x1b[0m"; fi' + end if + + call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat) + end subroutine handle_key + end program unsandbox_cli diff --git a/un.forth b/un.forth index 00d24d9..de7e697 100644 --- a/un.forth +++ b/un.forth @@ -39,6 +39,12 @@ \ Usage: gforth un.forth \ gforth un.forth session [options] \ gforth un.forth service [options] +\ gforth un.forth key [options] + +\ Constants +: portal-base ( -- addr len ) + s" https://unsandbox.com" +; \ Extension to language mapping (simple linear search) : ext-lang ( addr len -- addr len | 0 0 ) @@ -282,6 +288,77 @@ s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system ; +\ Key validate +: validate-key ( extend-flag -- ) + get-api-key + s" /tmp/unsandbox_key_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" API_KEY='" r@ write-file throw + get-api-key r@ write-file throw + s" '" r@ write-line throw + s" PORTAL_BASE='" r@ write-file throw + portal-base r@ write-file throw + s" '" r@ write-line throw + + \ Check if extend flag is set + 0= if + \ Normal validation + s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw + s" STATUS=$?" r@ write-line throw + s" if [ $STATUS -ne 0 ]; then" r@ write-line throw + s" echo -e '\\x1b[31mInvalid\\x1b[0m'" r@ write-line throw + s" exit 1" r@ write-line throw + s" fi" r@ write-line throw + s" EXPIRED=$(jq -r '.expired // false' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" if [ \"$EXPIRED\" = \"true\" ]; then" r@ write-line throw + s" echo -e '\\x1b[31mExpired\\x1b[0m'" r@ write-line throw + s" echo 'Public Key: '$(jq -r '.public_key // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Tier: '$(jq -r '.tier // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Expired: '$(jq -r '.expires_at // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo -e '\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m'" r@ write-line throw + s" rm -f /tmp/unsandbox_key_resp.json" r@ write-line throw + s" exit 1" r@ write-line throw + s" else" r@ write-line throw + s" echo -e '\\x1b[32mValid\\x1b[0m'" r@ write-line throw + s" echo 'Public Key: '$(jq -r '.public_key // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Tier: '$(jq -r '.tier // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Status: '$(jq -r '.status // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Expires: '$(jq -r '.expires_at // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Time Remaining: '$(jq -r '.time_remaining // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Rate Limit: '$(jq -r '.rate_limit // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Burst: '$(jq -r '.burst // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" echo 'Concurrency: '$(jq -r '.concurrency // \"N/A\"' /tmp/unsandbox_key_resp.json)" r@ write-line throw + s" fi" r@ write-line throw + s" rm -f /tmp/unsandbox_key_resp.json" r@ write-line throw + else + \ Extend mode + s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\")" r@ write-line throw + s" PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw + s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$PUBLIC_KEY\" 2>/dev/null" r@ write-line throw + then + + r> close-file throw + s" chmod +x /tmp/unsandbox_key_cmd.sh && /tmp/unsandbox_key_cmd.sh && rm -f /tmp/unsandbox_key_cmd.sh" system +; + +\ Handle key subcommand +: handle-key ( -- ) + argc @ 3 < if + 0 validate-key + 0 (bye) + then + + 2 arg 2dup s" --extend" compare 0= if + 2drop + 1 validate-key + 0 (bye) + then + + 2drop + 0 validate-key + 0 (bye) +; + \ Handle session subcommand : handle-session ( -- ) argc @ 3 < if @@ -398,6 +475,7 @@ s" Usage: gforth un.forth " type cr s" gforth un.forth session [options]" type cr s" gforth un.forth service [options]" type cr + s" gforth un.forth key [options]" type cr 1 (bye) then @@ -415,6 +493,11 @@ 0 (bye) then + 2dup s" key" compare 0= if + 2drop handle-key + 0 (bye) + then + \ Default: execute file execute-file ; diff --git a/un.fs b/un.fs index d0491e8..a1cbbb3 100644 --- a/un.fs +++ b/un.fs @@ -46,6 +46,7 @@ open System.Net open System.Text let apiBase = "https://api.unsandbox.com" +let portalBase = "https://unsandbox.com" let blue = "\x1B[34m" let red = "\x1B[31m" let green = "\x1B[32m" @@ -93,6 +94,7 @@ type Args = { mutable ServiceSleep: string option mutable ServiceWake: string option mutable ServiceDestroy: string option + mutable KeyExtend: bool } let getApiKey (argsKey: string option) = @@ -324,6 +326,87 @@ let cmdSession (args: Args) = | None -> printfn "%sSession created%s" green reset printfn "%s(Interactive sessions require WebSocket - use un2 for full support)%s" yellow reset +let openBrowser (url: string) = + try + let os = Environment.OSVersion.Platform + let cmd = + if os = PlatformID.Unix || os = PlatformID.MacOSX then + if System.IO.File.Exists("/usr/bin/xdg-open") then + System.Diagnostics.Process.Start("xdg-open", url) + else + System.Diagnostics.Process.Start("open", url) + else + System.Diagnostics.Process.Start("cmd", sprintf "/c start %s" url) + cmd.WaitForExit() + with ex -> + eprintfn "%sError opening browser: %s%s" red ex.Message reset + +let cmdKey (args: Args) = + let apiKey = getApiKey args.ApiKey + + ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls + + let request = WebRequest.Create(portalBase + "/keys/validate") :?> HttpWebRequest + request.Method <- "POST" + request.ContentType <- "application/json" + request.Headers.Add("Authorization", sprintf "Bearer %s" apiKey) + request.Timeout <- 30000 + + try + use response = request.GetResponse() :?> HttpWebResponse + use reader = new StreamReader(response.GetResponseStream()) + let responseText = reader.ReadToEnd() + let result = parseJson responseText + + let publicKey = match result.TryFind "public_key" with | Some v -> v.ToString() | None -> "N/A" + let tier = match result.TryFind "tier" with | Some v -> v.ToString() | None -> "N/A" + let status = match result.TryFind "status" with | Some v -> v.ToString() | None -> "N/A" + let expiresAt = match result.TryFind "expires_at" with | Some v -> v.ToString() | None -> "N/A" + let timeRemaining = match result.TryFind "time_remaining" with | Some v -> v.ToString() | None -> "N/A" + let rateLimit = match result.TryFind "rate_limit" with | Some v -> v.ToString() | None -> "N/A" + let burst = match result.TryFind "burst" with | Some v -> v.ToString() | None -> "N/A" + let concurrency = match result.TryFind "concurrency" with | Some v -> v.ToString() | None -> "N/A" + let expired = match result.TryFind "expired" with | Some v -> v.ToString() = "True" | None -> false + + if args.KeyExtend && publicKey <> "N/A" then + let extendUrl = sprintf "%s/keys/extend?pk=%s" portalBase publicKey + printfn "%sOpening browser to extend key...%s" blue reset + openBrowser extendUrl + elif expired then + printfn "%sExpired%s" red reset + printfn "Public Key: %s" publicKey + printfn "Tier: %s" tier + printfn "Expired: %s" expiresAt + printfn "%sTo renew: Visit https://unsandbox.com/keys/extend%s" yellow reset + exit 1 + else + printfn "%sValid%s" green reset + printfn "Public Key: %s" publicKey + printfn "Tier: %s" tier + printfn "Status: %s" status + printfn "Expires: %s" expiresAt + printfn "Time Remaining: %s" timeRemaining + printfn "Rate Limit: %s" rateLimit + printfn "Burst: %s" burst + printfn "Concurrency: %s" concurrency + with + | :? WebException as ex -> + printfn "%sInvalid%s" red reset + let errorMsg = + if ex.Response <> null then + use reader = new StreamReader(ex.Response.GetResponseStream()) + let body = reader.ReadToEnd() + try + let errorResult = parseJson body + match errorResult.TryFind "error" with + | Some err -> err.ToString() + | None -> body + with _ -> body + else + ex.Message + printfn "Reason: %s" errorMsg + exit 1 + let cmdService (args: Args) = let apiKey = getApiKey args.ApiKey @@ -406,6 +489,7 @@ let parseArgs (argv: string[]) = ServiceSleep = None ServiceWake = None ServiceDestroy = None + KeyExtend = false } let mutable i = 0 @@ -413,6 +497,7 @@ let parseArgs (argv: string[]) = match argv.[i] with | "session" -> args.Command <- Some "session" | "service" -> args.Command <- Some "service" + | "key" -> args.Command <- Some "key" | "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i] | "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i] | "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i] @@ -437,6 +522,7 @@ let parseArgs (argv: string[]) = | "--sleep" -> i <- i + 1; args.ServiceSleep <- Some argv.[i] | "--wake" -> i <- i + 1; args.ServiceWake <- Some argv.[i] | "--destroy" -> i <- i + 1; args.ServiceDestroy <- Some argv.[i] + | "--extend" -> args.KeyExtend <- true | arg when not (arg.StartsWith("-")) -> args.SourceFile <- Some arg | _ -> () i <- i + 1 @@ -447,6 +533,7 @@ let printHelp () = printfn "Usage: un [options] " printfn " un session [options]" printfn " un service [options]" + printfn " un key [options]" printfn "" printfn "Execute options:" printfn " -e KEY=VALUE Set environment variable" @@ -474,6 +561,10 @@ let printHelp () = printfn " --sleep ID Freeze service" printfn " --wake ID Unfreeze service" printfn " --destroy ID Destroy service" + printfn "" + printfn "Key options:" + printfn " --extend Open browser to extend key" + printfn " -k KEY API key to validate" [] let main argv = @@ -483,6 +574,7 @@ let main argv = match args.Command with | Some "session" -> cmdSession args; 0 | Some "service" -> cmdService args; 0 + | Some "key" -> cmdKey args; 0 | _ -> match args.SourceFile with | Some _ -> cmdExecute args; 0 diff --git a/un.go b/un.go index a97e351..bed2b0b 100644 --- a/un.go +++ b/un.go @@ -63,13 +63,13 @@ import ( ) const ( - APIBase = "https://api.unsandbox.com" - PortalBase = "https://unsandbox.com" - Blue = "\033[34m" - Red = "\033[31m" - Green = "\033[32m" - Yellow = "\033[33m" - Reset = "\033[0m" + APIBase = "https://api.unsandbox.com" + PortalBase = "https://unsandbox.com" + Blue = "\033[34m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Reset = "\033[0m" ) var extMap = map[string]string{ diff --git a/un.groovy b/un.groovy index c2040c7..e318b1d 100644 --- a/un.groovy +++ b/un.groovy @@ -54,6 +54,7 @@ def EXT_MAP = [ ] def API_BASE = 'https://api.unsandbox.com' +def PORTAL_BASE = 'https://unsandbox.com' def BLUE = '\033[34m' def RED = '\033[31m' def GREEN = '\033[32m' @@ -84,6 +85,7 @@ class Args { String serviceSleep = null String serviceWake = null String serviceDestroy = null + Boolean keyExtend = false } def getApiKey(argsKey) { @@ -274,6 +276,86 @@ def cmdSession(args) { println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}") } +def openBrowser(url) { + def osName = System.getProperty('os.name').toLowerCase() + try { + if (osName.contains('linux')) { + Runtime.runtime.exec(['xdg-open', url] as String[]) + } else if (osName.contains('mac')) { + Runtime.runtime.exec(['open', url] as String[]) + } else if (osName.contains('win')) { + Runtime.runtime.exec(['cmd', '/c', 'start', url] as String[]) + } + } catch (Exception e) { + System.err.println("${RED}Error opening browser: ${e.message}${RESET}") + } +} + +def cmdKey(args) { + def apiKey = getApiKey(args.apiKey) + + def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate", + '-H', 'Content-Type: application/json', + '-H', "Authorization: Bearer ${apiKey}", + '-d', '{}'] + + def proc = curlCmd.execute() + def output = proc.text + proc.waitFor() + + if (proc.exitValue() != 0) { + println("${RED}Invalid${RESET}") + System.err.println("${RED}Error: Failed to validate key${RESET}") + System.exit(1) + } + + def publicKeyMatch = output =~ /"public_key":"([^"]+)"/ + def tierMatch = output =~ /"tier":"([^"]+)"/ + def statusMatch = output =~ /"status":"([^"]+)"/ + def expiresAtMatch = output =~ /"expires_at":"([^"]+)"/ + def timeRemainingMatch = output =~ /"time_remaining":"([^"]+)"/ + def rateLimitMatch = output =~ /"rate_limit":([0-9.]+)/ + def burstMatch = output =~ /"burst":([0-9.]+)/ + def concurrencyMatch = output =~ /"concurrency":([0-9.]+)/ + def expiredMatch = output =~ /"expired":(true|false)/ + + def publicKey = publicKeyMatch.find() ? publicKeyMatch.group(1) : 'N/A' + def tier = tierMatch.find() ? tierMatch.group(1) : 'N/A' + def status = statusMatch.find() ? statusMatch.group(1) : 'N/A' + def expiresAt = expiresAtMatch.find() ? expiresAtMatch.group(1) : 'N/A' + def timeRemaining = timeRemainingMatch.find() ? timeRemainingMatch.group(1) : 'N/A' + def rateLimit = rateLimitMatch.find() ? rateLimitMatch.group(1) : 'N/A' + def burst = burstMatch.find() ? burstMatch.group(1) : 'N/A' + def concurrency = concurrencyMatch.find() ? concurrencyMatch.group(1) : 'N/A' + def expired = expiredMatch.find() ? expiredMatch.group(1) == 'true' : false + + if (args.keyExtend && publicKey != 'N/A') { + def extendUrl = "${PORTAL_BASE}/keys/extend?pk=${publicKey}" + println("${BLUE}Opening browser to extend key...${RESET}") + openBrowser(extendUrl) + return + } + + if (expired) { + println("${RED}Expired${RESET}") + println("Public Key: ${publicKey}") + println("Tier: ${tier}") + println("Expired: ${expiresAt}") + println("${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}") + System.exit(1) + } + + println("${GREEN}Valid${RESET}") + println("Public Key: ${publicKey}") + println("Tier: ${tier}") + println("Status: ${status}") + println("Expires: ${expiresAt}") + println("Time Remaining: ${timeRemaining}") + println("Rate Limit: ${rateLimit}") + println("Burst: ${burst}") + println("Concurrency: ${concurrency}") +} + def cmdService(args) { def apiKey = getApiKey(args.apiKey) @@ -378,6 +460,9 @@ def parseArgs(argv) { case 'service': args.command = 'service' break + case 'key': + args.command = 'key' + break case '-k': case '--api-key': args.apiKey = argv[++i] @@ -448,6 +533,9 @@ def parseArgs(argv) { case '--destroy': args.serviceDestroy = argv[++i] break + case '--extend': + args.keyExtend = true + break default: if (!argv[i].startsWith('-')) { args.sourceFile = argv[i] @@ -462,6 +550,7 @@ def printHelp() { println '''Usage: groovy un.groovy [options] groovy un.groovy session [options] groovy un.groovy service [options] + groovy un.groovy key [options] Execute options: -e KEY=VALUE Set environment variable @@ -489,6 +578,10 @@ Service options: --sleep ID Freeze service --wake ID Unfreeze service --destroy ID Destroy service + +Key options: + --extend Open browser to extend key + -k KEY API key to validate ''' } @@ -500,6 +593,8 @@ try { cmdSession(args) } else if (args.command == 'service') { cmdService(args) + } else if (args.command == 'key') { + cmdKey(args) } else if (args.sourceFile) { cmdExecute(args) } else { diff --git a/un.hs b/un.hs index 609f610..6b7048d 100644 --- a/un.hs +++ b/un.hs @@ -69,6 +69,9 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64 -- API constants +apiBase :: String +apiBase = "https://api.unsandbox.com" + portalBase :: String portalBase = "https://unsandbox.com" @@ -431,7 +434,7 @@ keyCommand opts = do validateKey :: String -> IO () validateKey apiKey = do let url = portalBase ++ "/keys/validate" - (exitCode, stdout, stderr) <- curlPost apiKey url "{}" + (exitCode, stdout, stderr) <- curlPostPortal apiKey url "{}" -- Check if valid:false appears in response let isInvalid = "\"valid\":false" `isPrefixOf` dropWhile (/= 'v') stdout @@ -516,7 +519,7 @@ validateKey apiKey = do extendKey :: String -> IO () extendKey apiKey = do let url = portalBase ++ "/keys/validate" - (exitCode, stdout, _) <- curlPost apiKey url "{}" + (exitCode, stdout, _) <- curlPostPortal apiKey url "{}" case extractJsonString stdout "public_key" of Nothing -> do @@ -532,3 +535,15 @@ extendKey apiKey = do ["-c", "xdg-open '" ++ extendUrl ++ "' 2>/dev/null || sensible-browser '" ++ extendUrl ++ "' 2>/dev/null || true"] "" return () + +-- HTTP helper for portal API +curlPostPortal :: String -> String -> String -> IO (ExitCode, String, String) +curlPostPortal apiKey url body = do + (exitCode, stdout, stderr) <- readProcessWithExitCode "curl" + [ "-s", "-X", "POST" + , url + , "-H", "Content-Type: application/json" + , "-H", "Authorization: Bearer " ++ apiKey + , "-d", body + ] "" + return (exitCode, stdout, stderr) diff --git a/un.js b/un.js index 585a0ca..eb513c6 100644 --- a/un.js +++ b/un.js @@ -224,28 +224,31 @@ async function validateKey(apiKey, shouldExtend = false) { try { const result = await portalRequest("/keys/validate", "POST", {}, apiKey); - if (result.error || result.status >= 400) { - console.log(`${RED}Invalid${RESET}`); - console.log(`Reason: ${result.error || 'Unknown error'}`); - process.exit(1); + // Handle --extend flag first + if (shouldExtend) { + const public_key = result.public_key; + if (public_key) { + const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${encodeURIComponent(public_key)}`; + console.log(`${BLUE}Opening browser to extend key...${RESET}`); + openBrowser(extendUrl); + return; + } else { + console.error(`${RED}Error: Could not retrieve public key${RESET}`); + process.exit(1); + } } + // Check if key is expired if (result.expired) { console.log(`${RED}Expired${RESET}`); console.log(`Public Key: ${result.public_key || 'N/A'}`); console.log(`Tier: ${result.tier || 'N/A'}`); console.log(`Expired: ${result.expires_at || 'N/A'}`); console.log(`${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}`); - - if (shouldExtend && result.public_key) { - const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${encodeURIComponent(result.public_key)}`; - console.log(`\nOpening browser to extend key...`); - openBrowser(extendUrl); - } - process.exit(1); } + // Valid key console.log(`${GREEN}Valid${RESET}`); console.log(`Public Key: ${result.public_key || 'N/A'}`); console.log(`Tier: ${result.tier || 'N/A'}`); @@ -255,12 +258,6 @@ async function validateKey(apiKey, shouldExtend = false) { console.log(`Rate Limit: ${result.rate_limit || 'N/A'}`); console.log(`Burst: ${result.burst || 'N/A'}`); console.log(`Concurrency: ${result.concurrency || 'N/A'}`); - - if (shouldExtend && result.public_key) { - const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${encodeURIComponent(result.public_key)}`; - console.log(`\nOpening browser to extend key...`); - openBrowser(extendUrl); - } } catch (error) { console.error(`${RED}Error validating key: ${error.message}${RESET}`); process.exit(1); diff --git a/un.lisp b/un.lisp index 2c7e1bf..8ae0073 100644 --- a/un.lisp +++ b/un.lisp @@ -242,6 +242,14 @@ (when public-key (format t "Public Key: ~a~%" public-key)) (when tier (format t "Tier: ~a~%" tier)) (when expires-at (format t "Expires: ~a~%" expires-at)) + (let ((time-remaining (parse-json-field response "time_remaining")) + (rate-limit (parse-json-field response "rate_limit")) + (burst (parse-json-field response "burst")) + (concurrency (parse-json-field response "concurrency"))) + (when time-remaining (format t "Time Remaining: ~a~%" time-remaining)) + (when rate-limit (format t "Rate Limit: ~a~%" rate-limit)) + (when burst (format t "Burst: ~a~%" burst)) + (when concurrency (format t "Concurrency: ~a~%" concurrency))) (when extend-flag (if public-key (let ((extend-url (format nil "~a/keys/extend?pk=~a" *portal-base* public-key))) diff --git a/un.m b/un.m index 39e03bd..a52bc83 100644 --- a/un.m +++ b/un.m @@ -43,6 +43,7 @@ #import static NSString* API_BASE = @"https://api.unsandbox.com"; +static NSString* PORTAL_BASE = @"https://unsandbox.com"; static NSString* BLUE = @"\033[34m"; static NSString* RED = @"\033[31m"; static NSString* GREEN = @"\033[32m"; @@ -263,6 +264,115 @@ void cmdExecute(NSArray* args) { exit(exitCode); } +NSDictionary* portalRequest(NSString* endpoint, NSString* method, NSDictionary* data, NSString* apiKey) { + NSString* urlString = [PORTAL_BASE stringByAppendingString:endpoint]; + NSURL* url = [NSURL URLWithString:urlString]; + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + [request setHTTPMethod:method]; + [request setValue:[@"Bearer " stringByAppendingString:apiKey] forHTTPHeaderField:@"Authorization"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setTimeoutInterval:30]; + + if (data) { + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error]; + if (error) { + fprintf(stderr, "%sError creating JSON: %s%s\n", + [RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]); + exit(1); + } + [request setHTTPBody:jsonData]; + } + + NSHTTPURLResponse* response = nil; + NSError* error = nil; + NSData* responseData = [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + + if (error || [response statusCode] >= 400) { + // For key validation, return the parsed JSON even on error + if (responseData) { + NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; + if (result) { + return result; + } + } + fprintf(stderr, "%sError: HTTP %ld%s\n", + [RED UTF8String], (long)[response statusCode], [RESET UTF8String]); + if (responseData) { + NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + fprintf(stderr, "%s\n", [errMsg UTF8String]); + } + exit(1); + } + + NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; + if (error) { + fprintf(stderr, "%sError parsing JSON: %s%s\n", + [RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]); + exit(1); + } + + return result; +} + +void openBrowser(NSString* url) { + NSString* command = [NSString stringWithFormat:@"open \"%@\"", url]; + system([command UTF8String]); +} + +void validateKey(NSString* apiKey, BOOL shouldExtend) { + NSDictionary* result = portalRequest(@"/keys/validate", @"POST", @{}, apiKey); + + if ([result[@"expired"] boolValue]) { + printf("%sExpired%s\n", [RED UTF8String], [RESET UTF8String]); + printf("Public Key: %s\n", [result[@"public_key"] UTF8String] ?: "N/A"); + printf("Tier: %s\n", [result[@"tier"] UTF8String] ?: "N/A"); + printf("Expired: %s\n", [result[@"expires_at"] UTF8String] ?: "N/A"); + printf("%sTo renew: Visit https://unsandbox.com/keys/extend%s\n", + [YELLOW UTF8String], [RESET UTF8String]); + + if (shouldExtend && result[@"public_key"]) { + NSString* extendUrl = [NSString stringWithFormat:@"%@/keys/extend?pk=%@", + PORTAL_BASE, result[@"public_key"]]; + printf("\n%sOpening browser to extend key...%s\n", [BLUE UTF8String], [RESET UTF8String]); + openBrowser(extendUrl); + } + exit(1); + } + + printf("%sValid%s\n", [GREEN UTF8String], [RESET UTF8String]); + printf("Public Key: %s\n", [result[@"public_key"] UTF8String] ?: "N/A"); + printf("Tier: %s\n", [result[@"tier"] UTF8String] ?: "N/A"); + printf("Status: %s\n", [result[@"status"] UTF8String] ?: "N/A"); + printf("Expires: %s\n", [result[@"expires_at"] UTF8String] ?: "N/A"); + printf("Time Remaining: %s\n", [result[@"time_remaining"] UTF8String] ?: "N/A"); + printf("Rate Limit: %s\n", [result[@"rate_limit"] UTF8String] ?: "N/A"); + printf("Burst: %s\n", [result[@"burst"] UTF8String] ?: "N/A"); + printf("Concurrency: %s\n", [result[@"concurrency"] UTF8String] ?: "N/A"); + + if (shouldExtend && result[@"public_key"]) { + NSString* extendUrl = [NSString stringWithFormat:@"%@/keys/extend?pk=%@", + PORTAL_BASE, result[@"public_key"]]; + printf("\n%sOpening browser to extend key...%s\n", [BLUE UTF8String], [RESET UTF8String]); + openBrowser(extendUrl); + } +} + +void cmdKey(NSArray* args) { + NSString* apiKey = getApiKey(); + BOOL shouldExtend = NO; + + for (NSString* arg in args) { + if ([arg isEqualToString:@"--extend"]) { + shouldExtend = YES; + } + } + + validateKey(apiKey, shouldExtend); +} + void cmdSession(NSArray* args) { NSString* apiKey = getApiKey(); BOOL listMode = NO; @@ -481,6 +591,7 @@ int main(int argc, const char* argv[]) { fprintf(stderr, "Usage: un.m [options] \n"); fprintf(stderr, " un.m session [options]\n"); fprintf(stderr, " un.m service [options]\n"); + fprintf(stderr, " un.m key [options]\n"); return 1; } @@ -495,6 +606,8 @@ int main(int argc, const char* argv[]) { cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); } else if ([firstArg isEqualToString:@"service"]) { cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); + } else if ([firstArg isEqualToString:@"key"]) { + cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]); } else { cmdExecute(args); } diff --git a/un.ml b/un.ml index 15af305..a87aae6 100755 --- a/un.ml +++ b/un.ml @@ -276,24 +276,34 @@ let display_key_info response extend = let status = extract_json_value response "status" in let public_key = extract_json_value response "public_key" in let tier = extract_json_value response "tier" in - let expires_at = extract_json_value response "expires_at" in + let valid_through = extract_json_value response "valid_through_datetime" in + let valid_for = extract_json_value response "valid_for_human" in + let rate_limit = extract_json_value response "rate_per_minute" in + let burst = extract_json_value response "burst" in + let concurrency = extract_json_value response "concurrency" in + let expired_at = extract_json_value response "expired_at_datetime" in match status with | Some "valid" -> - Printf.printf "%sValid%s\n" green reset; - (match public_key with Some pk -> Printf.printf "Public Key: %s\n" pk | None -> ()); - (match tier with Some t -> Printf.printf "Tier: %s\n" t | None -> ()); - (match expires_at with Some exp -> Printf.printf "Expires: %s\n" exp | None -> ()); + Printf.printf "%sValid%s\n\n" green reset; + (match public_key with Some pk -> Printf.printf "Public Key: %s\n" pk | None -> ()); + (match tier with Some t -> Printf.printf "Tier: %s\n" t | None -> ()); + Printf.printf "Status: valid\n"; + (match valid_through with Some exp -> Printf.printf "Expires: %s\n" exp | None -> ()); + (match valid_for with Some vf -> Printf.printf "Time Remaining: %s\n" vf | None -> ()); + (match rate_limit with Some r -> Printf.printf "Rate Limit: %s/min\n" r | None -> ()); + (match burst with Some b -> Printf.printf "Burst: %s\n" b | None -> ()); + (match concurrency with Some c -> Printf.printf "Concurrency: %s\n" c | None -> ()); if extend then (match public_key with | Some pk -> open_browser (Printf.sprintf "%s/keys/extend?pk=%s" portal_base pk) | None -> ()) | Some "expired" -> - Printf.printf "%sExpired%s\n" red reset; - (match public_key with Some pk -> Printf.printf "Public Key: %s\n" pk | None -> ()); - (match tier with Some t -> Printf.printf "Tier: %s\n" t | None -> ()); - (match expires_at with Some exp -> Printf.printf "Expired: %s\n" exp | None -> ()); - Printf.printf "%sTo renew: Visit %s/keys/extend%s\n" yellow portal_base reset; + Printf.printf "%sExpired%s\n\n" red reset; + (match public_key with Some pk -> Printf.printf "Public Key: %s\n" pk | None -> ()); + (match tier with Some t -> Printf.printf "Tier: %s\n" t | None -> ()); + (match expired_at with Some exp -> Printf.printf "Expired: %s\n" exp | None -> ()); + Printf.printf "\n%sTo renew:%s Visit %s/keys/extend\n" yellow reset portal_base; if extend then (match public_key with | Some pk -> open_browser (Printf.sprintf "%s/keys/extend?pk=%s" portal_base pk) diff --git a/un.pl b/un.pl index 8492861..be1ead4 100644 --- a/un.pl +++ b/un.pl @@ -362,9 +362,22 @@ sub cmd_service { exit 1; } -sub cmd_key { - my ($options) = @_; - my $api_key = get_api_key($options->{api_key}); +sub open_browser { + my ($url) = @_; + + # Try different browser open commands based on platform + if ($^O eq 'darwin') { + system('open', $url); + } elsif ($^O eq 'MSWin32') { + system('start', $url); + } else { + # Linux/Unix + system('xdg-open', $url, '>/dev/null', '2>&1', '&'); + } +} + +sub validate_key { + my ($api_key, $should_extend) = @_; # Call /keys/validate endpoint my $url = "$PORTAL_BASE/keys/validate"; @@ -374,47 +387,48 @@ sub cmd_key { $request->header('Content-Type' => 'application/json'); my $response = $ua->request($request); - - unless ($response->is_success) { - print STDERR "${RED}Error: HTTP ", $response->code, " - ", $response->content, "${RESET}\n"; - exit 1; - } - my $result = decode_json($response->content); - # Handle different states - my $status = $result->{status} || 'unknown'; + # Handle --extend flag first + if ($should_extend) { + my $public_key = $result->{public_key}; + if ($public_key) { + my $extend_url = "$PORTAL_BASE/keys/extend?pk=$public_key"; + print "${BLUE}Opening browser to extend key...${RESET}\n"; + open_browser($extend_url); + return; + } else { + print STDERR "${RED}Error: Could not retrieve public key${RESET}\n"; + exit 1; + } + } - if ($status eq 'valid') { - print "${GREEN}Valid${RESET}\n"; - print "Public Key: ", ($result->{public_key} // 'N/A'), "\n"; - print "Tier: ", ($result->{tier} // 'N/A'), "\n"; - print "Expires: ", ($result->{expires_at} // 'N/A'), "\n"; - } elsif ($status eq 'expired') { + # Check if key is expired + if ($result->{expired}) { print "${RED}Expired${RESET}\n"; print "Public Key: ", ($result->{public_key} // 'N/A'), "\n"; print "Tier: ", ($result->{tier} // 'N/A'), "\n"; - print "Expired: ", ($result->{expired_at} // 'N/A'), "\n"; + print "Expired: ", ($result->{expires_at} // 'N/A'), "\n"; print "${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}\n"; - - # Handle --extend flag for expired keys - if ($options->{extend} && $result->{public_key}) { - my $extend_url = "$PORTAL_BASE/keys/extend?pk=$result->{public_key}"; - print "\n${BLUE}Opening browser to: $extend_url${RESET}\n"; - system("xdg-open", $extend_url) if -x "/usr/bin/xdg-open"; - } - } elsif ($status eq 'invalid') { - print "${RED}Invalid${RESET}\n"; - } else { - print "${YELLOW}Unknown status: $status${RESET}\n"; + exit 1; } - # Handle --extend flag for valid keys - if ($options->{extend} && $status eq 'valid' && $result->{public_key}) { - my $extend_url = "$PORTAL_BASE/keys/extend?pk=$result->{public_key}"; - print "\n${BLUE}Opening browser to: $extend_url${RESET}\n"; - system("xdg-open", $extend_url) if -x "/usr/bin/xdg-open"; - } + # Valid key + print "${GREEN}Valid${RESET}\n"; + print "Public Key: ", ($result->{public_key} // 'N/A'), "\n"; + print "Tier: ", ($result->{tier} // 'N/A'), "\n"; + print "Status: ", ($result->{status} // 'N/A'), "\n"; + print "Expires: ", ($result->{expires_at} // 'N/A'), "\n"; + print "Time Remaining: ", ($result->{time_remaining} // 'N/A'), "\n"; + print "Rate Limit: ", ($result->{rate_limit} // 'N/A'), "\n"; + print "Burst: ", ($result->{burst} // 'N/A'), "\n"; + print "Concurrency: ", ($result->{concurrency} // 'N/A'), "\n"; +} + +sub cmd_key { + my ($options) = @_; + my $api_key = get_api_key($options->{api_key}); + validate_key($api_key, $options->{extend}); } sub main { diff --git a/un.pro b/un.pro index dfe3eb4..71481ea 100644 --- a/un.pro +++ b/un.pro @@ -39,6 +39,9 @@ :- initialization(main, main). +% Constants +portal_base('https://unsandbox.com'). + % Extension to language mapping ext_lang('.jl', 'julia'). ext_lang('.r', 'r'). @@ -198,6 +201,26 @@ service_create(Name, Ports, Bootstrap, ServiceType) :- [ApiKey, Json]), shell(Cmd, 0). +% Key validate +validate_key(Extend) :- + get_api_key(ApiKey), + portal_base(PortalBase), + ( Extend = true + -> % Build command for --extend mode + format(atom(Cmd), + 'RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null', + [PortalBase, ApiKey, PortalBase]) + ; % Build command for normal validation + format(atom(Cmd), + 'curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json', + [PortalBase, ApiKey]) + ), + shell(Cmd, 0). + +% Handle key subcommand +handle_key(['--extend'|_]) :- validate_key(true). +handle_key(_) :- validate_key(false). + % Handle session subcommand handle_session(['--list'|_]) :- session_list. handle_session(['-l'|_]) :- session_list. @@ -261,6 +284,8 @@ main(Argv) :- -> handle_session(Rest) ; Argv = ['service'|Rest] -> handle_service(Rest) + ; Argv = ['key'|Rest] + -> handle_key(Rest) ; Argv = [Filename|_] -> execute_file(Filename) ; write(user_error, 'Error: Invalid arguments\n'), diff --git a/un.ps1 b/un.ps1 index dff35f4..e1151de 100644 --- a/un.ps1 +++ b/un.ps1 @@ -43,6 +43,7 @@ # pwsh un.ps1 service [options] $API_BASE = "https://api.unsandbox.com" +$PORTAL_BASE = "https://unsandbox.com" $EXT_MAP = @{ ".ps1" = "powershell"; ".py" = "python"; ".js" = "javascript" @@ -68,7 +69,7 @@ function Get-ApiKey { } function Invoke-Api { - param($Endpoint, $Method = "GET", $Body = $null) + param($Endpoint, $Method = "GET", $Body = $null, $BaseUrl = $null) $apiKey = Get-ApiKey $headers = @{ @@ -76,7 +77,8 @@ function Invoke-Api { "Content-Type" = "application/json" } - $uri = "$API_BASE$Endpoint" + $base = if ($BaseUrl) { $BaseUrl } else { $API_BASE } + $uri = "$base$Endpoint" try { if ($Body) { @@ -164,6 +166,63 @@ function Invoke-Session { $result | ConvertTo-Json -Depth 5 } +function Invoke-Key { + param($Args) + + $extend = $Args -contains "--extend" + + try { + $result = Invoke-Api -Endpoint "/keys/validate" -Method "POST" -BaseUrl $PORTAL_BASE + + # Handle --extend flag + if ($extend) { + $publicKey = $result.public_key + if ($publicKey) { + $url = "$PORTAL_BASE/keys/extend?pk=$publicKey" + Write-Host "`e[34mOpening browser to extend key...`e[0m" + if ($IsWindows) { + Start-Process $url + } elseif ($IsMacOS) { + & open $url + } elseif ($IsLinux) { + & xdg-open $url + } else { + Write-Host "`e[33mPlease open manually: $url`e[0m" + } + return + } else { + Write-Error "Error: Could not retrieve public key" + exit 1 + } + } + + # Check if key is expired + if ($result.expired) { + Write-Host "`e[31mExpired`e[0m" + Write-Host "Public Key: $($result.public_key ?? 'N/A')" + Write-Host "Tier: $($result.tier ?? 'N/A')" + Write-Host "Expired: $($result.expires_at ?? 'N/A')" + Write-Host "`e[33mTo renew: Visit $PORTAL_BASE/keys/extend`e[0m" + exit 1 + } + + # Valid key + Write-Host "`e[32mValid`e[0m" + Write-Host "Public Key: $($result.public_key ?? 'N/A')" + Write-Host "Tier: $($result.tier ?? 'N/A')" + Write-Host "Status: $($result.status ?? 'N/A')" + Write-Host "Expires: $($result.expires_at ?? 'N/A')" + Write-Host "Time Remaining: $($result.time_remaining ?? 'N/A')" + Write-Host "Rate Limit: $($result.rate_limit ?? 'N/A')" + Write-Host "Burst: $($result.burst ?? 'N/A')" + Write-Host "Concurrency: $($result.concurrency ?? 'N/A')" + } catch { + Write-Host "`e[31mInvalid`e[0m" + Write-Host "Reason: $($_.Exception.Message)" + exit 1 + } +} + function Invoke-Service { param($Args) @@ -253,6 +312,7 @@ if ($args.Count -eq 0 -or $args[0] -eq "--help" -or $args[0] -eq "-h") { Usage: pwsh un.ps1 [options] pwsh un.ps1 session [options] pwsh un.ps1 service [options] + pwsh un.ps1 key [options] Execute options: -e KEY=VALUE Environment variable @@ -274,6 +334,9 @@ Service options: --sleep ID Freeze service --wake ID Unfreeze service --destroy ID Destroy service + +Key options: + --extend Open browser to extend key "@ exit 0 } @@ -282,6 +345,8 @@ if ($args[0] -eq "session") { Invoke-Session -Args $args[1..($args.Count-1)] } elseif ($args[0] -eq "service") { Invoke-Service -Args $args[1..($args.Count-1)] +} elseif ($args[0] -eq "key") { + Invoke-Key -Args $args[1..($args.Count-1)] } else { # Parse execute args $sourceFile = $null diff --git a/un.raku b/un.raku index 4e43900..c376f0c 100644 --- a/un.raku +++ b/un.raku @@ -42,6 +42,7 @@ use JSON::Fast; constant $API_BASE = "https://api.unsandbox.com"; +constant $PORTAL_BASE = "https://unsandbox.com"; constant $BLUE = "\e[34m"; constant $RED = "\e[31m"; constant $GREEN = "\e[32m"; @@ -462,11 +463,81 @@ sub cmd-service(@args) { exit 1; } +sub validate-key(Bool $extend) { + my $api-key = get-api-key(); + + # Build curl command + my @args = 'curl', '-s', '-X', 'POST'; + @args.append: "$PORTAL_BASE/keys/validate"; + @args.append: '-H', 'Content-Type: application/json'; + @args.append: '-H', "Authorization: Bearer $api-key"; + + my $proc = run |@args, :out, :err; + my $body = $proc.out.slurp; + my $err-msg = $proc.err.slurp; + + if $proc.exitcode != 0 { + say "{$RED}Invalid{$RESET}"; + note "Reason: $err-msg" if $err-msg; + exit 1; + } + + my %result = from-json($body); + + # Handle --extend flag + if $extend { + my $public-key = %result; + if $public-key { + say "{$BLUE}Opening browser to extend key...{$RESET}"; + run 'xdg-open', "$PORTAL_BASE/keys/extend?pk=$public-key"; + return; + } else { + note "{$RED}Error: Could not retrieve public key{$RESET}"; + exit 1; + } + } + + # Check if key is expired + if %result { + say "{$RED}Expired{$RESET}"; + say "Public Key: {%result // 'N/A'}"; + say "Tier: {%result // 'N/A'}"; + say "Expired: {%result // 'N/A'}"; + say "{$YELLOW}To renew: Visit https://unsandbox.com/keys/extend{$RESET}"; + exit 1; + } + + # Valid key + say "{$GREEN}Valid{$RESET}"; + say "Public Key: {%result // 'N/A'}"; + say "Tier: {%result // 'N/A'}"; + say "Status: {%result // 'N/A'}"; + say "Expires: {%result // 'N/A'}"; + say "Time Remaining: {%result // 'N/A'}"; + say "Rate Limit: {%result // 'N/A'}"; + say "Burst: {%result // 'N/A'}"; + say "Concurrency: {%result // 'N/A'}"; +} + +sub cmd-key(@args) { + my $extend = False; + + # Parse arguments + for @args -> $arg { + if $arg eq '--extend' { + $extend = True; + } + } + + validate-key($extend); +} + sub MAIN(*@args) { unless @args { note "Usage: un.raku [options] "; note " un.raku session [options]"; note " un.raku service [options]"; + note " un.raku key [options]"; exit 1; } @@ -477,6 +548,9 @@ sub MAIN(*@args) { when 'service' { cmd-service(@args[1..*]); } + when 'key' { + cmd-key(@args[1..*]); + } default { cmd-execute(@args); } diff --git a/un.scm b/un.scm index eb513f7..fa790f8 100644 --- a/un.scm +++ b/un.scm @@ -181,33 +181,43 @@ (status (json-extract-string response "status")) (public-key (json-extract-string response "public_key")) (tier (json-extract-string response "tier")) - (expires-at (json-extract-string response "expires_at"))) + (valid-through (json-extract-string response "valid_through_datetime")) + (valid-for (json-extract-string response "valid_for_human")) + (rate-limit (json-extract-string response "rate_per_minute")) + (burst (json-extract-string response "burst")) + (concurrency (json-extract-string response "concurrency")) + (expired-at (json-extract-string response "expired_at_datetime"))) (cond ;; Valid key ((and status (string=? status "valid")) - (format #t "~aValid~a\n" green reset) - (when public-key (format #t "Public Key: ~a\n" public-key)) - (when tier (format #t "Tier: ~a\n" tier)) - (when expires-at (format #t "Expires: ~a\n" expires-at)) + (format #t "~aValid~a\n\n" green reset) + (when public-key (format #t "Public Key: ~a\n" public-key)) + (when tier (format #t "Tier: ~a\n" tier)) + (format #t "Status: valid\n") + (when valid-through (format #t "Expires: ~a\n" valid-through)) + (when valid-for (format #t "Time Remaining: ~a\n" valid-for)) + (when rate-limit (format #t "Rate Limit: ~a/min\n" rate-limit)) + (when burst (format #t "Burst: ~a\n" burst)) + (when concurrency (format #t "Concurrency: ~a\n" concurrency)) (when extend (if public-key (let ((url (format #f "~a/keys/extend?pk=~a" portal-base public-key))) - (format #t "~aOpening browser to extend key...~a\n" yellow reset) + (format #t "~aOpening browser to extend key...~a\n" blue reset) (open-browser url)) (format #t "~aError: No public_key in response~a\n" red reset)))) ;; Expired key ((and status (string=? status "expired")) - (format #t "~aExpired~a\n" red reset) - (when public-key (format #t "Public Key: ~a\n" public-key)) - (when tier (format #t "Tier: ~a\n" tier)) - (when expires-at (format #t "Expired: ~a\n" expires-at)) - (format #t "~aTo renew: Visit ~a/keys/extend~a\n" yellow portal-base reset) + (format #t "~aExpired~a\n\n" red reset) + (when public-key (format #t "Public Key: ~a\n" public-key)) + (when tier (format #t "Tier: ~a\n" tier)) + (when expired-at (format #t "Expired: ~a\n" expired-at)) + (format #t "\n~aTo renew:~a Visit ~a/keys/extend\n" yellow reset portal-base) (when extend (if public-key (let ((url (format #f "~a/keys/extend?pk=~a" portal-base public-key))) - (format #t "~aOpening browser...~a\n" yellow reset) + (format #t "~aOpening browser...~a\n" blue reset) (open-browser url)) (format #t "~aError: No public_key in response~a\n" red reset)))) diff --git a/un.ts b/un.ts index 49bb8f1..f5a0060 100644 --- a/un.ts +++ b/un.ts @@ -186,6 +186,44 @@ function apiRequest(endpoint: string, method: string = "GET", data: any = null, }); } +function portalRequest(endpoint: string, method: string = "GET", data: any = null, apiKey: string): Promise { + return new Promise((resolve, reject) => { + const url = new URL(PORTAL_BASE + endpoint); + const options: https.RequestOptions = { + hostname: url.hostname, + path: url.pathname + url.search, + method: method, + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + timeout: 30000 + }; + + const req = https.request(options, (res) => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(body); + resolve(parsed); + } catch (e) { + resolve({ error: body, status: res.statusCode }); + } + }); + }); + + req.on('error', (e) => { + reject(e); + }); + + if (data) { + req.write(JSON.stringify(data)); + } + req.end(); + }); +} + async function cmdExecute(args: Args): Promise { const apiKey = getApiKey(args.apiKey); @@ -381,42 +419,76 @@ async function cmdService(args: Args): Promise { process.exit(1); } -async function cmdKey(args: Args): Promise { - const apiKey = getApiKey(args.apiKey); +function openBrowser(url: string): void { + const { exec } = require('child_process'); + const platform = process.platform; + let command: string; - // Validate the key - const result = await apiRequest("/keys/validate", "POST", {}, apiKey); + if (platform === 'darwin') { + command = `open "${url}"`; + } else if (platform === 'win32') { + command = `start "${url}"`; + } else { + command = `xdg-open "${url}"`; + } - if (result.status === "valid") { + exec(command, (error: any) => { + if (error) { + console.error(`${RED}Error opening browser: ${error.message}${RESET}`); + console.log(`Please visit: ${url}`); + } + }); +} + +async function validateKey(apiKey: string, shouldExtend: boolean): Promise { + try { + const result = await portalRequest("/keys/validate", "POST", {}, apiKey); + + // Handle --extend flag first + if (shouldExtend) { + const public_key = result.public_key; + if (public_key) { + const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${encodeURIComponent(public_key)}`; + console.log(`${BLUE}Opening browser to extend key...${RESET}`); + openBrowser(extendUrl); + return; + } else { + console.error(`${RED}Error: Could not retrieve public key${RESET}`); + process.exit(1); + } + } + + // Check if key is expired + if (result.expired) { + console.log(`${RED}Expired${RESET}`); + console.log(`Public Key: ${result.public_key || 'N/A'}`); + console.log(`Tier: ${result.tier || 'N/A'}`); + console.log(`Expired: ${result.expires_at || 'N/A'}`); + console.log(`${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}`); + process.exit(1); + } + + // Valid key console.log(`${GREEN}Valid${RESET}`); console.log(`Public Key: ${result.public_key || 'N/A'}`); console.log(`Tier: ${result.tier || 'N/A'}`); + console.log(`Status: ${result.status || 'N/A'}`); console.log(`Expires: ${result.expires_at || 'N/A'}`); - - // Handle --extend flag - if (args.extend && result.public_key) { - const extendUrl = `${PORTAL_BASE}/keys/extend?pk=${result.public_key}`; - console.log(`\n${BLUE}Opening: ${extendUrl}${RESET}`); - - // Try to open browser using common commands - const { exec } = require('child_process'); - exec(`xdg-open "${extendUrl}" || open "${extendUrl}" || start "${extendUrl}"`, (error: any) => { - if (error) { - console.error(`${YELLOW}Could not open browser automatically. Visit: ${extendUrl}${RESET}`); - } - }); - } - } else if (result.status === "expired") { - console.log(`${RED}Expired${RESET}`); - console.log(`Public Key: ${result.public_key || 'N/A'}`); - console.log(`Tier: ${result.tier || 'N/A'}`); - console.log(`Expired: ${result.expires_at || 'N/A'}`); - console.log(`${YELLOW}To renew: Visit ${PORTAL_BASE}/keys/extend${RESET}`); - } else { - console.log(`${RED}Invalid${RESET}`); + console.log(`Time Remaining: ${result.time_remaining || 'N/A'}`); + console.log(`Rate Limit: ${result.rate_limit || 'N/A'}`); + console.log(`Burst: ${result.burst || 'N/A'}`); + console.log(`Concurrency: ${result.concurrency || 'N/A'}`); + } catch (error: any) { + console.error(`${RED}Error validating key: ${error.message}${RESET}`); + process.exit(1); } } +async function cmdKey(args: Args): Promise { + const apiKey = getApiKey(args.apiKey); + await validateKey(apiKey, args.extend); +} + function parseArgs(argv: string[]): Args { const args: Args = { command: null, diff --git a/un.v b/un.v index 5994266..1107040 100644 --- a/un.v +++ b/un.v @@ -126,17 +126,22 @@ fn extract_json_string(json string, key string) string { } fn cmd_key(extend bool, api_key string) { - cmd := "curl -s -X POST '${api_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{}'" + cmd := "curl -s -X POST '${portal_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{}'" result := exec_curl(cmd) - status := extract_json_string(result, 'status') public_key := extract_json_string(result, 'public_key') tier := extract_json_string(result, 'tier') - expired_at := extract_json_string(result, 'expired_at') + status := extract_json_string(result, 'status') + expires_at := extract_json_string(result, 'expires_at') + time_remaining := extract_json_string(result, 'time_remaining') + rate_limit := extract_json_string(result, 'rate_limit') + burst := extract_json_string(result, 'burst') + concurrency := extract_json_string(result, 'concurrency') + expired := extract_json_string(result, 'expired') if extend && public_key != '' { url := '${portal_base}/keys/extend?pk=${public_key}' - println('${yellow}Opening browser: ${url}${reset}') + println('${blue}Opening browser to extend key...${reset}') // Try xdg-open (Linux), open (macOS), or start (Windows) os.execute('xdg-open "${url}"') or { @@ -149,30 +154,40 @@ fn cmd_key(extend bool, api_key string) { return } - match status { - 'valid' { - println('${green}Valid${reset}') - println('Public Key: ${public_key}') - println('Tier: ${tier}') - if expired_at != '' { - println('Expires: ${expired_at}') - } - } - 'expired' { - println('${red}Expired${reset}') - println('Public Key: ${public_key}') - println('Tier: ${tier}') - if expired_at != '' { - println('Expired: ${expired_at}') - } - println('${yellow}To renew: Visit ${portal_base}/keys/extend${reset}') - } - 'invalid' { - println('${red}Invalid${reset}') - } - else { - println('${yellow}Unknown status: ${status}${reset}') + if expired == 'true' { + println('${red}Expired${reset}') + println('Public Key: ${public_key}') + println('Tier: ${tier}') + if expires_at != '' { + println('Expired: ${expires_at}') } + println('${yellow}To renew: Visit https://unsandbox.com/keys/extend${reset}') + exit(1) + } + + // Valid key + println('${green}Valid${reset}') + println('Public Key: ${public_key}') + if tier != '' { + println('Tier: ${tier}') + } + if status != '' { + println('Status: ${status}') + } + if expires_at != '' { + println('Expires: ${expires_at}') + } + if time_remaining != '' { + println('Time Remaining: ${time_remaining}') + } + if rate_limit != '' { + println('Rate Limit: ${rate_limit}') + } + if burst != '' { + println('Burst: ${burst}') + } + if concurrency != '' { + println('Concurrency: ${concurrency}') } } diff --git a/un_deno.ts b/un_deno.ts index 37469e6..d95a6c2 100644 --- a/un_deno.ts +++ b/un_deno.ts @@ -39,6 +39,7 @@ // Full-featured CLI matching un.c/un.py capabilities const API_BASE = "https://api.unsandbox.com"; +const PORTAL_BASE = "https://unsandbox.com"; const BLUE = "\x1b[34m"; const RED = "\x1b[31m"; const GREEN = "\x1b[32m"; @@ -92,8 +93,10 @@ async function apiRequest( method: string, data?: unknown, apiKey?: string, + baseUrl?: string, ): Promise { - const url = `${API_BASE}${endpoint}`; + const base = baseUrl || API_BASE; + const url = `${base}${endpoint}`; const headers: Record = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", @@ -328,6 +331,69 @@ async function cmdSession(args: string[]) { ); } +async function cmdKey(args: string[]) { + const apiKey = getApiKey(); + let extend = false; + + // Parse arguments + for (let i = 0; i < args.length; i++) { + if (args[i] === "--extend") { + extend = true; + } + } + + try { + const result = await apiRequest("/keys/validate", "POST", undefined, apiKey, PORTAL_BASE); + + // Handle --extend flag + if (extend) { + const publicKey = result.public_key; + if (publicKey) { + const url = `${PORTAL_BASE}/keys/extend?pk=${publicKey}`; + console.log(`${BLUE}Opening browser to extend key...${RESET}`); + if (Deno.build.os === "darwin") { + await new Deno.Command("open", { args: [url] }).output(); + } else if (Deno.build.os === "linux") { + await new Deno.Command("xdg-open", { args: [url] }).output(); + } else if (Deno.build.os === "windows") { + await new Deno.Command("cmd", { args: ["/c", "start", url] }).output(); + } else { + console.log(`${YELLOW}Please open manually: ${url}${RESET}`); + } + return; + } else { + console.error(`${RED}Error: Could not retrieve public key${RESET}`); + Deno.exit(1); + } + } + + // Check if key is expired + if (result.expired) { + console.log(`${RED}Expired${RESET}`); + console.log(`Public Key: ${result.public_key || "N/A"}`); + console.log(`Tier: ${result.tier || "N/A"}`); + console.log(`Expired: ${result.expires_at || "N/A"}`); + console.log(`${YELLOW}To renew: Visit ${PORTAL_BASE}/keys/extend${RESET}`); + Deno.exit(1); + } + + // Valid key + console.log(`${GREEN}Valid${RESET}`); + console.log(`Public Key: ${result.public_key || "N/A"}`); + console.log(`Tier: ${result.tier || "N/A"}`); + console.log(`Status: ${result.status || "N/A"}`); + console.log(`Expires: ${result.expires_at || "N/A"}`); + console.log(`Time Remaining: ${result.time_remaining || "N/A"}`); + console.log(`Rate Limit: ${result.rate_limit || "N/A"}`); + console.log(`Burst: ${result.burst || "N/A"}`); + console.log(`Concurrency: ${result.concurrency || "N/A"}`); + } catch (e) { + console.log(`${RED}Invalid${RESET}`); + console.log(`Reason: ${e}`); + Deno.exit(1); + } +} + async function cmdService(args: string[]) { const apiKey = getApiKey(); let listMode = false; @@ -509,6 +575,7 @@ async function main() { console.error("Usage: un_deno.ts [options] "); console.error(" un_deno.ts session [options]"); console.error(" un_deno.ts service [options]"); + console.error(" un_deno.ts key [options]"); Deno.exit(1); } @@ -518,6 +585,8 @@ async function main() { await cmdSession(args.slice(1)); } else if (firstArg === "service") { await cmdService(args.slice(1)); + } else if (firstArg === "key") { + await cmdKey(args.slice(1)); } else { await cmdExecute(args); } diff --git a/un_inception.c b/un_inception.c index c86cc05..91ac8bd 100644 --- a/un_inception.c +++ b/un_inception.c @@ -50,6 +50,7 @@ #include #define API_BASE "https://api.unsandbox.com" +#define PORTAL_BASE "https://unsandbox.com" #define BLUE "\033[34m" #define RED "\033[31m" #define GREEN "\033[32m" @@ -312,6 +313,142 @@ void cmd_session(int list, const char *kill, const char *shell, const char *netw printf("\n%sSession created%s\n", GREEN, RESET); } +void cmd_key(int extend, const char *api_key) { + char cmd[4096]; + + snprintf(cmd, sizeof(cmd), + "curl -s -X POST '%s/keys/validate' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer %s'", + PORTAL_BASE, api_key); + + FILE *curl = popen(cmd, "r"); + if (!curl) { + fprintf(stderr, "%sError running curl%s\n", RED, RESET); + exit(1); + } + + char response[16384]; + size_t resp_len = fread(response, 1, sizeof(response) - 1, curl); + response[resp_len] = 0; + pclose(curl); + + // Parse JSON response (simple string matching) + char *public_key_start = strstr(response, "\"public_key\":\""); + char public_key[256] = ""; + if (public_key_start) { + public_key_start += 14; + char *end = strchr(public_key_start, '"'); + if (end) { + size_t len = end - public_key_start; + if (len < sizeof(public_key)) { + strncpy(public_key, public_key_start, len); + public_key[len] = 0; + } + } + } + + // Handle --extend flag + if (extend) { + if (public_key[0]) { + printf("%sOpening browser to extend key...%s\n", BLUE, RESET); + char open_cmd[512]; + #ifdef __APPLE__ + snprintf(open_cmd, sizeof(open_cmd), "open '%s/keys/extend?pk=%s'", PORTAL_BASE, public_key); + #elif __linux__ + snprintf(open_cmd, sizeof(open_cmd), "xdg-open '%s/keys/extend?pk=%s'", PORTAL_BASE, public_key); + #else + snprintf(open_cmd, sizeof(open_cmd), "start '%s/keys/extend?pk=%s'", PORTAL_BASE, public_key); + #endif + system(open_cmd); + return; + } else { + fprintf(stderr, "%sError: Could not retrieve public key%s\n", RED, RESET); + exit(1); + } + } + + // Check if expired + if (strstr(response, "\"expired\":true")) { + printf("%sExpired%s\n", RED, RESET); + printf("Public Key: %s\n", public_key[0] ? public_key : "N/A"); + + char *tier_start = strstr(response, "\"tier\":\""); + if (tier_start) { + tier_start += 8; + char *end = strchr(tier_start, '"'); + if (end) printf("Tier: %.*s\n", (int)(end - tier_start), tier_start); + } + + char *expires_start = strstr(response, "\"expires_at\":\""); + if (expires_start) { + expires_start += 14; + char *end = strchr(expires_start, '"'); + if (end) printf("Expired: %.*s\n", (int)(end - expires_start), expires_start); + } + + printf("%sTo renew: Visit %s/keys/extend%s\n", YELLOW, PORTAL_BASE, RESET); + exit(1); + } + + // Valid key + printf("%sValid%s\n", GREEN, RESET); + printf("Public Key: %s\n", public_key[0] ? public_key : "N/A"); + + // Extract and print other fields + char *tier_start = strstr(response, "\"tier\":\""); + if (tier_start) { + tier_start += 8; + char *end = strchr(tier_start, '"'); + if (end) printf("Tier: %.*s\n", (int)(end - tier_start), tier_start); + } + + char *status_start = strstr(response, "\"status\":\""); + if (status_start) { + status_start += 10; + char *end = strchr(status_start, '"'); + if (end) printf("Status: %.*s\n", (int)(end - status_start), status_start); + } + + char *expires_start = strstr(response, "\"expires_at\":\""); + if (expires_start) { + expires_start += 14; + char *end = strchr(expires_start, '"'); + if (end) printf("Expires: %.*s\n", (int)(end - expires_start), expires_start); + } + + char *time_rem_start = strstr(response, "\"time_remaining\":\""); + if (time_rem_start) { + time_rem_start += 18; + char *end = strchr(time_rem_start, '"'); + if (end) printf("Time Remaining: %.*s\n", (int)(end - time_rem_start), time_rem_start); + } + + char *rate_start = strstr(response, "\"rate_limit\":"); + if (rate_start) { + rate_start += 13; + char *end = strchr(rate_start, ','); + if (!end) end = strchr(rate_start, '}'); + if (end) printf("Rate Limit: %.*s\n", (int)(end - rate_start), rate_start); + } + + char *burst_start = strstr(response, "\"burst\":"); + if (burst_start) { + burst_start += 8; + char *end = strchr(burst_start, ','); + if (!end) end = strchr(burst_start, '}'); + if (end) printf("Burst: %.*s\n", (int)(end - burst_start), burst_start); + } + + char *conc_start = strstr(response, "\"concurrency\":"); + if (conc_start) { + conc_start += 14; + char *end = strchr(conc_start, ','); + if (!end) end = strchr(conc_start, '}'); + if (end) printf("Concurrency: %.*s\n", (int)(end - conc_start), conc_start); + } +} + void cmd_service(const char *name, const char *ports, const char *domains, const char *service_type, const char *bootstrap, int list, const char *info, const char *logs, const char *tail, const char *sleep_svc, const char *wake, const char *destroy, const char *network, int vcpu, const char *api_key) { char cmd[8192]; @@ -438,10 +575,21 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Usage: %s [options] \n", argv[0]); fprintf(stderr, " %s session [options]\n", argv[0]); fprintf(stderr, " %s service [options]\n", argv[0]); + fprintf(stderr, " %s key [options]\n", argv[0]); return 1; } // Parse command + if (strcmp(argv[1], "key") == 0) { + int extend = 0; + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--extend") == 0) extend = 1; + else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i]; + } + cmd_key(extend, api_key); + return 0; + } + if (strcmp(argv[1], "session") == 0) { int list = 0; const char *kill = NULL;