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:
parent
eb5d4ca8bf
commit
5380af54cb
32 changed files with 3140 additions and 70 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -5,6 +5,14 @@
|
|||
*.class
|
||||
*.beam
|
||||
/un
|
||||
/un_go
|
||||
/un_rust
|
||||
/un_cpp
|
||||
/un_d
|
||||
/un_nim
|
||||
/un_v
|
||||
/un_zig
|
||||
/Un.class
|
||||
|
||||
# Build directories
|
||||
_build/
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ Each implementation supports:
|
|||
- **Artifact collection** (`-a -o ./output`)
|
||||
- **Interactive sessions** (`session` subcommand)
|
||||
- **Persistent services** (`service` subcommand)
|
||||
- **API key management** (`key` subcommand)
|
||||
- **Network modes** (`-n zerotrust` or `-n semitrusted`)
|
||||
|
||||
## Usage
|
||||
|
|
@ -104,6 +105,12 @@ Each implementation supports:
|
|||
|
||||
# Persistent service
|
||||
./un.py service --name myapp --ports 8080 --bootstrap "python3 -m http.server 8080"
|
||||
|
||||
# Check API key status
|
||||
./un.py key
|
||||
|
||||
# Extend/renew API key (opens browser)
|
||||
./un.py key --extend
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
|
|
|||
99
Un.cs
99
Un.cs
|
|
@ -49,6 +49,7 @@ using System.Text;
|
|||
class Un
|
||||
{
|
||||
private const string API_BASE = "https://api.unsandbox.com";
|
||||
private const string PORTAL_BASE = "https://unsandbox.com";
|
||||
private const string BLUE = "\x1B[34m";
|
||||
private const string RED = "\x1B[31m";
|
||||
private const string GREEN = "\x1B[32m";
|
||||
|
|
@ -86,6 +87,10 @@ class Un
|
|||
{
|
||||
CmdService(parsedArgs);
|
||||
}
|
||||
else if (parsedArgs.Command == "key")
|
||||
{
|
||||
CmdKey(parsedArgs);
|
||||
}
|
||||
else if (parsedArgs.SourceFile != null)
|
||||
{
|
||||
CmdExecute(parsedArgs);
|
||||
|
|
@ -243,6 +248,91 @@ class Un
|
|||
Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
|
||||
}
|
||||
|
||||
static void CmdKey(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
|
||||
var result = ApiRequest("/keys/validate", "POST", null, apiKey);
|
||||
|
||||
if (!result.ContainsKey("valid"))
|
||||
{
|
||||
Console.Error.WriteLine($"{RED}Error: Invalid response from server{RESET}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
bool isValid = (bool)result["valid"];
|
||||
bool isExpired = result.ContainsKey("expired") && (bool)result["expired"];
|
||||
|
||||
if (isValid && !isExpired)
|
||||
{
|
||||
Console.WriteLine($"{GREEN}Valid{RESET}");
|
||||
if (result.ContainsKey("public_key"))
|
||||
{
|
||||
Console.WriteLine($"Public Key: {result["public_key"]}");
|
||||
}
|
||||
if (result.ContainsKey("tier"))
|
||||
{
|
||||
Console.WriteLine($"Tier: {result["tier"]}");
|
||||
}
|
||||
if (result.ContainsKey("expires_at"))
|
||||
{
|
||||
Console.WriteLine($"Expires: {result["expires_at"]}");
|
||||
}
|
||||
}
|
||||
else if (isExpired)
|
||||
{
|
||||
Console.WriteLine($"{RED}Expired{RESET}");
|
||||
if (result.ContainsKey("public_key"))
|
||||
{
|
||||
Console.WriteLine($"Public Key: {result["public_key"]}");
|
||||
}
|
||||
if (result.ContainsKey("tier"))
|
||||
{
|
||||
Console.WriteLine($"Tier: {result["tier"]}");
|
||||
}
|
||||
if (result.ContainsKey("expired_at"))
|
||||
{
|
||||
Console.WriteLine($"Expired: {result["expired_at"]}");
|
||||
}
|
||||
Console.WriteLine($"{YELLOW}To renew: Visit {PORTAL_BASE}/keys/extend{RESET}");
|
||||
|
||||
if (args.KeyExtend && result.ContainsKey("public_key"))
|
||||
{
|
||||
string publicKey = (string)result["public_key"];
|
||||
string url = $"{PORTAL_BASE}/keys/extend?pk={publicKey}";
|
||||
Console.WriteLine($"{YELLOW}Opening: {url}{RESET}");
|
||||
OpenBrowser(url);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{RED}Invalid{RESET}");
|
||||
}
|
||||
}
|
||||
|
||||
static void OpenBrowser(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
else if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
{
|
||||
System.Diagnostics.Process.Start("xdg-open", url);
|
||||
}
|
||||
else if (Environment.OSVersion.Platform == PlatformID.MacOSX)
|
||||
{
|
||||
System.Diagnostics.Process.Start("open", url);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"{RED}Failed to open browser: {ex.Message}{RESET}");
|
||||
}
|
||||
}
|
||||
|
||||
static void CmdService(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
|
|
@ -680,6 +770,7 @@ class Un
|
|||
public string ServiceWake = null;
|
||||
public string ServiceDestroy = null;
|
||||
public string ServiceType = null;
|
||||
public bool KeyExtend = false;
|
||||
}
|
||||
|
||||
static Args ParseArgs(string[] args)
|
||||
|
|
@ -690,6 +781,7 @@ class Un
|
|||
string arg = args[i];
|
||||
if (arg == "session") result.Command = "session";
|
||||
else if (arg == "service") result.Command = "service";
|
||||
else if (arg == "key") result.Command = "key";
|
||||
else if (arg == "-k" || arg == "--api-key") result.ApiKey = args[++i];
|
||||
else if (arg == "-n" || arg == "--network") result.Network = args[++i];
|
||||
else if (arg == "-v" || arg == "--vcpu") result.Vcpu = int.Parse(args[++i]);
|
||||
|
|
@ -714,6 +806,7 @@ class Un
|
|||
else if (arg == "--sleep") result.ServiceSleep = args[++i];
|
||||
else if (arg == "--wake") result.ServiceWake = args[++i];
|
||||
else if (arg == "--destroy") result.ServiceDestroy = args[++i];
|
||||
else if (arg == "--extend") result.KeyExtend = true;
|
||||
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
||||
}
|
||||
return result;
|
||||
|
|
@ -724,6 +817,7 @@ class Un
|
|||
Console.WriteLine(@"Usage: Un [options] <source_file>
|
||||
Un session [options]
|
||||
Un service [options]
|
||||
Un key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Set environment variable
|
||||
|
|
@ -750,6 +844,9 @@ Service options:
|
|||
--tail ID Get last 9000 lines
|
||||
--sleep ID Freeze service
|
||||
--wake ID Unfreeze service
|
||||
--destroy ID Destroy service");
|
||||
--destroy ID Destroy service
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend expired key");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
116
Un.java
116
Un.java
|
|
@ -48,6 +48,7 @@ import java.util.Base64;
|
|||
|
||||
public class Un {
|
||||
private static final String API_BASE = "https://api.unsandbox.com";
|
||||
private static final String PORTAL_BASE = "https://unsandbox.com";
|
||||
private static final String BLUE = "\033[34m";
|
||||
private static final String RED = "\033[31m";
|
||||
private static final String GREEN = "\033[32m";
|
||||
|
|
@ -78,6 +79,8 @@ public class Un {
|
|||
cmdSession(parsedArgs);
|
||||
} else if (parsedArgs.command.equals("service")) {
|
||||
cmdService(parsedArgs);
|
||||
} else if (parsedArgs.command.equals("key")) {
|
||||
cmdKey(parsedArgs);
|
||||
} else if (parsedArgs.sourceFile != null) {
|
||||
cmdExecute(parsedArgs);
|
||||
} else {
|
||||
|
|
@ -307,6 +310,108 @@ public class Un {
|
|||
System.exit(1);
|
||||
}
|
||||
|
||||
private static void cmdKey(Args args) throws Exception {
|
||||
String apiKey = getApiKey(args.apiKey);
|
||||
|
||||
if (args.keyExtend) {
|
||||
// First validate to get public_key
|
||||
Map<String, Object> result = validateKey(apiKey);
|
||||
String publicKey = (String) result.get("public_key");
|
||||
if (publicKey == null || publicKey.isEmpty()) {
|
||||
System.err.println(RED + "Error: Could not retrieve public key" + RESET);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String extendUrl = PORTAL_BASE + "/keys/extend?pk=" + urlEncode(publicKey);
|
||||
System.out.println(YELLOW + "Opening browser to extend key:" + RESET);
|
||||
System.out.println(extendUrl);
|
||||
|
||||
// Try to open browser
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("mac")) {
|
||||
Runtime.getRuntime().exec(new String[]{"open", extendUrl});
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
Runtime.getRuntime().exec(new String[]{"xdg-open", extendUrl});
|
||||
} else if (os.contains("win")) {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", extendUrl});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Browser opening failed, URL already printed
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: validate key
|
||||
Map<String, Object> result = validateKey(apiKey);
|
||||
Boolean expired = (Boolean) result.get("expired");
|
||||
String publicKey = (String) result.get("public_key");
|
||||
String tier = (String) result.get("tier");
|
||||
String status = (String) result.get("status");
|
||||
String expiresAt = (String) result.get("expires_at");
|
||||
String timeRemaining = (String) result.get("time_remaining");
|
||||
Object rateLimit = result.get("rate_limit");
|
||||
Object burst = result.get("burst");
|
||||
Object concurrency = result.get("concurrency");
|
||||
|
||||
if (expired != null && expired) {
|
||||
System.out.println(RED + "Expired" + RESET);
|
||||
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A"));
|
||||
System.out.println("Tier: " + (tier != null ? tier : "N/A"));
|
||||
System.out.println("Expired: " + (expiresAt != null ? expiresAt : "N/A"));
|
||||
System.out.println(YELLOW + "To renew: Visit " + PORTAL_BASE + "/keys/extend" + RESET);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Valid key
|
||||
System.out.println(GREEN + "Valid" + RESET);
|
||||
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A"));
|
||||
System.out.println("Tier: " + (tier != null ? tier : "N/A"));
|
||||
System.out.println("Status: " + (status != null ? status : "N/A"));
|
||||
System.out.println("Expires: " + (expiresAt != null ? expiresAt : "N/A"));
|
||||
System.out.println("Time Remaining: " + (timeRemaining != null ? timeRemaining : "N/A"));
|
||||
System.out.println("Rate Limit: " + (rateLimit != null ? rateLimit : "N/A"));
|
||||
System.out.println("Burst: " + (burst != null ? burst : "N/A"));
|
||||
System.out.println("Concurrency: " + (concurrency != null ? concurrency : "N/A"));
|
||||
}
|
||||
|
||||
private static Map<String, Object> validateKey(String apiKey) throws Exception {
|
||||
URL url = new URL(PORTAL_BASE + "/keys/validate");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(30000);
|
||||
|
||||
int status = conn.getResponseCode();
|
||||
if (status < 200 || status >= 300) {
|
||||
String error = readStream(conn.getErrorStream());
|
||||
// Try to parse error JSON
|
||||
try {
|
||||
Map<String, Object> errorJson = parseJson(error);
|
||||
String reason = (String) errorJson.getOrDefault("error", error);
|
||||
System.out.println(RED + "Invalid" + RESET);
|
||||
System.out.println("Reason: " + reason);
|
||||
} catch (Exception e) {
|
||||
System.out.println(RED + "Invalid" + RESET);
|
||||
System.out.println("Reason: " + error);
|
||||
}
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String response = readStream(conn.getInputStream());
|
||||
return parseJson(response);
|
||||
}
|
||||
|
||||
private static String urlEncode(String s) {
|
||||
try {
|
||||
return URLEncoder.encode(s, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getApiKey(String argsKey) {
|
||||
String key = argsKey != null ? argsKey : System.getenv("UNSANDBOX_API_KEY");
|
||||
if (key == null || key.isEmpty()) {
|
||||
|
|
@ -562,6 +667,9 @@ public class Un {
|
|||
String serviceSleep = null;
|
||||
String serviceWake = null;
|
||||
String serviceDestroy = null;
|
||||
|
||||
// Key args
|
||||
boolean keyExtend = false;
|
||||
}
|
||||
|
||||
private static Args parseArgs(String[] args) {
|
||||
|
|
@ -572,6 +680,8 @@ public class Un {
|
|||
result.command = "session";
|
||||
} else if (arg.equals("service")) {
|
||||
result.command = "service";
|
||||
} else if (arg.equals("key")) {
|
||||
result.command = "key";
|
||||
} else if (arg.equals("-k") || arg.equals("--api-key")) {
|
||||
result.apiKey = args[++i];
|
||||
} else if (arg.equals("-n") || arg.equals("--network")) {
|
||||
|
|
@ -613,6 +723,8 @@ public class Un {
|
|||
result.serviceWake = args[++i];
|
||||
} else if (arg.equals("--destroy")) {
|
||||
result.serviceDestroy = args[++i];
|
||||
} else if (arg.equals("--extend")) {
|
||||
result.keyExtend = true;
|
||||
} else if (!arg.startsWith("-")) {
|
||||
result.sourceFile = arg;
|
||||
}
|
||||
|
|
@ -624,6 +736,7 @@ public class Un {
|
|||
System.out.println("Usage: java Un [options] <source_file>");
|
||||
System.out.println(" java Un session [options]");
|
||||
System.out.println(" java Un service [options]");
|
||||
System.out.println(" java Un key [options]");
|
||||
System.out.println();
|
||||
System.out.println("Execute options:");
|
||||
System.out.println(" -e KEY=VALUE Set environment variable");
|
||||
|
|
@ -651,5 +764,8 @@ public class Un {
|
|||
System.out.println(" --sleep ID Freeze service");
|
||||
System.out.println(" --wake ID Unfreeze service");
|
||||
System.out.println(" --destroy ID Destroy service");
|
||||
System.out.println();
|
||||
System.out.println("Key options:");
|
||||
System.out.println(" --extend Open browser to extend key");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
un.clj
106
un.clj
|
|
@ -63,6 +63,8 @@
|
|||
(def yellow "\u001b[33m")
|
||||
(def reset "\u001b[0m")
|
||||
|
||||
(def portal-base "https://unsandbox.com")
|
||||
|
||||
(def ext-map
|
||||
{".hs" "haskell" ".ml" "ocaml" ".clj" "clojure" ".scm" "scheme"
|
||||
".lisp" "commonlisp" ".erl" "erlang" ".ex" "elixir" ".exs" "elixir"
|
||||
|
|
@ -126,6 +128,17 @@
|
|||
(str "https://api.unsandbox.com" endpoint)
|
||||
"-H" (str "Authorization: Bearer " api-key))))
|
||||
|
||||
(defn curl-portal-post [api-key endpoint json-data]
|
||||
(let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")]
|
||||
(spit tmp-file json-data)
|
||||
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST"
|
||||
(str portal-base endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (str "Authorization: Bearer " api-key)
|
||||
"-d" (str "@" tmp-file))]
|
||||
(io/delete-file tmp-file true)
|
||||
out)))
|
||||
|
||||
(defn execute-command [file env-vars artifacts out-dir network vcpu]
|
||||
(let [api-key (get-api-key)
|
||||
ext (get-extension file)
|
||||
|
|
@ -198,6 +211,43 @@
|
|||
(println (str green "Service created" reset))
|
||||
(println (curl-post api-key "/services" json)))))))
|
||||
|
||||
(defn validate-key [api-key extend?]
|
||||
(let [response (curl-portal-post api-key "/keys/validate" "{}")
|
||||
status (extract-field "status" response)
|
||||
public-key (extract-field "public_key" response)
|
||||
tier (extract-field "tier" response)
|
||||
expires-at (extract-field "expires_at" 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)))
|
||||
(when extend?
|
||||
(let [url (str portal-base "/keys/extend?pk=" public-key)]
|
||||
(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))
|
||||
(when extend?
|
||||
(let [url (str portal-base "/keys/extend?pk=" public-key)]
|
||||
(sh "xdg-open" url))))
|
||||
|
||||
:else
|
||||
(do
|
||||
(println (str red "Invalid" reset))
|
||||
(println response)))))
|
||||
|
||||
(defn key-command [extend?]
|
||||
(let [api-key (get-api-key)]
|
||||
(validate-key api-key extend?)))
|
||||
|
||||
(defn parse-args [args]
|
||||
(loop [args args
|
||||
file nil
|
||||
|
|
@ -215,110 +265,122 @@
|
|||
service-ports nil
|
||||
service-bootstrap nil
|
||||
service-type nil
|
||||
key-extend false
|
||||
mode :execute]
|
||||
(cond
|
||||
(empty? args)
|
||||
(case mode
|
||||
:session (session-command (or session-action :create) session-id session-shell network vcpu)
|
||||
:service (service-command (or service-action :create) service-id service-name service-ports service-bootstrap service-type network vcpu)
|
||||
:key (key-command key-extend)
|
||||
:execute (if file
|
||||
(execute-command file env-vars artifacts out-dir network vcpu)
|
||||
(do (println "Usage: un.clj [options] <source_file>")
|
||||
(println " un.clj session [options]")
|
||||
(println " un.clj service [options]")
|
||||
(println " un.clj key [options]")
|
||||
(System/exit 1))))
|
||||
|
||||
(= (first args) "session")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type :session)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :session)
|
||||
|
||||
(= (first args) "service")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type :service)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :service)
|
||||
|
||||
(= (first args) "key")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :key)
|
||||
|
||||
;; Key options
|
||||
(and (= mode :key) (= (first args) "--extend"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type true mode)
|
||||
|
||||
;; Session options
|
||||
(and (= mode :session) (= (first args) "--list"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu :list session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :session) (= (first args) "--kill"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu :kill (second args) session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :session) (or (= (first args) "--shell") (= (first args) "-s")))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id (second args)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
;; Service options
|
||||
(and (= mode :service) (= (first args) "--list"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:list service-id service-name service-ports service-bootstrap service-type mode)
|
||||
:list service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--info"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:info (second args) service-name service-ports service-bootstrap service-type mode)
|
||||
:info (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--logs"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:logs (second args) service-name service-ports service-bootstrap service-type mode)
|
||||
:logs (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--sleep"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:sleep (second args) service-name service-ports service-bootstrap service-type mode)
|
||||
:sleep (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--wake"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:wake (second args) service-name service-ports service-bootstrap service-type mode)
|
||||
:wake (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--destroy"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:destroy (second args) service-name service-ports service-bootstrap service-type mode)
|
||||
:destroy (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--name"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:create service-id (second args) service-ports service-bootstrap service-type mode)
|
||||
:create service-id (second args) service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--ports"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name (second args) service-bootstrap service-type mode)
|
||||
service-action service-id service-name (second args) service-bootstrap service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--bootstrap"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports (second args) service-type mode)
|
||||
service-action service-id service-name service-ports (second args) service-type key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--type"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap (second args) mode)
|
||||
service-action service-id service-name service-ports service-bootstrap (second args) key-extend mode)
|
||||
|
||||
;; Execute options
|
||||
(= (first args) "-e")
|
||||
(let [[k v] (str/split (second args) #"=" 2)]
|
||||
(recur (rest (rest args)) file (conj env-vars [k v]) artifacts out-dir network vcpu
|
||||
session-action session-id session-shell service-action service-id service-name service-ports service-bootstrap service-type mode))
|
||||
session-action session-id session-shell service-action service-id service-name service-ports service-bootstrap service-type key-extend mode))
|
||||
|
||||
(= (first args) "-a")
|
||||
(recur (rest args) file env-vars true out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(= (first args) "-o")
|
||||
(recur (rest (rest args)) file env-vars artifacts (second args) network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(= (first args) "-n")
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir (second args) vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
(= (first args) "-v")
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network (Integer/parseInt (second args)) session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
;; Source file
|
||||
(and (= mode :execute) (not (.startsWith (first args) "-")) (nil? file))
|
||||
(recur (rest args) (first args) env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
|
||||
:else
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type mode))))
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode))))
|
||||
|
||||
(parse-args *command-line-args*)
|
||||
|
|
|
|||
96
un.cpp
96
un.cpp
|
|
@ -55,6 +55,7 @@
|
|||
using namespace std;
|
||||
|
||||
const string API_BASE = "https://api.unsandbox.com";
|
||||
const string PORTAL_BASE = "https://unsandbox.com";
|
||||
const string BLUE = "\033[34m";
|
||||
const string RED = "\033[31m";
|
||||
const string GREEN = "\033[32m";
|
||||
|
|
@ -294,6 +295,87 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
exit(1);
|
||||
}
|
||||
|
||||
void cmd_validate_key(bool extend, const string& api_key) {
|
||||
string cmd = "curl -s -X POST '" + PORTAL_BASE + "/keys/validate' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "'";
|
||||
|
||||
string result = exec_curl(cmd);
|
||||
|
||||
// Parse JSON response
|
||||
size_t status_pos = result.find("\"status\":\"");
|
||||
size_t public_key_pos = result.find("\"public_key\":\"");
|
||||
size_t tier_pos = result.find("\"tier\":\"");
|
||||
size_t expires_pos = result.find("\"expires_at\":\"");
|
||||
|
||||
if (status_pos == string::npos) {
|
||||
cerr << RED << "Error: Invalid API response" << RESET << endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Extract status
|
||||
status_pos += 10;
|
||||
size_t status_end = result.find("\"", status_pos);
|
||||
string status = result.substr(status_pos, status_end - status_pos);
|
||||
|
||||
// Extract public_key
|
||||
string public_key;
|
||||
if (public_key_pos != string::npos) {
|
||||
public_key_pos += 14;
|
||||
size_t pk_end = result.find("\"", public_key_pos);
|
||||
public_key = result.substr(public_key_pos, pk_end - public_key_pos);
|
||||
}
|
||||
|
||||
// Extract tier
|
||||
string tier;
|
||||
if (tier_pos != string::npos) {
|
||||
tier_pos += 8;
|
||||
size_t tier_end = result.find("\"", tier_pos);
|
||||
tier = result.substr(tier_pos, tier_end - tier_pos);
|
||||
}
|
||||
|
||||
// Extract expires_at
|
||||
string expires_at;
|
||||
if (expires_pos != string::npos) {
|
||||
expires_pos += 14;
|
||||
size_t expires_end = result.find("\"", expires_pos);
|
||||
expires_at = result.substr(expires_pos, expires_end - expires_pos);
|
||||
}
|
||||
|
||||
if (status == "valid") {
|
||||
cout << GREEN << "Valid" << RESET << endl;
|
||||
if (!public_key.empty()) {
|
||||
cout << "Public Key: " << public_key << endl;
|
||||
}
|
||||
if (!tier.empty()) {
|
||||
cout << "Tier: " << tier << endl;
|
||||
}
|
||||
if (!expires_at.empty()) {
|
||||
cout << "Expires: " << expires_at << endl;
|
||||
}
|
||||
} else if (status == "expired") {
|
||||
cout << RED << "Expired" << RESET << endl;
|
||||
if (!public_key.empty()) {
|
||||
cout << "Public Key: " << public_key << endl;
|
||||
}
|
||||
if (!tier.empty()) {
|
||||
cout << "Tier: " << tier << endl;
|
||||
}
|
||||
if (!expires_at.empty()) {
|
||||
cout << "Expired: " << expires_at << endl;
|
||||
}
|
||||
cout << YELLOW << "To renew: Visit " << PORTAL_BASE << "/keys/extend" << RESET << endl;
|
||||
|
||||
if (extend && !public_key.empty()) {
|
||||
string url = PORTAL_BASE + "/keys/extend?pk=" + public_key;
|
||||
string browser_cmd = "xdg-open '" + url + "' 2>/dev/null || open '" + url + "' 2>/dev/null";
|
||||
system(browser_cmd.c_str());
|
||||
}
|
||||
} else {
|
||||
cout << RED << "Invalid" << RESET << endl;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
string api_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : "";
|
||||
|
||||
|
|
@ -301,6 +383,7 @@ int main(int argc, char* argv[]) {
|
|||
cerr << "Usage: " << argv[0] << " [options] <source_file>" << endl;
|
||||
cerr << " " << argv[0] << " session [options]" << endl;
|
||||
cerr << " " << argv[0] << " service [options]" << endl;
|
||||
cerr << " " << argv[0] << " key [options]" << endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -356,6 +439,19 @@ int main(int argc, char* argv[]) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (cmd_type == "key") {
|
||||
bool extend = false;
|
||||
|
||||
for (int i = 2; i < argc; i++) {
|
||||
string arg = argv[i];
|
||||
if (arg == "--extend") extend = true;
|
||||
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_validate_key(extend, api_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Execute mode
|
||||
vector<string> envs, files;
|
||||
bool artifacts = false;
|
||||
|
|
|
|||
89
un.cr
89
un.cr
|
|
@ -67,6 +67,7 @@ YELLOW = "\033[33m"
|
|||
RESET = "\033[0m"
|
||||
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
|
||||
def detect_language(filename : String) : String
|
||||
ext = File.extname(filename).downcase
|
||||
|
|
@ -231,6 +232,84 @@ def cmd_session(args)
|
|||
exit 1
|
||||
end
|
||||
|
||||
def cmd_key(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
|
||||
# Validate key
|
||||
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
||||
headers = HTTP::Headers{
|
||||
"Content-Type" => "application/json",
|
||||
"Authorization" => "Bearer #{api_key}"
|
||||
}
|
||||
|
||||
begin
|
||||
response = HTTP::Client.post(url, headers: headers, body: "{}")
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
status = result["status"]?.try(&.as_s?) || "unknown"
|
||||
public_key = result["public_key"]?.try(&.as_s?) || "N/A"
|
||||
tier = result["tier"]?.try(&.as_s?) || "N/A"
|
||||
|
||||
case status
|
||||
when "valid"
|
||||
puts "#{GREEN}Valid#{RESET}"
|
||||
puts "Public Key: #{public_key}"
|
||||
puts "Tier: #{tier}"
|
||||
if expires_at = result["expires_at"]?.try(&.as_s?)
|
||||
puts "Expires: #{expires_at}"
|
||||
end
|
||||
|
||||
# Handle --extend flag
|
||||
if args[:extend]?.as?(Bool)
|
||||
extend_url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
|
||||
puts "\n#{BLUE}Opening browser to extend key...#{RESET}"
|
||||
# Try common browser commands
|
||||
["xdg-open", "open", "firefox", "chromium", "google-chrome"].each do |browser|
|
||||
if system("which #{browser} > /dev/null 2>&1")
|
||||
system("#{browser} '#{extend_url}' > /dev/null 2>&1 &")
|
||||
break
|
||||
end
|
||||
end
|
||||
puts extend_url
|
||||
end
|
||||
|
||||
when "expired"
|
||||
puts "#{RED}Expired#{RESET}"
|
||||
puts "Public Key: #{public_key}"
|
||||
puts "Tier: #{tier}"
|
||||
if expired_at = result["expires_at"]?.try(&.as_s?)
|
||||
puts "Expired: #{expired_at}"
|
||||
end
|
||||
puts "#{YELLOW}To renew: Visit #{PORTAL_BASE}/keys/extend#{RESET}"
|
||||
|
||||
# Handle --extend flag for expired keys
|
||||
if args[:extend]?.as?(Bool)
|
||||
extend_url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
|
||||
puts "\n#{BLUE}Opening browser to renew key...#{RESET}"
|
||||
["xdg-open", "open", "firefox", "chromium", "google-chrome"].each do |browser|
|
||||
if system("which #{browser} > /dev/null 2>&1")
|
||||
system("#{browser} '#{extend_url}' > /dev/null 2>&1 &")
|
||||
break
|
||||
end
|
||||
end
|
||||
puts extend_url
|
||||
end
|
||||
|
||||
when "invalid"
|
||||
puts "#{RED}Invalid#{RESET}"
|
||||
STDERR.puts "#{RED}Error: API key is not valid#{RESET}"
|
||||
exit 1
|
||||
|
||||
else
|
||||
puts "#{YELLOW}Unknown status: #{status}#{RESET}"
|
||||
end
|
||||
|
||||
rescue ex
|
||||
STDERR.puts "#{RED}Error: Failed to validate key: #{ex.message}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_service(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
|
||||
|
|
@ -355,11 +434,12 @@ def main
|
|||
ports: nil,
|
||||
domains: nil,
|
||||
service_type: nil,
|
||||
bootstrap: nil
|
||||
bootstrap: nil,
|
||||
extend: false
|
||||
} of Symbol => (String | Array(String) | Bool | Nil)
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr session [options]\n un.cr service [options]"
|
||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr session [options]\n un.cr service [options]\n un.cr key [options]"
|
||||
|
||||
opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k }
|
||||
opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
||||
|
|
@ -379,6 +459,7 @@ def main
|
|||
opts.on("--domains=DOMAINS", "Comma-separated domains") { |d| args[:domains] = d }
|
||||
opts.on("--type=TYPE", "Service type for SRV records") { |t| args[:service_type] = t }
|
||||
opts.on("--bootstrap=CMD", "Bootstrap command/file") { |b| args[:bootstrap] = b }
|
||||
opts.on("--extend", "Open browser to extend/renew key") { args[:extend] = true }
|
||||
|
||||
opts.unknown_args do |before, after|
|
||||
if before.size > 0
|
||||
|
|
@ -387,6 +468,8 @@ def main
|
|||
args[:command] = "session"
|
||||
when "service"
|
||||
args[:command] = "service"
|
||||
when "key"
|
||||
args[:command] = "key"
|
||||
else
|
||||
args[:source_file] = before[0]
|
||||
end
|
||||
|
|
@ -400,6 +483,8 @@ def main
|
|||
cmd_session(args)
|
||||
elsif args[:command] == "service"
|
||||
cmd_service(args)
|
||||
elsif args[:command] == "key"
|
||||
cmd_key(args)
|
||||
elsif args[:source_file]
|
||||
cmd_execute(args)
|
||||
else
|
||||
|
|
|
|||
139
un.d
139
un.d
|
|
@ -54,6 +54,7 @@ import std.array;
|
|||
import std.algorithm;
|
||||
|
||||
immutable string API_BASE = "https://api.unsandbox.com";
|
||||
immutable string PORTAL_BASE = "https://unsandbox.com";
|
||||
immutable string BLUE = "\033[34m";
|
||||
immutable string RED = "\033[31m";
|
||||
immutable string GREEN = "\033[32m";
|
||||
|
|
@ -223,6 +224,126 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
|
|||
exit(1);
|
||||
}
|
||||
|
||||
void openBrowser(string url) {
|
||||
version(linux) {
|
||||
executeShell("xdg-open \"" ~ url ~ "\" 2>/dev/null &");
|
||||
} else version(OSX) {
|
||||
executeShell("open \"" ~ url ~ "\"");
|
||||
} else version(Windows) {
|
||||
executeShell("start \"\" \"" ~ url ~ "\"");
|
||||
} else {
|
||||
stderr.writefln("%sError: Unsupported platform for browser opening%s", RED, RESET);
|
||||
}
|
||||
}
|
||||
|
||||
string formatDuration(long totalMinutes) {
|
||||
long days = totalMinutes / (24 * 60);
|
||||
long hours = (totalMinutes % (24 * 60)) / 60;
|
||||
long minutes = totalMinutes % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return format("%dd %dh %dm", days, hours, minutes);
|
||||
} else if (hours > 0) {
|
||||
return format("%dh %dm", hours, minutes);
|
||||
} else {
|
||||
return format("%dm", minutes);
|
||||
}
|
||||
}
|
||||
|
||||
void validateKey(string apiKey, bool extend) {
|
||||
import std.json;
|
||||
import std.datetime;
|
||||
|
||||
string cmd = format(`curl -s -w '\n%%{http_code}' -X POST '%s/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s'`, PORTAL_BASE, apiKey);
|
||||
string response = execCurl(cmd);
|
||||
|
||||
auto lines = response.split("\n");
|
||||
string body = lines.length > 1 ? lines[0..$-1].join("\n") : response;
|
||||
string statusCode = lines.length > 1 ? lines[$-1] : "200";
|
||||
|
||||
JSONValue result;
|
||||
try {
|
||||
result = parseJSON(body);
|
||||
} catch (Exception e) {
|
||||
stderr.writefln("%sError parsing response: %s%s", RED, e.msg, RESET);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (statusCode[0] == '4' || statusCode[0] == '5') {
|
||||
// Invalid key
|
||||
writefln("%sInvalid%s", RED, RESET);
|
||||
if ("error" in result) {
|
||||
writefln("Reason: %s", result["error"].str);
|
||||
} else if ("message" in result) {
|
||||
writefln("Reason: %s", result["message"].str);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
bool valid = result["valid"].type == JSONType.true_;
|
||||
bool expired = result["expired"].type == JSONType.true_;
|
||||
string publicKey = "public_key" in result ? result["public_key"].str : "";
|
||||
string tier = "tier" in result ? result["tier"].str : "";
|
||||
string status = "status" in result ? result["status"].str : "";
|
||||
|
||||
if (expired) {
|
||||
// Expired key
|
||||
writefln("%sExpired%s", RED, RESET);
|
||||
writefln("Public Key: %s", publicKey);
|
||||
writefln("Tier: %s", tier);
|
||||
if ("expires_at" in result) {
|
||||
writefln("Expired: %s", result["expires_at"].str);
|
||||
}
|
||||
writefln("%sTo renew: Visit https://unsandbox.com/keys/extend%s", YELLOW, RESET);
|
||||
|
||||
if (extend) {
|
||||
string extendURL = PORTAL_BASE ~ "/keys/extend?pk=" ~ publicKey;
|
||||
writefln("\n%sOpening browser to extend key...%s", GREEN, RESET);
|
||||
openBrowser(extendURL);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
// Valid key
|
||||
writefln("%sValid%s", GREEN, RESET);
|
||||
writefln("Public Key: %s", publicKey);
|
||||
writefln("Tier: %s", tier);
|
||||
writefln("Status: %s", status);
|
||||
|
||||
if ("expires_at" in result) {
|
||||
string expiresAt = result["expires_at"].str;
|
||||
writefln("Expires: %s", expiresAt);
|
||||
|
||||
// Calculate time remaining (simplified - just show the date)
|
||||
// Full datetime parsing would require additional complexity
|
||||
}
|
||||
|
||||
if ("rate_limit" in result && result["rate_limit"].type != JSONType.null_) {
|
||||
writefln("Rate Limit: %.0f req/min", result["rate_limit"].floating);
|
||||
}
|
||||
if ("burst" in result && result["burst"].type != JSONType.null_) {
|
||||
writefln("Burst: %.0f req", result["burst"].floating);
|
||||
}
|
||||
if ("concurrency" in result && result["concurrency"].type != JSONType.null_) {
|
||||
writefln("Concurrency: %.0f", result["concurrency"].floating);
|
||||
}
|
||||
|
||||
if (extend) {
|
||||
string extendURL = PORTAL_BASE ~ "/keys/extend?pk=" ~ publicKey;
|
||||
writefln("\n%sOpening browser to extend key...%s", GREEN, RESET);
|
||||
openBrowser(extendURL);
|
||||
}
|
||||
} else {
|
||||
// Invalid key
|
||||
writefln("%sInvalid%s", RED, RESET);
|
||||
if ("error" in result) {
|
||||
writefln("Reason: %s", result["error"].str);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(string[] args) {
|
||||
string apiKey = environment.get("UNSANDBOX_API_KEY", "");
|
||||
|
||||
|
|
@ -230,6 +351,7 @@ int main(string[] args) {
|
|||
stderr.writefln("Usage: %s [options] <source_file>", args[0]);
|
||||
stderr.writefln(" %s session [options]", args[0]);
|
||||
stderr.writefln(" %s service [options]", args[0]);
|
||||
stderr.writefln(" %s key [options]", args[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -281,6 +403,23 @@ int main(string[] args) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (args[1] == "key") {
|
||||
bool extend = false;
|
||||
|
||||
for (size_t i = 2; i < args.length; i++) {
|
||||
if (args[i] == "--extend") extend = true;
|
||||
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i];
|
||||
}
|
||||
|
||||
if (apiKey.empty) {
|
||||
stderr.writefln("%sError: UNSANDBOX_API_KEY not set%s", RED, RESET);
|
||||
return 1;
|
||||
}
|
||||
|
||||
validateKey(apiKey, extend);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Execute mode
|
||||
string[] envs;
|
||||
bool artifacts = false;
|
||||
|
|
|
|||
64
un.dart
64
un.dart
|
|
@ -44,6 +44,7 @@ import 'dart:io';
|
|||
import 'dart:convert';
|
||||
|
||||
const String apiBase = 'https://api.unsandbox.com';
|
||||
const String portalBase = 'https://unsandbox.com';
|
||||
const String blue = '\x1B[34m';
|
||||
const String red = '\x1B[31m';
|
||||
const String green = '\x1B[32m';
|
||||
|
|
@ -90,6 +91,7 @@ class Args {
|
|||
String? serviceSleep;
|
||||
String? serviceWake;
|
||||
String? serviceDestroy;
|
||||
bool keyExtend = false;
|
||||
}
|
||||
|
||||
String getApiKey(String? argsKey) {
|
||||
|
|
@ -114,7 +116,8 @@ String detectLanguage(String filename) {
|
|||
return lang;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String apiKey) async {
|
||||
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String apiKey, {String? baseUrl}) async {
|
||||
final base = baseUrl ?? apiBase;
|
||||
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
|
||||
|
||||
try {
|
||||
|
|
@ -122,7 +125,7 @@ Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, Stri
|
|||
await tempFile.writeAsString(jsonData);
|
||||
}
|
||||
|
||||
final args = ['curl', '-s', '-X', method, '$apiBase$endpoint',
|
||||
final args = ['curl', '-s', '-X', method, '$base$endpoint',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', 'Authorization: Bearer $apiKey'];
|
||||
|
||||
|
|
@ -348,6 +351,51 @@ Future<void> cmdService(Args args) async {
|
|||
exit(1);
|
||||
}
|
||||
|
||||
Future<void> cmdKey(Args args) async {
|
||||
final apiKey = getApiKey(args.apiKey);
|
||||
|
||||
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]);
|
||||
} else {
|
||||
print('${yellow}Please open manually: $url$reset');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
stderr.writeln('${red}Error validating key: $e$reset');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Args parseArgs(List<String> argv) {
|
||||
final args = Args();
|
||||
var i = 0;
|
||||
|
|
@ -359,6 +407,9 @@ Args parseArgs(List<String> argv) {
|
|||
case 'service':
|
||||
args.command = 'service';
|
||||
break;
|
||||
case 'key':
|
||||
args.command = 'key';
|
||||
break;
|
||||
case '-k':
|
||||
case '--api-key':
|
||||
args.apiKey = argv[++i];
|
||||
|
|
@ -432,6 +483,9 @@ Args parseArgs(List<String> argv) {
|
|||
case '--destroy':
|
||||
args.serviceDestroy = argv[++i];
|
||||
break;
|
||||
case '--extend':
|
||||
args.keyExtend = true;
|
||||
break;
|
||||
default:
|
||||
if (!argv[i].startsWith('-')) {
|
||||
args.sourceFile = argv[i];
|
||||
|
|
@ -447,6 +501,7 @@ void printHelp() {
|
|||
Usage: dart un.dart [options] <source_file>
|
||||
dart un.dart session [options]
|
||||
dart un.dart service [options]
|
||||
dart un.dart key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Set environment variable
|
||||
|
|
@ -474,6 +529,9 @@ Service options:
|
|||
--sleep ID Freeze service
|
||||
--wake ID Unfreeze service
|
||||
--destroy ID Destroy service
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key
|
||||
''');
|
||||
}
|
||||
|
||||
|
|
@ -485,6 +543,8 @@ void main(List<String> arguments) async {
|
|||
await cmdSession(args);
|
||||
} else if (args.command == 'service') {
|
||||
await cmdService(args);
|
||||
} else if (args.command == 'key') {
|
||||
await cmdKey(args);
|
||||
} else if (args.sourceFile != null) {
|
||||
await cmdExecute(args);
|
||||
} else {
|
||||
|
|
|
|||
105
un.erl
Normal file → Executable file
105
un.erl
Normal file → Executable 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]).
|
||||
|
|
|
|||
148
un.ex
Normal file → Executable file
148
un.ex
Normal file → Executable file
|
|
@ -58,6 +58,8 @@ defmodule Un do
|
|||
@yellow "\e[33m"
|
||||
@reset "\e[0m"
|
||||
|
||||
@portal_base "https://unsandbox.com"
|
||||
|
||||
@ext_map %{
|
||||
".ex" => "elixir", ".exs" => "elixir", ".erl" => "erlang",
|
||||
".py" => "python", ".js" => "javascript", ".ts" => "typescript",
|
||||
|
|
@ -76,12 +78,14 @@ defmodule Un do
|
|||
def main([]), do: print_usage()
|
||||
def main(["session" | rest]), do: session_command(rest)
|
||||
def main(["service" | rest]), do: service_command(rest)
|
||||
def main(["key" | rest]), do: key_command(rest)
|
||||
def main(args), do: execute_command(args)
|
||||
|
||||
defp print_usage do
|
||||
IO.puts("Usage: un.ex [options] <source_file>")
|
||||
IO.puts(" un.ex session [options]")
|
||||
IO.puts(" un.ex service [options]")
|
||||
IO.puts(" un.ex key [--extend]")
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
|
|
@ -207,6 +211,134 @@ defmodule Un do
|
|||
IO.puts(response)
|
||||
end
|
||||
|
||||
# Key command
|
||||
defp key_command(args) do
|
||||
api_key = get_api_key()
|
||||
|
||||
if "--extend" in args do
|
||||
validate_key(api_key, extend: true)
|
||||
else
|
||||
validate_key(api_key, extend: false)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_key(api_key, extend: extend) do
|
||||
json = "{}"
|
||||
response = portal_curl_post(api_key, "/keys/validate", json)
|
||||
|
||||
# Try to use Jason if available, otherwise fall back to manual parsing
|
||||
try do
|
||||
case Jason.decode(response) do
|
||||
{:ok, data} ->
|
||||
display_key_info(data, extend)
|
||||
|
||||
{:error, _} ->
|
||||
# Fallback if Jason is not available, parse manually
|
||||
display_key_info_manual(response, extend)
|
||||
end
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
# If Jason module doesn't exist, use manual parsing
|
||||
display_key_info_manual(response, extend)
|
||||
end
|
||||
end
|
||||
|
||||
defp display_key_info(data, extend) do
|
||||
status = Map.get(data, "status")
|
||||
public_key = Map.get(data, "public_key")
|
||||
tier = Map.get(data, "tier")
|
||||
expires_at = Map.get(data, "expires_at")
|
||||
|
||||
case status do
|
||||
"valid" ->
|
||||
IO.puts("#{@green}Valid#{@reset}")
|
||||
IO.puts("Public Key: #{public_key}")
|
||||
IO.puts("Tier: #{tier}")
|
||||
IO.puts("Expires: #{expires_at}")
|
||||
|
||||
if extend do
|
||||
open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
|
||||
end
|
||||
|
||||
"expired" ->
|
||||
IO.puts("#{@red}Expired#{@reset}")
|
||||
IO.puts("Public Key: #{public_key}")
|
||||
IO.puts("Tier: #{tier}")
|
||||
IO.puts("Expired: #{expires_at}")
|
||||
IO.puts("#{@yellow}To renew: Visit #{@portal_base}/keys/extend#{@reset}")
|
||||
|
||||
if extend do
|
||||
open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
|
||||
end
|
||||
|
||||
"invalid" ->
|
||||
IO.puts("#{@red}Invalid#{@reset}")
|
||||
|
||||
_ ->
|
||||
IO.puts("#{@red}Unknown status: #{status}#{@reset}")
|
||||
end
|
||||
end
|
||||
|
||||
defp display_key_info_manual(response, extend) do
|
||||
# Simple manual parsing for JSON response
|
||||
status = extract_json_value(response, "status")
|
||||
public_key = extract_json_value(response, "public_key")
|
||||
tier = extract_json_value(response, "tier")
|
||||
expires_at = extract_json_value(response, "expires_at")
|
||||
|
||||
case status do
|
||||
"valid" ->
|
||||
IO.puts("#{@green}Valid#{@reset}")
|
||||
IO.puts("Public Key: #{public_key}")
|
||||
IO.puts("Tier: #{tier}")
|
||||
IO.puts("Expires: #{expires_at}")
|
||||
|
||||
if extend do
|
||||
open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
|
||||
end
|
||||
|
||||
"expired" ->
|
||||
IO.puts("#{@red}Expired#{@reset}")
|
||||
IO.puts("Public Key: #{public_key}")
|
||||
IO.puts("Tier: #{tier}")
|
||||
IO.puts("Expired: #{expires_at}")
|
||||
IO.puts("#{@yellow}To renew: Visit #{@portal_base}/keys/extend#{@reset}")
|
||||
|
||||
if extend do
|
||||
open_browser("#{@portal_base}/keys/extend?pk=#{public_key}")
|
||||
end
|
||||
|
||||
"invalid" ->
|
||||
IO.puts("#{@red}Invalid#{@reset}")
|
||||
|
||||
_ ->
|
||||
IO.puts("#{@red}Unknown status: #{status}#{@reset}")
|
||||
IO.puts(response)
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_json_value(json_str, key) do
|
||||
case Regex.run(~r/"#{key}"\s*:\s*"([^"]*)"/, json_str) do
|
||||
[_, value] -> value
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp open_browser(url) do
|
||||
IO.puts("#{@blue}Opening browser: #{url}#{@reset}")
|
||||
|
||||
case :os.type() do
|
||||
{:unix, :linux} ->
|
||||
System.cmd("xdg-open", [url], stderr_to_stdout: true)
|
||||
{:unix, :darwin} ->
|
||||
System.cmd("open", [url], stderr_to_stdout: true)
|
||||
{:win32, _} ->
|
||||
System.cmd("cmd", ["/c", "start", url], stderr_to_stdout: true)
|
||||
_ ->
|
||||
IO.puts("#{@yellow}Please open manually: #{url}#{@reset}")
|
||||
end
|
||||
end
|
||||
|
||||
# Helpers
|
||||
defp get_api_key do
|
||||
case System.get_env("UNSANDBOX_API_KEY") do
|
||||
|
|
@ -246,6 +378,22 @@ defmodule Un do
|
|||
output
|
||||
end
|
||||
|
||||
defp portal_curl_post(api_key, endpoint, json) do
|
||||
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
|
||||
File.write!(tmp_file, json)
|
||||
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
"-s", "-X", "POST",
|
||||
"#{@portal_base}#{endpoint}",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-H", "Authorization: Bearer #{api_key}",
|
||||
"-d", "@#{tmp_file}"
|
||||
], stderr_to_stdout: true)
|
||||
|
||||
File.rm(tmp_file)
|
||||
output
|
||||
end
|
||||
|
||||
defp curl_get(api_key, endpoint) do
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
"-s",
|
||||
|
|
|
|||
176
un.go
176
un.go
|
|
@ -54,18 +54,22 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
APIBase = "https://api.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{
|
||||
|
|
@ -454,6 +458,154 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
func openBrowser(url string) error {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", url)
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "start", url)
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
days := int(d.Hours() / 24)
|
||||
hours := int(d.Hours()) % 24
|
||||
minutes := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
||||
} else {
|
||||
return fmt.Sprintf("%dm", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
func validateKey(apiKey string, extend bool) {
|
||||
url := PortalBase + "/keys/validate"
|
||||
reqBody := bytes.NewBuffer(nil)
|
||||
|
||||
req, err := http.NewRequest("POST", url, reqBody)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError creating request: %v%s\n", Red, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError making request: %v%s\n", Red, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError reading response: %v%s\n", Red, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError parsing response: %v%s\n", Red, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
// Invalid key
|
||||
fmt.Printf("%sInvalid%s\n", Red, Reset)
|
||||
if reason, ok := result["error"].(string); ok {
|
||||
fmt.Printf("Reason: %s\n", reason)
|
||||
} else if reason, ok := result["message"].(string); ok {
|
||||
fmt.Printf("Reason: %s\n", reason)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
valid, _ := result["valid"].(bool)
|
||||
expired, _ := result["expired"].(bool)
|
||||
publicKey, _ := result["public_key"].(string)
|
||||
tier, _ := result["tier"].(string)
|
||||
status, _ := result["status"].(string)
|
||||
|
||||
if expired {
|
||||
// Expired key
|
||||
fmt.Printf("%sExpired%s\n", Red, Reset)
|
||||
fmt.Printf("Public Key: %s\n", publicKey)
|
||||
fmt.Printf("Tier: %s\n", tier)
|
||||
if expiresAt, ok := result["expires_at"].(string); ok {
|
||||
fmt.Printf("Expired: %s\n", expiresAt)
|
||||
}
|
||||
fmt.Printf("%sTo renew: Visit https://unsandbox.com/keys/extend%s\n", Yellow, Reset)
|
||||
|
||||
if extend {
|
||||
extendURL := PortalBase + "/keys/extend?pk=" + publicKey
|
||||
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
||||
if err := openBrowser(extendURL); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
||||
fmt.Printf("Please visit: %s\n", extendURL)
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if valid {
|
||||
// Valid key
|
||||
fmt.Printf("%sValid%s\n", Green, Reset)
|
||||
fmt.Printf("Public Key: %s\n", publicKey)
|
||||
fmt.Printf("Tier: %s\n", tier)
|
||||
fmt.Printf("Status: %s\n", status)
|
||||
|
||||
if expiresAt, ok := result["expires_at"].(string); ok {
|
||||
fmt.Printf("Expires: %s\n", expiresAt)
|
||||
|
||||
// Calculate time remaining
|
||||
expireTime, err := time.Parse(time.RFC3339, expiresAt)
|
||||
if err == nil {
|
||||
remaining := time.Until(expireTime)
|
||||
if remaining > 0 {
|
||||
fmt.Printf("Time Remaining: %s\n", formatDuration(remaining))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit, ok := result["rate_limit"].(float64); ok {
|
||||
fmt.Printf("Rate Limit: %.0f req/min\n", rateLimit)
|
||||
}
|
||||
if burst, ok := result["burst"].(float64); ok {
|
||||
fmt.Printf("Burst: %.0f req\n", burst)
|
||||
}
|
||||
if concurrency, ok := result["concurrency"].(float64); ok {
|
||||
fmt.Printf("Concurrency: %.0f\n", concurrency)
|
||||
}
|
||||
|
||||
if extend {
|
||||
extendURL := PortalBase + "/keys/extend?pk=" + publicKey
|
||||
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
||||
if err := openBrowser(extendURL); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
||||
fmt.Printf("Please visit: %s\n", extendURL)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Invalid key
|
||||
fmt.Printf("%sInvalid%s\n", Red, Reset)
|
||||
if reason, ok := result["error"].(string); ok {
|
||||
fmt.Printf("Reason: %s\n", reason)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Common flags
|
||||
apiKey := flag.String("k", "", "API key (or set UNSANDBOX_API_KEY)")
|
||||
|
|
@ -497,6 +649,11 @@ func main() {
|
|||
serviceVcpu := serviceCmd.Int("v", 0, "vCPU count")
|
||||
serviceKey := serviceCmd.String("k", "", "API key")
|
||||
|
||||
// Key flags
|
||||
keyCmd := flag.NewFlagSet("key", flag.ExitOnError)
|
||||
keyExtend := keyCmd.Bool("extend", false, "Open browser to extend key")
|
||||
keyKey := keyCmd.String("k", "", "API key")
|
||||
|
||||
// Parse
|
||||
flag.Parse()
|
||||
|
||||
|
|
@ -529,6 +686,12 @@ func main() {
|
|||
}
|
||||
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, net, vc, key)
|
||||
return
|
||||
|
||||
case "key":
|
||||
keyCmd.Parse(os.Args[2:])
|
||||
key := getAPIKey(*keyKey)
|
||||
validateKey(key, *keyExtend)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +700,7 @@ func main() {
|
|||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <source_file>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s session [options]\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s service [options]\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s key [options]\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
|
|||
191
un.hs
191
un.hs
|
|
@ -68,6 +68,10 @@ import Control.Monad (when, unless, forM_)
|
|||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
|
||||
-- API constants
|
||||
portalBase :: String
|
||||
portalBase = "https://unsandbox.com"
|
||||
|
||||
-- ANSI colors
|
||||
blue, red, green, yellow, reset :: String
|
||||
blue = "\x1b[34m"
|
||||
|
|
@ -105,7 +109,7 @@ escapeJSON = concatMap escape
|
|||
escape c = [c]
|
||||
|
||||
-- Parse command line arguments
|
||||
data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Help
|
||||
data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Key KeyOpts | Help
|
||||
|
||||
data ExecuteOpts = ExecuteOpts
|
||||
{ exFile :: String
|
||||
|
|
@ -140,12 +144,25 @@ data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
|
|||
| ServiceSleep String | ServiceWake String | ServiceDestroy String
|
||||
| ServiceCreate
|
||||
|
||||
data KeyOpts = KeyOpts
|
||||
{ keyExtend :: Bool
|
||||
}
|
||||
|
||||
-- Parse arguments
|
||||
parseArgs :: [String] -> IO Command
|
||||
parseArgs ("session":rest) = Session <$> parseSession rest
|
||||
parseArgs ("service":rest) = Service <$> parseService rest
|
||||
parseArgs ("key":rest) = Key <$> parseKey rest
|
||||
parseArgs args = parseExecute args
|
||||
|
||||
parseKey :: [String] -> IO KeyOpts
|
||||
parseKey args = return $ parseKeyArgs args defaultKeyOpts
|
||||
where
|
||||
defaultKeyOpts = KeyOpts False
|
||||
parseKeyArgs [] opts = opts
|
||||
parseKeyArgs ("--extend":rest) opts = parseKeyArgs rest opts { keyExtend = True }
|
||||
parseKeyArgs (_:rest) opts = parseKeyArgs rest opts
|
||||
|
||||
parseSession :: [String] -> IO SessionOpts
|
||||
parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
||||
where
|
||||
|
|
@ -208,6 +225,7 @@ main = do
|
|||
Execute opts -> executeCommand opts
|
||||
Session opts -> sessionCommand opts
|
||||
Service opts -> serviceCommand opts
|
||||
Key opts -> keyCommand opts
|
||||
Help -> printHelp
|
||||
|
||||
printHelp :: IO ()
|
||||
|
|
@ -216,6 +234,7 @@ printHelp = do
|
|||
putStrLn " un.hs [options] <source_file> Execute code"
|
||||
putStrLn " un.hs session [options] Manage sessions"
|
||||
putStrLn " un.hs service [options] Manage services"
|
||||
putStrLn " un.hs key [options] Validate/extend API key"
|
||||
putStrLn ""
|
||||
putStrLn "Execute options:"
|
||||
putStrLn " -e KEY=VALUE Environment variable"
|
||||
|
|
@ -224,6 +243,9 @@ printHelp = do
|
|||
putStrLn " -o DIR Output directory"
|
||||
putStrLn " -n MODE Network mode (zerotrust|semitrusted)"
|
||||
putStrLn " -v N vCPU count (1-8)"
|
||||
putStrLn ""
|
||||
putStrLn "Key options:"
|
||||
putStrLn " --extend Open browser to extend/renew key"
|
||||
exitFailure
|
||||
|
||||
-- Execute command
|
||||
|
|
@ -259,7 +281,7 @@ executeCommand opts = do
|
|||
++ envJSON ++ filesJSON ++ artifactsJSON ++ networkJSON ++ vcpuJSON ++ "}"
|
||||
|
||||
-- Call API
|
||||
(exitCode, stdout, stderr) <- curlPost apiKey "/execute" json
|
||||
(exitCode, stdout, stderr) <- curlPost apiKey "https://api.unsandbox.com/execute" json
|
||||
|
||||
-- Print output
|
||||
unless (null stdout) $ putStr $ blue ++ stdout ++ reset
|
||||
|
|
@ -275,17 +297,17 @@ sessionCommand opts = do
|
|||
apiKey <- getApiKey
|
||||
case sessAction opts of
|
||||
SessionList -> do
|
||||
(_, stdout, _) <- curlGet apiKey "/sessions"
|
||||
(_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/sessions"
|
||||
putStrLn stdout
|
||||
SessionKill sid -> do
|
||||
(_, stdout, _) <- curlDelete apiKey ("/sessions/" ++ sid)
|
||||
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/sessions/" ++ sid)
|
||||
putStrLn $ green ++ "Session terminated: " ++ sid ++ reset
|
||||
SessionCreate -> do
|
||||
let shell = maybe "bash" id (sessShell opts)
|
||||
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (sessNetwork opts)
|
||||
let vcpuJSON = maybe "" (\v -> ",\"vcpu\":" ++ show v) (sessVcpu opts)
|
||||
let json = "{\"shell\":\"" ++ shell ++ "\"" ++ networkJSON ++ vcpuJSON ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey "/sessions" json
|
||||
(_, stdout, _) <- curlPost apiKey "https://api.unsandbox.com/sessions" json
|
||||
putStrLn $ yellow ++ "Session created (WebSocket required for interactivity)" ++ reset
|
||||
putStrLn stdout
|
||||
|
||||
|
|
@ -295,22 +317,22 @@ serviceCommand opts = do
|
|||
apiKey <- getApiKey
|
||||
case svcAction opts of
|
||||
ServiceList -> do
|
||||
(_, stdout, _) <- curlGet apiKey "/services"
|
||||
(_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/services"
|
||||
putStrLn stdout
|
||||
ServiceInfo sid -> do
|
||||
(_, stdout, _) <- curlGet apiKey ("/services/" ++ sid)
|
||||
(_, stdout, _) <- curlGet apiKey ("https://api.unsandbox.com/services/" ++ sid)
|
||||
putStrLn stdout
|
||||
ServiceLogs sid -> do
|
||||
(_, stdout, _) <- curlGet apiKey ("/services/" ++ sid ++ "/logs")
|
||||
(_, stdout, _) <- curlGet apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/logs")
|
||||
putStrLn stdout
|
||||
ServiceSleep sid -> do
|
||||
(_, stdout, _) <- curlPost apiKey ("/services/" ++ sid ++ "/sleep") "{}"
|
||||
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/sleep") "{}"
|
||||
putStrLn $ green ++ "Service sleeping: " ++ sid ++ reset
|
||||
ServiceWake sid -> do
|
||||
(_, stdout, _) <- curlPost apiKey ("/services/" ++ sid ++ "/wake") "{}"
|
||||
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/wake") "{}"
|
||||
putStrLn $ green ++ "Service waking: " ++ sid ++ reset
|
||||
ServiceDestroy sid -> do
|
||||
(_, stdout, _) <- curlDelete apiKey ("/services/" ++ sid)
|
||||
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/services/" ++ sid)
|
||||
putStrLn $ green ++ "Service destroyed: " ++ sid ++ reset
|
||||
ServiceCreate -> do
|
||||
case svcName opts of
|
||||
|
|
@ -324,16 +346,16 @@ serviceCommand opts = do
|
|||
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (svcNetwork opts)
|
||||
let vcpuJSON = maybe "" (\v -> ",\"vcpu\":" ++ show v) (svcVcpu opts)
|
||||
let json = "{\"name\":\"" ++ name ++ "\"" ++ portsJSON ++ typeJSON ++ bootstrapJSON ++ networkJSON ++ vcpuJSON ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey "/services" json
|
||||
(_, stdout, _) <- curlPost apiKey "https://api.unsandbox.com/services" json
|
||||
putStrLn $ green ++ "Service created" ++ reset
|
||||
putStrLn stdout
|
||||
|
||||
-- HTTP helpers using curl
|
||||
curlPost :: String -> String -> String -> IO (ExitCode, String, String)
|
||||
curlPost apiKey endpoint body = do
|
||||
curlPost apiKey url body = do
|
||||
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
|
||||
[ "-s", "-X", "POST"
|
||||
, "https://api.unsandbox.com" ++ endpoint
|
||||
, url
|
||||
, "-H", "Content-Type: application/json"
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
, "-d", body
|
||||
|
|
@ -341,17 +363,17 @@ curlPost apiKey endpoint body = do
|
|||
return (exitCode, stdout, stderr)
|
||||
|
||||
curlGet :: String -> String -> IO (ExitCode, String, String)
|
||||
curlGet apiKey endpoint =
|
||||
curlGet apiKey url =
|
||||
readProcessWithExitCode "curl"
|
||||
[ "-s", "https://api.unsandbox.com" ++ endpoint
|
||||
[ "-s", url
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
] ""
|
||||
|
||||
curlDelete :: String -> String -> IO (ExitCode, String, String)
|
||||
curlDelete apiKey endpoint =
|
||||
curlDelete apiKey url =
|
||||
readProcessWithExitCode "curl"
|
||||
[ "-s", "-X", "DELETE"
|
||||
, "https://api.unsandbox.com" ++ endpoint
|
||||
, url
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
] ""
|
||||
|
||||
|
|
@ -377,3 +399,136 @@ parseExitCode resp =
|
|||
Just (_, ':':v) -> Just $ takeWhile isDigit v
|
||||
_ -> Nothing
|
||||
find f = foldr (\x acc -> if f x then Just x else acc) Nothing
|
||||
|
||||
-- Extract JSON string field (simple parser for basic cases)
|
||||
extractJsonString :: String -> String -> Maybe String
|
||||
extractJsonString json field =
|
||||
case break (== '"') rest of
|
||||
(_, '"':value) ->
|
||||
case break (== '"') value of
|
||||
(v, _) -> Just v
|
||||
_ -> Nothing
|
||||
where
|
||||
needle = "\"" ++ field ++ "\":"
|
||||
rest = case dropWhile (/= '"') $ dropWhile (not . isPrefixOf needle) $ tails json of
|
||||
(_:xs) -> case dropWhile (/= ':') xs of
|
||||
(_:ys) -> dropWhile (`elem` " \t\n") ys
|
||||
_ -> ""
|
||||
_ -> ""
|
||||
tails [] = [[]]
|
||||
tails s@(_:xs) = s : tails xs
|
||||
|
||||
-- Key command
|
||||
keyCommand :: KeyOpts -> IO ()
|
||||
keyCommand opts = do
|
||||
apiKey <- getApiKey
|
||||
|
||||
if keyExtend opts
|
||||
then extendKey apiKey
|
||||
else validateKey apiKey
|
||||
|
||||
-- Validate API key and show status
|
||||
validateKey :: String -> IO ()
|
||||
validateKey apiKey = do
|
||||
let url = portalBase ++ "/keys/validate"
|
||||
(exitCode, stdout, stderr) <- curlPost apiKey url "{}"
|
||||
|
||||
-- Check if valid:false appears in response
|
||||
let isInvalid = "\"valid\":false" `isPrefixOf` dropWhile (/= 'v') stdout
|
||||
|
||||
if exitCode /= ExitSuccess || isInvalid
|
||||
then do
|
||||
-- Parse error response
|
||||
let reason = extractJsonString stdout "reason"
|
||||
case reason of
|
||||
Just "expired" -> do
|
||||
putStrLn $ red ++ "Expired" ++ reset ++ "\n"
|
||||
|
||||
-- Show key details
|
||||
case extractJsonString stdout "public_key" of
|
||||
Just pk -> putStrLn $ "Public Key: " ++ pk
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "tier" of
|
||||
Just tier -> putStrLn $ "Tier: " ++ tier
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "expired_at_datetime" of
|
||||
Just expiredAt -> do
|
||||
putStr $ "Expired: " ++ expiredAt
|
||||
case extractJsonString stdout "expired_ago" of
|
||||
Just ago -> putStrLn $ " (" ++ ago ++ ")"
|
||||
Nothing -> putStrLn ""
|
||||
Nothing -> return ()
|
||||
|
||||
putStrLn ""
|
||||
putStrLn $ yellow ++ "To renew:" ++ reset ++ " Visit https://unsandbox.com/keys/extend"
|
||||
exitFailure
|
||||
|
||||
Just "invalid_key" -> do
|
||||
putStrLn $ red ++ "Invalid" ++ reset ++ ": key not found"
|
||||
exitFailure
|
||||
|
||||
Just "suspended" -> do
|
||||
putStrLn $ red ++ "Suspended" ++ reset ++ ": key has been suspended"
|
||||
exitFailure
|
||||
|
||||
_ -> do
|
||||
putStrLn $ red ++ "Invalid key" ++ reset
|
||||
exitFailure
|
||||
else do
|
||||
-- Parse valid response
|
||||
putStrLn $ green ++ "Valid" ++ reset ++ "\n"
|
||||
|
||||
case extractJsonString stdout "public_key" of
|
||||
Just pk -> putStrLn $ "Public Key: " ++ pk
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "tier" of
|
||||
Just tier -> putStrLn $ "Tier: " ++ tier
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "status" of
|
||||
Just status -> putStrLn $ "Status: " ++ status
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "valid_through_datetime" of
|
||||
Just validThrough -> putStrLn $ "Expires: " ++ validThrough
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "valid_for_human" of
|
||||
Just validFor -> putStrLn $ "Time Remaining: " ++ validFor
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "rate_per_minute" of
|
||||
Just rate -> putStrLn $ "Rate Limit: " ++ rate ++ "/min"
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "burst" of
|
||||
Just burst -> putStrLn $ "Burst: " ++ burst
|
||||
Nothing -> return ()
|
||||
|
||||
case extractJsonString stdout "concurrency" of
|
||||
Just conc -> putStrLn $ "Concurrency: " ++ conc
|
||||
Nothing -> return ()
|
||||
|
||||
-- Extend key (open browser to extend page)
|
||||
extendKey :: String -> IO ()
|
||||
extendKey apiKey = do
|
||||
let url = portalBase ++ "/keys/validate"
|
||||
(exitCode, stdout, _) <- curlPost apiKey url "{}"
|
||||
|
||||
case extractJsonString stdout "public_key" of
|
||||
Nothing -> do
|
||||
hPutStrLn stderr "Error: Invalid key or could not retrieve public key"
|
||||
exitFailure
|
||||
Just publicKey -> do
|
||||
let extendUrl = portalBase ++ "/keys/extend?pk=" ++ publicKey
|
||||
putStrLn "Opening extension page in browser..."
|
||||
putStrLn $ "If browser doesn't open, visit: " ++ extendUrl
|
||||
|
||||
-- Try to open URL in browser (Linux-specific)
|
||||
_ <- readProcessWithExitCode "sh"
|
||||
["-c", "xdg-open '" ++ extendUrl ++ "' 2>/dev/null || sensible-browser '" ++ extendUrl ++ "' 2>/dev/null || true"]
|
||||
""
|
||||
return ()
|
||||
|
|
|
|||
160
un.jl
Normal file → Executable file
160
un.jl
Normal file → Executable file
|
|
@ -41,6 +41,7 @@ using HTTP
|
|||
using JSON
|
||||
using Base64
|
||||
using ArgParse
|
||||
using Printf
|
||||
|
||||
# Extension to language mapping
|
||||
const EXT_MAP = Dict(
|
||||
|
|
@ -67,6 +68,7 @@ const YELLOW = "\033[33m"
|
|||
const RESET = "\033[0m"
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com"
|
||||
const PORTAL_BASE = "https://unsandbox.com"
|
||||
|
||||
function detect_language(filename::String)::String
|
||||
ext = lowercase(match(r"\.[^.]+$", filename).match)
|
||||
|
|
@ -334,6 +336,149 @@ function cmd_service(args)
|
|||
exit(1)
|
||||
end
|
||||
|
||||
function validate_key(api_key::String)
|
||||
url = PORTAL_BASE * "/keys/validate"
|
||||
headers = [
|
||||
"Authorization" => "Bearer $api_key",
|
||||
"Content-Type" => "application/json"
|
||||
]
|
||||
|
||||
try
|
||||
response = HTTP.post(url, headers, "{}", readtimeout=300)
|
||||
data = JSON.parse(String(response.body))
|
||||
|
||||
# Check if valid
|
||||
if get(data, "valid", false)
|
||||
# Print valid key info
|
||||
println("$(GREEN)Valid$(RESET)\n")
|
||||
println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Status:", get(data, "status", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Expires:", get(data, "valid_through_datetime", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Time Remaining:", get(data, "valid_for_human", "N/A")))
|
||||
println(@sprintf("%-20s %s/min", "Rate Limit:", get(data, "rate_per_minute", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Burst:", get(data, "burst", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Concurrency:", get(data, "concurrency", "N/A")))
|
||||
return 0
|
||||
else
|
||||
# Handle invalid response
|
||||
reason = get(data, "reason", "unknown")
|
||||
if reason == "expired"
|
||||
println("$(RED)Expired$(RESET)\n")
|
||||
println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A")))
|
||||
expired_at = get(data, "expired_at_datetime", "N/A")
|
||||
expired_ago = get(data, "expired_ago", "")
|
||||
if !isempty(expired_ago)
|
||||
println(@sprintf("%-20s %s (%s)", "Expired:", expired_at, expired_ago))
|
||||
else
|
||||
println(@sprintf("%-20s %s", "Expired:", expired_at))
|
||||
end
|
||||
renew_url = get(data, "renew_url", "https://unsandbox.com/pricing")
|
||||
println("\n$(YELLOW)To renew:$(RESET) Visit $renew_url")
|
||||
elseif reason == "invalid_key"
|
||||
println("$(RED)Invalid$(RESET): key not found")
|
||||
elseif reason == "suspended"
|
||||
println("$(RED)Suspended$(RESET): key has been suspended")
|
||||
else
|
||||
println("$(RED)Invalid$(RESET): $reason")
|
||||
end
|
||||
return 1
|
||||
end
|
||||
catch e
|
||||
if isa(e, HTTP.ExceptionRequest.StatusError)
|
||||
# Parse error response from body
|
||||
try
|
||||
data = JSON.parse(String(e.response.body))
|
||||
reason = get(data, "reason", "unknown")
|
||||
|
||||
if reason == "expired"
|
||||
println("$(RED)Expired$(RESET)\n")
|
||||
println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A")))
|
||||
println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A")))
|
||||
expired_at = get(data, "expired_at_datetime", "N/A")
|
||||
expired_ago = get(data, "expired_ago", "")
|
||||
if !isempty(expired_ago)
|
||||
println(@sprintf("%-20s %s (%s)", "Expired:", expired_at, expired_ago))
|
||||
else
|
||||
println(@sprintf("%-20s %s", "Expired:", expired_at))
|
||||
end
|
||||
renew_url = get(data, "renew_url", "https://unsandbox.com/pricing")
|
||||
println("\n$(YELLOW)To renew:$(RESET) Visit $renew_url")
|
||||
elseif reason == "invalid_key"
|
||||
println("$(RED)Invalid$(RESET): key not found")
|
||||
elseif reason == "suspended"
|
||||
println("$(RED)Suspended$(RESET): key has been suspended")
|
||||
else
|
||||
println("$(RED)Invalid$(RESET): $reason")
|
||||
end
|
||||
catch
|
||||
println(stderr, "$(RED)Error: HTTP $(e.status)$(RESET)")
|
||||
end
|
||||
return 1
|
||||
else
|
||||
println(stderr, "$(RED)Error: Request failed: $e$(RESET)")
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function cmd_key(args)
|
||||
api_key = get_api_key(args["api-key"])
|
||||
|
||||
# Handle --extend flag
|
||||
if args["extend"]
|
||||
# Validate key to get public key
|
||||
url = PORTAL_BASE * "/keys/validate"
|
||||
headers = [
|
||||
"Authorization" => "Bearer $api_key",
|
||||
"Content-Type" => "application/json"
|
||||
]
|
||||
|
||||
try
|
||||
response = HTTP.post(url, headers, "{}", readtimeout=300)
|
||||
data = JSON.parse(String(response.body))
|
||||
|
||||
public_key = get(data, "public_key", nothing)
|
||||
if public_key === nothing
|
||||
println(stderr, "$(RED)Error: Invalid key or could not retrieve public key$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Build extend URL
|
||||
extend_url = "$(PORTAL_BASE)/keys/extend?pk=$(public_key)"
|
||||
|
||||
println("Opening extension page in browser...")
|
||||
println("If browser doesn't open, visit: $extend_url")
|
||||
|
||||
# Try to open browser
|
||||
if Sys.isapple()
|
||||
run(`open $extend_url`)
|
||||
elseif Sys.islinux()
|
||||
try
|
||||
run(`xdg-open $extend_url`)
|
||||
catch
|
||||
try
|
||||
run(`sensible-browser $extend_url`)
|
||||
catch
|
||||
# Already printed the URL
|
||||
end
|
||||
end
|
||||
elseif Sys.iswindows()
|
||||
run(`cmd /c start $extend_url`)
|
||||
end
|
||||
|
||||
exit(0)
|
||||
catch e
|
||||
println(stderr, "$(RED)Error: Failed to validate key: $e$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
# Default: validate and display key info
|
||||
exit(validate_key(api_key))
|
||||
end
|
||||
|
||||
function main()
|
||||
s = ArgParseSettings(description="Unsandbox CLI - Execute code in secure sandboxes")
|
||||
|
||||
|
|
@ -364,6 +509,9 @@ function main()
|
|||
"service"
|
||||
help = "Manage persistent services"
|
||||
action = :command
|
||||
"key"
|
||||
help = "Check API key validity and expiration"
|
||||
action = :command
|
||||
end
|
||||
|
||||
@add_arg_table! s["session"] begin
|
||||
|
|
@ -412,16 +560,26 @@ function main()
|
|||
help = "API key"
|
||||
end
|
||||
|
||||
@add_arg_table! s["key"] begin
|
||||
"--extend"
|
||||
help = "Open browser to extend/renew key"
|
||||
action = :store_true
|
||||
"--api-key", "-k"
|
||||
help = "API key"
|
||||
end
|
||||
|
||||
args = parse_args(ARGS, s)
|
||||
|
||||
if args["%COMMAND%"] == "session"
|
||||
cmd_session(args["session"])
|
||||
elseif args["%COMMAND%"] == "service"
|
||||
cmd_service(args["service"])
|
||||
elseif args["%COMMAND%"] == "key"
|
||||
cmd_key(args["key"])
|
||||
elseif args["source_file"] !== nothing
|
||||
cmd_execute(args)
|
||||
else
|
||||
println(stderr, "$(RED)Error: Provide source_file or use 'session'/'service' subcommand$(RESET)")
|
||||
println(stderr, "$(RED)Error: Provide source_file or use 'session'/'service'/'key' subcommand$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
133
un.js
133
un.js
|
|
@ -54,8 +54,10 @@
|
|||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
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";
|
||||
|
|
@ -152,6 +154,124 @@ function apiRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
|||
});
|
||||
}
|
||||
|
||||
function portalRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(PORTAL_BASE + endpoint);
|
||||
const options = {
|
||||
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', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch (e) {
|
||||
resolve(body);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const errorBody = JSON.parse(body);
|
||||
resolve({ error: errorBody.error || body, status: res.statusCode });
|
||||
} catch (e) {
|
||||
resolve({ error: body, status: res.statusCode });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function openBrowser(url) {
|
||||
const platform = process.platform;
|
||||
let command;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
command = `open "${url}"`;
|
||||
} else if (platform === 'win32') {
|
||||
command = `start "${url}"`;
|
||||
} else {
|
||||
command = `xdg-open "${url}"`;
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
console.error(`${RED}Error opening browser: ${error.message}${RESET}`);
|
||||
console.log(`Please visit: ${url}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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'}`);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdKey(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
await validateKey(apiKey, args.extend);
|
||||
}
|
||||
|
||||
async function cmdExecute(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
|
||||
|
|
@ -377,13 +497,14 @@ function parseArgs(argv) {
|
|||
destroy: null,
|
||||
execute: null,
|
||||
command_arg: null,
|
||||
extend: false,
|
||||
};
|
||||
|
||||
let i = 2;
|
||||
while (i < argv.length) {
|
||||
const arg = argv[i];
|
||||
|
||||
if (arg === 'session' || arg === 'service') {
|
||||
if (arg === 'session' || arg === 'service' || arg === 'key') {
|
||||
args.command = arg;
|
||||
i++;
|
||||
} else if (arg === '-e' && i + 1 < argv.length) {
|
||||
|
|
@ -467,6 +588,9 @@ function parseArgs(argv) {
|
|||
} else if (arg === '--command' && i + 1 < argv.length) {
|
||||
args.command_arg = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--extend') {
|
||||
args.extend = true;
|
||||
i++;
|
||||
} else if (!arg.startsWith('-')) {
|
||||
args.sourceFile = arg;
|
||||
i++;
|
||||
|
|
@ -486,6 +610,8 @@ async function main() {
|
|||
await cmdSession(args);
|
||||
} else if (args.command === 'service') {
|
||||
await cmdService(args);
|
||||
} else if (args.command === 'key') {
|
||||
await cmdKey(args);
|
||||
} else if (args.sourceFile) {
|
||||
await cmdExecute(args);
|
||||
} else {
|
||||
|
|
@ -495,6 +621,7 @@ Usage:
|
|||
${process.argv[1]} [options] <source_file>
|
||||
${process.argv[1]} session [options]
|
||||
${process.argv[1]} service [options]
|
||||
${process.argv[1]} key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -529,6 +656,10 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key expiration
|
||||
-k KEY API key to validate
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
82
un.kt
82
un.kt
|
|
@ -47,6 +47,7 @@ import java.util.Base64
|
|||
import kotlin.system.exitProcess
|
||||
|
||||
val API_BASE = "https://api.unsandbox.com"
|
||||
val PORTAL_BASE = "https://unsandbox.com"
|
||||
val BLUE = "\u001B[34m"
|
||||
val RED = "\u001B[31m"
|
||||
val GREEN = "\u001B[32m"
|
||||
|
|
@ -92,7 +93,8 @@ data class Args(
|
|||
var serviceTail: String? = null,
|
||||
var serviceSleep: String? = null,
|
||||
var serviceWake: String? = null,
|
||||
var serviceDestroy: String? = null
|
||||
var serviceDestroy: String? = null,
|
||||
var keyExtend: Boolean = false
|
||||
)
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
|
@ -102,6 +104,7 @@ fun main(args: Array<String>) {
|
|||
when (parsedArgs.command) {
|
||||
"session" -> cmdSession(parsedArgs)
|
||||
"service" -> cmdService(parsedArgs)
|
||||
"key" -> cmdKey(parsedArgs)
|
||||
else -> if (parsedArgs.sourceFile != null) {
|
||||
cmdExecute(parsedArgs)
|
||||
} else {
|
||||
|
|
@ -329,6 +332,77 @@ fun cmdService(args: Args) {
|
|||
exitProcess(1)
|
||||
}
|
||||
|
||||
fun cmdKey(args: Args) {
|
||||
val apiKey = getApiKey(args.apiKey)
|
||||
|
||||
val result = validateKey(apiKey)
|
||||
val valid = result["valid"] as? Boolean ?: false
|
||||
val expired = result["expired"] as? Boolean ?: false
|
||||
val publicKey = result["public_key"] as? String ?: ""
|
||||
val tier = result["tier"] as? String ?: ""
|
||||
val expiresAt = result["expires_at"] as? String ?: ""
|
||||
|
||||
if (args.keyExtend) {
|
||||
if (publicKey.isEmpty()) {
|
||||
System.err.println("${RED}Error: Could not retrieve public key${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
val extendUrl = "$PORTAL_BASE/keys/extend?pk=$publicKey"
|
||||
println("${YELLOW}Opening browser to extend key...${RESET}")
|
||||
println(extendUrl)
|
||||
|
||||
// Try to open browser using common commands
|
||||
val osName = System.getProperty("os.name").lowercase()
|
||||
val openCmd = when {
|
||||
osName.contains("mac") || osName.contains("darwin") -> "open"
|
||||
osName.contains("win") -> "start"
|
||||
else -> "xdg-open"
|
||||
}
|
||||
|
||||
try {
|
||||
Runtime.getRuntime().exec(arrayOf(openCmd, extendUrl))
|
||||
} catch (e: Exception) {
|
||||
println("${YELLOW}Could not open browser automatically. Please visit the URL above.${RESET}")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
println("${RED}Status: Expired${RESET}")
|
||||
println("Public Key: $publicKey")
|
||||
println("Tier: $tier")
|
||||
println("Expired: $expiresAt")
|
||||
println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}")
|
||||
} else if (valid) {
|
||||
println("${GREEN}Status: Valid${RESET}")
|
||||
println("Public Key: $publicKey")
|
||||
println("Tier: $tier")
|
||||
println("Expires: $expiresAt")
|
||||
} else {
|
||||
println("${RED}Status: Invalid${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
fun validateKey(apiKey: String): Map<String, Any> {
|
||||
val url = URL("$PORTAL_BASE/keys/validate")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Authorization", "Bearer $apiKey")
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.connectTimeout = 30000
|
||||
connection.readTimeout = 30000
|
||||
|
||||
if (connection.responseCode !in 200..299) {
|
||||
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
|
||||
throw RuntimeException("HTTP ${connection.responseCode} - $error")
|
||||
}
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
return parseJson(response)
|
||||
}
|
||||
|
||||
fun getApiKey(argsKey: String?): String {
|
||||
val key = argsKey ?: System.getenv("UNSANDBOX_API_KEY")
|
||||
if (key.isNullOrEmpty()) {
|
||||
|
|
@ -500,6 +574,7 @@ fun parseArgs(args: Array<String>): Args {
|
|||
when (args[i]) {
|
||||
"session" -> result.command = "session"
|
||||
"service" -> result.command = "service"
|
||||
"key" -> result.command = "key"
|
||||
"-k", "--api-key" -> result.apiKey = args[++i]
|
||||
"-n", "--network" -> result.network = args[++i]
|
||||
"-v", "--vcpu" -> result.vcpu = args[++i].toInt()
|
||||
|
|
@ -525,6 +600,7 @@ fun parseArgs(args: Array<String>): Args {
|
|||
"--sleep" -> result.serviceSleep = args[++i]
|
||||
"--wake" -> result.serviceWake = args[++i]
|
||||
"--destroy" -> result.serviceDestroy = args[++i]
|
||||
"--extend" -> result.keyExtend = true
|
||||
else -> if (!args[i].startsWith("-")) result.sourceFile = args[i]
|
||||
}
|
||||
i++
|
||||
|
|
@ -537,6 +613,7 @@ fun printHelp() {
|
|||
Usage: kotlin UnKt [options] <source_file>
|
||||
kotlin UnKt session [options]
|
||||
kotlin UnKt service [options]
|
||||
kotlin UnKt key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Set environment variable
|
||||
|
|
@ -564,5 +641,8 @@ Service options:
|
|||
--sleep ID Freeze service
|
||||
--wake ID Unfreeze service
|
||||
--destroy ID Destroy service
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key
|
||||
""".trimIndent())
|
||||
}
|
||||
|
|
|
|||
94
un.lisp
94
un.lisp
|
|
@ -53,6 +53,8 @@
|
|||
(defparameter *yellow* (format nil "~C[33m" #\Escape))
|
||||
(defparameter *reset* (format nil "~C[0m" #\Escape))
|
||||
|
||||
(defparameter *portal-base* "https://unsandbox.com")
|
||||
|
||||
(defparameter *ext-map*
|
||||
'((".hs" . "haskell") (".ml" . "ocaml") (".clj" . "clojure")
|
||||
(".scm" . "scheme") (".lisp" . "commonlisp") (".erl" . "erlang")
|
||||
|
|
@ -119,6 +121,16 @@
|
|||
(format nil "https://api.unsandbox.com~a" endpoint)
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key))))
|
||||
|
||||
(defun curl-post-portal (api-key endpoint json-data)
|
||||
(let ((tmp-file (write-temp-file json-data)))
|
||||
(unwind-protect
|
||||
(run-curl (list "curl" "-s" "-X" "POST"
|
||||
(format nil "~a~a" *portal-base* endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key)
|
||||
"-d" (format nil "@~a" tmp-file)))
|
||||
(delete-file tmp-file))))
|
||||
|
||||
(defun get-api-key ()
|
||||
(or (uiop:getenv "UNSANDBOX_API_KEY")
|
||||
(progn
|
||||
|
|
@ -183,6 +195,84 @@
|
|||
(format t "Error: --name required to create service~%")
|
||||
(uiop:quit 1)))))
|
||||
|
||||
(defun parse-json-field (json field)
|
||||
"Simple JSON field parser - extracts value for given field"
|
||||
(let* ((field-pattern (format nil "\"~a\":" field))
|
||||
(start (search field-pattern json)))
|
||||
(when start
|
||||
(let* ((value-start (+ start (length field-pattern)))
|
||||
(first-char (char json value-start)))
|
||||
(cond
|
||||
((char= first-char #\")
|
||||
;; String value
|
||||
(let ((str-start (1+ value-start)))
|
||||
(loop for i from str-start below (length json)
|
||||
when (and (char= (char json i) #\")
|
||||
(not (char= (char json (1- i)) #\\)))
|
||||
return (subseq json str-start i))))
|
||||
((char= first-char #\{)
|
||||
;; Object value - skip for now
|
||||
nil)
|
||||
((char= first-char #\[)
|
||||
;; Array value - skip for now
|
||||
nil)
|
||||
(t
|
||||
;; Number, boolean, or null
|
||||
(let ((end (or (position #\, json :start value-start)
|
||||
(position #\} json :start value-start)
|
||||
(length json))))
|
||||
(string-trim '(#\Space #\Tab #\Newline #\Return)
|
||||
(subseq json value-start end)))))))))
|
||||
|
||||
(defun open-browser (url)
|
||||
"Open URL in default browser"
|
||||
(uiop:run-program (list "xdg-open" url) :ignore-error-status t))
|
||||
|
||||
(defun validate-key (extend-flag)
|
||||
(let* ((api-key (get-api-key))
|
||||
(response (curl-post-portal api-key "/keys/validate" "{}"))
|
||||
(status (parse-json-field response "status"))
|
||||
(public-key (parse-json-field response "public_key"))
|
||||
(tier (parse-json-field response "tier"))
|
||||
(expires-at (parse-json-field response "expires_at")))
|
||||
|
||||
(cond
|
||||
((string= status "valid")
|
||||
(format t "~aValid~a~%" *green* *reset*)
|
||||
(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))
|
||||
(when extend-flag
|
||||
(if public-key
|
||||
(let ((extend-url (format nil "~a/keys/extend?pk=~a" *portal-base* public-key)))
|
||||
(format t "~aOpening browser to extend key...~a~%" *blue* *reset*)
|
||||
(open-browser extend-url))
|
||||
(format t "~aError: No public_key in response~a~%" *red* *reset*))))
|
||||
|
||||
((string= status "expired")
|
||||
(format t "~aExpired~a~%" *red* *reset*)
|
||||
(when public-key (format t "Public Key: ~a~%" public-key))
|
||||
(when tier (format t "Tier: ~a~%" tier))
|
||||
(when expires-at (format t "Expired: ~a~%" expires-at))
|
||||
(format t "~aTo renew: Visit ~a/keys/extend~a~%" *yellow* *portal-base* *reset*)
|
||||
(when extend-flag
|
||||
(if public-key
|
||||
(let ((extend-url (format nil "~a/keys/extend?pk=~a" *portal-base* public-key)))
|
||||
(format t "~aOpening browser to extend key...~a~%" *blue* *reset*)
|
||||
(open-browser extend-url))
|
||||
(format t "~aError: No public_key in response~a~%" *red* *reset*))))
|
||||
|
||||
((string= status "invalid")
|
||||
(format t "~aInvalid~a~%" *red* *reset*)
|
||||
(format t "Response: ~a~%" response))
|
||||
|
||||
(t
|
||||
(format t "~aUnknown status~a~%" *red* *reset*)
|
||||
(format t "Response: ~a~%" response)))))
|
||||
|
||||
(defun key-cmd (extend-flag)
|
||||
(validate-key extend-flag))
|
||||
|
||||
(defun main ()
|
||||
(let ((args (uiop:command-line-arguments)))
|
||||
(if (null args)
|
||||
|
|
@ -190,6 +280,7 @@
|
|||
(format t "Usage: un.lisp [options] <source_file>~%")
|
||||
(format t " un.lisp session [options]~%")
|
||||
(format t " un.lisp service [options]~%")
|
||||
(format t " un.lisp key [--extend]~%")
|
||||
(uiop:quit 1))
|
||||
(cond
|
||||
((string= (first args) "session")
|
||||
|
|
@ -231,6 +322,9 @@
|
|||
(t
|
||||
(format t "Error: Invalid service command~%")
|
||||
(uiop:quit 1))))
|
||||
((string= (first args) "key")
|
||||
(let ((extend-flag (and (> (length args) 1) (string= (second args) "--extend"))))
|
||||
(key-cmd extend-flag)))
|
||||
(t
|
||||
(execute-cmd (first args)))))))
|
||||
|
||||
|
|
|
|||
99
un.lua
99
un.lua
|
|
@ -53,6 +53,7 @@
|
|||
local json = require("cjson")
|
||||
|
||||
local API_BASE = "https://api.unsandbox.com"
|
||||
local PORTAL_BASE = "https://unsandbox.com"
|
||||
local BLUE = "\27[34m"
|
||||
local RED = "\27[31m"
|
||||
local GREEN = "\27[32m"
|
||||
|
|
@ -303,6 +304,91 @@ local function cmd_session(options)
|
|||
print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET)
|
||||
end
|
||||
|
||||
local function cmd_key(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
|
||||
if options.extend then
|
||||
-- Get public_key from validation response
|
||||
local url = PORTAL_BASE .. "/keys/validate"
|
||||
local tmpfile = os.tmpname()
|
||||
|
||||
local cmd = "curl -s -X POST " .. shell_escape(url) ..
|
||||
" -H 'Authorization: Bearer " .. api_key .. "'" ..
|
||||
" -H 'Content-Type: application/json'" ..
|
||||
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
|
||||
|
||||
local handle = io.popen(cmd)
|
||||
local http_code = handle:read("*a"):match("(%d+)$")
|
||||
handle:close()
|
||||
|
||||
local file = io.open(tmpfile, "r")
|
||||
local response = file:read("*all")
|
||||
file:close()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not http_code or tonumber(http_code) < 200 or tonumber(http_code) >= 300 then
|
||||
io.stderr:write(RED .. "Error: HTTP " .. (http_code or "000") .. " - " .. response .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
local result = json.decode(response)
|
||||
local public_key = result.public_key
|
||||
|
||||
if not public_key then
|
||||
io.stderr:write(RED .. "Error: Could not retrieve public key" .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
-- Open browser with extend URL
|
||||
local extend_url = PORTAL_BASE .. "/keys/extend?pk=" .. public_key
|
||||
print(GREEN .. "Opening browser to extend key..." .. RESET)
|
||||
print(extend_url)
|
||||
os.execute("xdg-open " .. shell_escape(extend_url) .. " 2>/dev/null || open " .. shell_escape(extend_url) .. " 2>/dev/null")
|
||||
return
|
||||
end
|
||||
|
||||
-- Validate key (default action)
|
||||
local url = PORTAL_BASE .. "/keys/validate"
|
||||
local tmpfile = os.tmpname()
|
||||
|
||||
local cmd = "curl -s -X POST " .. shell_escape(url) ..
|
||||
" -H 'Authorization: Bearer " .. api_key .. "'" ..
|
||||
" -H 'Content-Type: application/json'" ..
|
||||
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
|
||||
|
||||
local handle = io.popen(cmd)
|
||||
local http_code = handle:read("*a"):match("(%d+)$")
|
||||
handle:close()
|
||||
|
||||
local file = io.open(tmpfile, "r")
|
||||
local response = file:read("*all")
|
||||
file:close()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not http_code or tonumber(http_code) < 200 or tonumber(http_code) >= 300 then
|
||||
io.stderr:write(RED .. "Error: Invalid API key" .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
local result = json.decode(response)
|
||||
|
||||
if result.status == "valid" then
|
||||
print(GREEN .. "Valid" .. RESET)
|
||||
if result.public_key then print("Public Key: " .. result.public_key) end
|
||||
if result.tier then print("Tier: " .. result.tier) end
|
||||
if result.expires_at then print("Expires: " .. result.expires_at) end
|
||||
elseif result.status == "expired" then
|
||||
print(RED .. "Expired" .. RESET)
|
||||
if result.public_key then print("Public Key: " .. result.public_key) end
|
||||
if result.tier then print("Tier: " .. result.tier) end
|
||||
if result.expired_at then print("Expired: " .. result.expired_at) end
|
||||
print(YELLOW .. "To renew: Visit https://unsandbox.com/keys/extend" .. RESET)
|
||||
else
|
||||
print(RED .. "Invalid" .. RESET)
|
||||
if result.message then print("Message: " .. result.message) end
|
||||
end
|
||||
end
|
||||
|
||||
local function cmd_service(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
|
||||
|
|
@ -440,14 +526,15 @@ local function main()
|
|||
wake = nil,
|
||||
destroy = nil,
|
||||
execute = nil,
|
||||
command = nil
|
||||
command = nil,
|
||||
extend = false
|
||||
}
|
||||
|
||||
local i = 1
|
||||
while i <= #arg do
|
||||
local a = arg[i]
|
||||
|
||||
if a == "session" or a == "service" then
|
||||
if a == "session" or a == "service" or a == "key" then
|
||||
options.command = a
|
||||
elseif a == "-e" then
|
||||
i = i + 1
|
||||
|
|
@ -525,6 +612,8 @@ local function main()
|
|||
elseif a == "--command" then
|
||||
i = i + 1
|
||||
options.command = arg[i]
|
||||
elseif a == "--extend" then
|
||||
options.extend = true
|
||||
elseif not a:match("^%-") then
|
||||
options.source_file = a
|
||||
end
|
||||
|
|
@ -536,6 +625,8 @@ local function main()
|
|||
cmd_session(options)
|
||||
elseif options.command == "service" then
|
||||
cmd_service(options)
|
||||
elseif options.command == "key" then
|
||||
cmd_key(options)
|
||||
elseif options.source_file then
|
||||
cmd_execute(options)
|
||||
else
|
||||
|
|
@ -546,6 +637,7 @@ Usage:
|
|||
]] .. arg[0] .. [[ [options] <source_file>
|
||||
]] .. arg[0] .. [[ session [options]
|
||||
]] .. arg[0] .. [[ service [options]
|
||||
]] .. arg[0] .. [[ key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -580,6 +672,9 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend/renew key
|
||||
]])
|
||||
os.exit(1)
|
||||
end
|
||||
|
|
|
|||
96
un.ml
Normal file → Executable file
96
un.ml
Normal file → Executable file
|
|
@ -62,6 +62,9 @@ let green = "\x1b[32m"
|
|||
let yellow = "\x1b[33m"
|
||||
let reset = "\x1b[0m"
|
||||
|
||||
(* Portal base URL *)
|
||||
let portal_base = "https://unsandbox.com"
|
||||
|
||||
(* Extension to language mapping *)
|
||||
let ext_to_lang ext =
|
||||
match ext with
|
||||
|
|
@ -118,6 +121,23 @@ let curl_post api_key endpoint json =
|
|||
let _ = Unix.close_process_in ic in
|
||||
output
|
||||
|
||||
let portal_curl_post api_key endpoint json =
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s"
|
||||
portal_base endpoint api_key tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
output
|
||||
|
||||
let curl_get api_key endpoint =
|
||||
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s -H 'Authorization: Bearer %s'"
|
||||
endpoint api_key in
|
||||
|
|
@ -147,6 +167,31 @@ let curl_delete api_key endpoint =
|
|||
let _ = Unix.close_process_in ic in
|
||||
output
|
||||
|
||||
(* Extract JSON value - simple regex-based parser *)
|
||||
let extract_json_value json_str key =
|
||||
let pattern = "\"" ^ key ^ "\"\\s*:\\s*\"\\([^\"]*\\)\"" in
|
||||
let regex = Str.regexp pattern in
|
||||
try
|
||||
let _ = Str.search_forward regex json_str 0 in
|
||||
Some (Str.matched_group 1 json_str)
|
||||
with Not_found -> None
|
||||
|
||||
(* Open browser *)
|
||||
let open_browser url =
|
||||
Printf.printf "%sOpening browser: %s%s\n" blue url reset;
|
||||
let _ = match Sys.os_type with
|
||||
| "Unix" | "Cygwin" ->
|
||||
(* Try xdg-open for Linux *)
|
||||
(try Sys.command (Printf.sprintf "xdg-open '%s' 2>/dev/null" url)
|
||||
with _ ->
|
||||
(* Fallback to open for macOS *)
|
||||
try Sys.command (Printf.sprintf "open '%s' 2>/dev/null" url)
|
||||
with _ -> 1)
|
||||
| "Win32" ->
|
||||
Sys.command (Printf.sprintf "start '%s'" url)
|
||||
| _ -> 1
|
||||
in ()
|
||||
|
||||
(* Parse JSON response for stdout/stderr/exit_code *)
|
||||
let extract_field field json =
|
||||
try
|
||||
|
|
@ -226,6 +271,53 @@ let execute_command file env_vars artifacts out_dir network vcpu =
|
|||
in
|
||||
exit exit_code
|
||||
|
||||
(* Display key info *)
|
||||
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
|
||||
|
||||
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 -> ());
|
||||
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;
|
||||
if extend then
|
||||
(match public_key with
|
||||
| Some pk -> open_browser (Printf.sprintf "%s/keys/extend?pk=%s" portal_base pk)
|
||||
| None -> ())
|
||||
| Some "invalid" ->
|
||||
Printf.printf "%sInvalid%s\n" red reset
|
||||
| Some s ->
|
||||
Printf.printf "%sUnknown status: %s%s\n" red s reset;
|
||||
Printf.printf "%s\n" response
|
||||
| None ->
|
||||
Printf.printf "%sError: Could not parse response%s\n" red reset;
|
||||
Printf.printf "%s\n" response
|
||||
|
||||
(* Validate key command *)
|
||||
let validate_key api_key extend =
|
||||
let json = "{}" in
|
||||
let response = portal_curl_post api_key "/keys/validate" json in
|
||||
display_key_info response extend
|
||||
|
||||
(* Key command *)
|
||||
let key_command extend =
|
||||
let api_key = get_api_key () in
|
||||
validate_key api_key extend
|
||||
|
||||
(* Session command *)
|
||||
let session_command action shell network vcpu =
|
||||
let api_key = get_api_key () in
|
||||
|
|
@ -364,7 +456,11 @@ let () =
|
|||
Printf.printf "Usage: un.ml [options] <source_file>\n";
|
||||
Printf.printf " un.ml session [options]\n";
|
||||
Printf.printf " un.ml service [options]\n";
|
||||
Printf.printf " un.ml key [--extend]\n";
|
||||
exit 1
|
||||
| "key" :: rest ->
|
||||
let extend = List.mem "--extend" rest in
|
||||
key_command extend
|
||||
| "session" :: rest ->
|
||||
let rec parse_session action shell network vcpu = function
|
||||
| [] -> session_command action shell network vcpu
|
||||
|
|
|
|||
88
un.nim
88
un.nim
|
|
@ -47,6 +47,7 @@ import os, strutils, osproc, strformat
|
|||
|
||||
const
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
BLUE = "\x1b[34m"
|
||||
RED = "\x1b[31m"
|
||||
GREEN = "\x1b[32m"
|
||||
|
|
@ -188,6 +189,81 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
|
|||
stderr.writeLine(RED & "Error: Specify --name to create a service" & RESET)
|
||||
quit(1)
|
||||
|
||||
proc cmdKey(extend: bool, apiKey: string) =
|
||||
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let response = execCurl(cmd)
|
||||
|
||||
# Parse JSON response manually (simple approach)
|
||||
if response.contains("\"status\":\"valid\""):
|
||||
echo GREEN & "Valid" & RESET
|
||||
# Extract and display key info
|
||||
if response.contains("\"public_key\":"):
|
||||
let pkStart = response.find("\"public_key\":\"") + 14
|
||||
let pkEnd = response.find("\"", pkStart)
|
||||
if pkEnd > pkStart:
|
||||
let publicKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & publicKey
|
||||
if extend:
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}"
|
||||
echo BLUE & "Opening browser to extend key..." & RESET
|
||||
discard execProcess(fmt"xdg-open '{extendUrl}'")
|
||||
|
||||
# Extract tier
|
||||
if response.contains("\"tier\":"):
|
||||
let tierStart = response.find("\"tier\":\"") + 8
|
||||
let tierEnd = response.find("\"", tierStart)
|
||||
if tierEnd > tierStart:
|
||||
echo "Tier: " & response[tierStart..<tierEnd]
|
||||
|
||||
# Extract expires_at
|
||||
if response.contains("\"expires_at\":"):
|
||||
let expiresStart = response.find("\"expires_at\":\"") + 14
|
||||
let expiresEnd = response.find("\"", expiresStart)
|
||||
if expiresEnd > expiresStart:
|
||||
echo "Expires: " & response[expiresStart..<expiresEnd]
|
||||
|
||||
elif response.contains("\"status\":\"expired\""):
|
||||
echo RED & "Expired" & RESET
|
||||
|
||||
# Extract and display key info
|
||||
var publicKey = ""
|
||||
if response.contains("\"public_key\":"):
|
||||
let pkStart = response.find("\"public_key\":\"") + 14
|
||||
let pkEnd = response.find("\"", pkStart)
|
||||
if pkEnd > pkStart:
|
||||
publicKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & publicKey
|
||||
|
||||
# Extract tier
|
||||
if response.contains("\"tier\":"):
|
||||
let tierStart = response.find("\"tier\":\"") + 8
|
||||
let tierEnd = response.find("\"", tierStart)
|
||||
if tierEnd > tierStart:
|
||||
echo "Tier: " & response[tierStart..<tierEnd]
|
||||
|
||||
# Extract expires_at
|
||||
if response.contains("\"expires_at\":"):
|
||||
let expiresStart = response.find("\"expires_at\":\"") + 14
|
||||
let expiresEnd = response.find("\"", expiresStart)
|
||||
if expiresEnd > expiresStart:
|
||||
echo "Expired: " & response[expiresStart..<expiresEnd]
|
||||
|
||||
echo ""
|
||||
echo YELLOW & "To renew: Visit " & PORTAL_BASE & "/keys/extend" & RESET
|
||||
|
||||
if extend and publicKey != "":
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}"
|
||||
echo BLUE & "Opening browser to extend key..." & RESET
|
||||
discard execProcess(fmt"xdg-open '{extendUrl}'")
|
||||
|
||||
else:
|
||||
echo RED & "Invalid" & RESET
|
||||
if response.contains("\"error\":"):
|
||||
let errStart = response.find("\"error\":\"") + 9
|
||||
let errEnd = response.find("\"", errStart)
|
||||
if errEnd > errStart:
|
||||
echo "Error: " & response[errStart..<errEnd]
|
||||
|
||||
proc main() =
|
||||
var apiKey = getEnv("UNSANDBOX_API_KEY", "")
|
||||
let args = commandLineParams()
|
||||
|
|
@ -196,8 +272,20 @@ proc main() =
|
|||
stderr.writeLine("Usage: un.nim [options] <source_file>")
|
||||
stderr.writeLine(" un.nim session [options]")
|
||||
stderr.writeLine(" un.nim service [options]")
|
||||
stderr.writeLine(" un.nim key [options]")
|
||||
quit(1)
|
||||
|
||||
if args[0] == "key":
|
||||
var extend = false
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
of "--extend": extend = true
|
||||
of "-k": apiKey = args[i+1]; inc i
|
||||
inc i
|
||||
cmdKey(extend, apiKey)
|
||||
return
|
||||
|
||||
if args[0] == "session":
|
||||
var list = false
|
||||
var kill, shell, network = ""
|
||||
|
|
|
|||
121
un.php
Normal file → Executable file
121
un.php
Normal file → Executable file
|
|
@ -53,6 +53,7 @@
|
|||
*/
|
||||
|
||||
const API_BASE = 'https://api.unsandbox.com';
|
||||
const PORTAL_BASE = 'https://unsandbox.com';
|
||||
const BLUE = "\033[34m";
|
||||
const RED = "\033[31m";
|
||||
const GREEN = "\033[32m";
|
||||
|
|
@ -263,6 +264,113 @@ function cmd_session($options) {
|
|||
echo YELLOW . "(Interactive sessions require WebSocket - use un2 for full support)" . RESET . "\n";
|
||||
}
|
||||
|
||||
function validate_key($api_key) {
|
||||
$url = PORTAL_BASE . '/keys/validate';
|
||||
$ch = curl_init($url);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $api_key,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($response === false) {
|
||||
fwrite(STDERR, RED . "Error: " . curl_error($ch) . RESET . "\n");
|
||||
curl_close($ch);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if ($http_code === 200 && isset($data['valid']) && $data['valid']) {
|
||||
echo GREEN . "Valid" . RESET . "\n\n";
|
||||
echo "Public Key: " . ($data['public_key'] ?? 'N/A') . "\n";
|
||||
echo "Tier: " . ($data['tier'] ?? 'N/A') . "\n";
|
||||
echo "Status: " . ($data['status'] ?? 'N/A') . "\n";
|
||||
echo "Expires: " . ($data['expires_at'] ?? 'N/A') . "\n";
|
||||
echo "Time Remaining: " . ($data['time_remaining'] ?? 'N/A') . "\n";
|
||||
echo "Rate Limit: " . ($data['rate_limit'] ?? 'N/A') . " req/min\n";
|
||||
echo "Burst: " . ($data['burst'] ?? 'N/A') . "\n";
|
||||
echo "Concurrency: " . ($data['concurrency'] ?? 'N/A') . "\n";
|
||||
} elseif ($http_code === 200 && isset($data['valid']) && !$data['valid'] && isset($data['status']) && $data['status'] === 'expired') {
|
||||
echo RED . "Expired" . RESET . "\n\n";
|
||||
echo "Public Key: " . ($data['public_key'] ?? 'N/A') . "\n";
|
||||
echo "Tier: " . ($data['tier'] ?? 'N/A') . "\n";
|
||||
echo "Expired: " . ($data['expires_at'] ?? 'N/A') . "\n\n";
|
||||
echo YELLOW . "To renew: Visit https://unsandbox.com/keys/extend" . RESET . "\n";
|
||||
} else {
|
||||
echo RED . "Invalid" . RESET . "\n\n";
|
||||
if (isset($data['error'])) {
|
||||
echo "Error: " . $data['error'] . "\n";
|
||||
} elseif (isset($data['reason'])) {
|
||||
echo "Reason: " . $data['reason'] . "\n";
|
||||
} else {
|
||||
echo "HTTP $http_code - $response\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cmd_key($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
|
||||
if ($options['extend']) {
|
||||
// First validate to get public_key
|
||||
$url = PORTAL_BASE . '/keys/validate';
|
||||
$ch = curl_init($url);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $api_key,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$public_key = $data['public_key'] ?? null;
|
||||
|
||||
if (!$public_key) {
|
||||
fwrite(STDERR, RED . "Error: Could not retrieve public key" . RESET . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$extend_url = PORTAL_BASE . '/keys/extend?pk=' . urlencode($public_key);
|
||||
echo "Opening browser to: $extend_url\n";
|
||||
|
||||
// Detect platform and open browser
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
exec('xdg-open ' . escapeshellarg($extend_url) . ' > /dev/null 2>&1 &');
|
||||
} elseif (PHP_OS_FAMILY === 'Darwin') {
|
||||
exec('open ' . escapeshellarg($extend_url) . ' > /dev/null 2>&1 &');
|
||||
} elseif (PHP_OS_FAMILY === 'Windows') {
|
||||
exec('start ' . escapeshellarg($extend_url) . ' > NUL 2>&1');
|
||||
} else {
|
||||
echo YELLOW . "Cannot auto-open browser on this platform. Please visit:" . RESET . "\n";
|
||||
echo "$extend_url\n";
|
||||
}
|
||||
} else {
|
||||
validate_key($api_key);
|
||||
}
|
||||
}
|
||||
|
||||
function cmd_service($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
|
||||
|
|
@ -392,7 +500,7 @@ function main() {
|
|||
'wake' => null,
|
||||
'destroy' => null,
|
||||
'execute' => null,
|
||||
'command' => null
|
||||
'extend' => false
|
||||
];
|
||||
|
||||
for ($i = 1; $i < count($argv); $i++) {
|
||||
|
|
@ -401,6 +509,7 @@ function main() {
|
|||
switch ($arg) {
|
||||
case 'session':
|
||||
case 'service':
|
||||
case 'key':
|
||||
$options['command'] = $arg;
|
||||
break;
|
||||
case '-e':
|
||||
|
|
@ -486,6 +595,9 @@ function main() {
|
|||
case '--command':
|
||||
$options['command'] = $argv[++$i];
|
||||
break;
|
||||
case '--extend':
|
||||
$options['extend'] = true;
|
||||
break;
|
||||
default:
|
||||
if (!str_starts_with($arg, '-')) {
|
||||
$options['source_file'] = $arg;
|
||||
|
|
@ -498,6 +610,8 @@ function main() {
|
|||
cmd_session($options);
|
||||
} elseif ($options['command'] === 'service') {
|
||||
cmd_service($options);
|
||||
} elseif ($options['command'] === 'key') {
|
||||
cmd_key($options);
|
||||
} elseif ($options['source_file']) {
|
||||
cmd_execute($options);
|
||||
} else {
|
||||
|
|
@ -507,6 +621,7 @@ Usage:
|
|||
{$argv[0]} [options] <source_file>
|
||||
{$argv[0]} session [options]
|
||||
{$argv[0]} service [options]
|
||||
{$argv[0]} key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -541,6 +656,10 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
-k KEY API key (or use UNSANDBOX_API_KEY env var)
|
||||
--extend Open browser to extend/renew key
|
||||
";
|
||||
exit(1);
|
||||
}
|
||||
|
|
|
|||
69
un.pl
69
un.pl
|
|
@ -59,6 +59,7 @@ use MIME::Base64;
|
|||
use File::Path qw(make_path);
|
||||
|
||||
my $API_BASE = 'https://api.unsandbox.com';
|
||||
my $PORTAL_BASE = 'https://unsandbox.com';
|
||||
my $BLUE = "\033[34m";
|
||||
my $RED = "\033[31m";
|
||||
my $GREEN = "\033[32m";
|
||||
|
|
@ -361,6 +362,61 @@ sub cmd_service {
|
|||
exit 1;
|
||||
}
|
||||
|
||||
sub cmd_key {
|
||||
my ($options) = @_;
|
||||
my $api_key = get_api_key($options->{api_key});
|
||||
|
||||
# Call /keys/validate endpoint
|
||||
my $url = "$PORTAL_BASE/keys/validate";
|
||||
my $ua = LWP::UserAgent->new(timeout => 30);
|
||||
my $request = HTTP::Request->new('POST' => $url);
|
||||
$request->header('Authorization' => "Bearer $api_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';
|
||||
|
||||
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') {
|
||||
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 "${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";
|
||||
}
|
||||
|
||||
# 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";
|
||||
}
|
||||
}
|
||||
|
||||
sub main {
|
||||
my %options = (
|
||||
command => undef,
|
||||
|
|
@ -391,13 +447,14 @@ sub main {
|
|||
wake => undef,
|
||||
destroy => undef,
|
||||
execute => undef,
|
||||
command => undef
|
||||
command => undef,
|
||||
extend => 0
|
||||
);
|
||||
|
||||
for (my $i = 0; $i < @ARGV; $i++) {
|
||||
my $arg = $ARGV[$i];
|
||||
|
||||
if ($arg eq 'session' || $arg eq 'service') {
|
||||
if ($arg eq 'session' || $arg eq 'service' || $arg eq 'key') {
|
||||
$options{command} = $arg;
|
||||
} elsif ($arg eq '-e') {
|
||||
push @{$options{env}}, $ARGV[++$i];
|
||||
|
|
@ -453,6 +510,8 @@ sub main {
|
|||
$options{execute} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--command') {
|
||||
$options{command} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--extend') {
|
||||
$options{extend} = 1;
|
||||
} elsif ($arg !~ /^-/) {
|
||||
$options{source_file} = $arg;
|
||||
}
|
||||
|
|
@ -462,6 +521,8 @@ sub main {
|
|||
cmd_session(\%options);
|
||||
} elsif ($options{command} && $options{command} eq 'service') {
|
||||
cmd_service(\%options);
|
||||
} elsif ($options{command} && $options{command} eq 'key') {
|
||||
cmd_key(\%options);
|
||||
} elsif ($options{source_file}) {
|
||||
cmd_execute(\%options);
|
||||
} else {
|
||||
|
|
@ -472,6 +533,7 @@ Usage:
|
|||
$0 [options] <source_file>
|
||||
$0 session [options]
|
||||
$0 service [options]
|
||||
$0 key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -506,6 +568,9 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend/renew key
|
||||
HELP
|
||||
exit 1;
|
||||
}
|
||||
|
|
|
|||
78
un.py
78
un.py
|
|
@ -58,8 +58,10 @@ import base64
|
|||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import webbrowser
|
||||
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
BLUE = "\033[34m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
|
|
@ -267,6 +269,73 @@ def cmd_session(args):
|
|||
print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}")
|
||||
|
||||
|
||||
def validate_key(api_key, extend=False):
|
||||
"""Validate API key and display information"""
|
||||
url = f"{PORTAL_BASE}/keys/validate"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
req = urllib.request.Request(url, method="POST", headers=headers)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
result = json.loads(resp.read().decode('utf-8'))
|
||||
|
||||
# Handle --extend flag
|
||||
if extend:
|
||||
public_key = result.get("public_key")
|
||||
if public_key:
|
||||
extend_url = f"{PORTAL_BASE}/keys/extend?pk={public_key}"
|
||||
print(f"{BLUE}Opening browser to extend key...{RESET}")
|
||||
webbrowser.open(extend_url)
|
||||
return
|
||||
else:
|
||||
print(f"{RED}Error: Could not retrieve public key{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check if key is expired
|
||||
if result.get("expired", False):
|
||||
print(f"{RED}Expired{RESET}")
|
||||
print(f"Public Key: {result.get('public_key', 'N/A')}")
|
||||
print(f"Tier: {result.get('tier', 'N/A')}")
|
||||
print(f"Expired: {result.get('expires_at', 'N/A')}")
|
||||
print(f"{YELLOW}To renew: Visit https://unsandbox.com/keys/extend{RESET}")
|
||||
sys.exit(1)
|
||||
|
||||
# Valid key
|
||||
print(f"{GREEN}Valid{RESET}")
|
||||
print(f"Public Key: {result.get('public_key', 'N/A')}")
|
||||
print(f"Tier: {result.get('tier', 'N/A')}")
|
||||
print(f"Status: {result.get('status', 'N/A')}")
|
||||
print(f"Expires: {result.get('expires_at', 'N/A')}")
|
||||
print(f"Time Remaining: {result.get('time_remaining', 'N/A')}")
|
||||
print(f"Rate Limit: {result.get('rate_limit', 'N/A')}")
|
||||
print(f"Burst: {result.get('burst', 'N/A')}")
|
||||
print(f"Concurrency: {result.get('concurrency', 'N/A')}")
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode('utf-8') if e.fp else str(e)
|
||||
try:
|
||||
error_json = json.loads(error_body)
|
||||
reason = error_json.get("error", error_body)
|
||||
except:
|
||||
reason = error_body
|
||||
print(f"{RED}Invalid{RESET}")
|
||||
print(f"Reason: {reason}")
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"{RED}Error: {e.reason}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_key(args):
|
||||
"""Validate API key"""
|
||||
api_key = get_api_key(args.key)
|
||||
validate_key(api_key, extend=args.extend)
|
||||
|
||||
|
||||
def cmd_service(args):
|
||||
"""Manage persistent services"""
|
||||
api_key = get_api_key(args.api_key)
|
||||
|
|
@ -380,6 +449,11 @@ Examples:
|
|||
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
# Key subcommand
|
||||
key_parser = subparsers.add_parser("key", help="Validate API key")
|
||||
key_parser.add_argument("-k", "--key", help="API key to validate (or set UNSANDBOX_API_KEY)")
|
||||
key_parser.add_argument("--extend", action="store_true", help="Open browser to extend key")
|
||||
|
||||
# Session subcommand
|
||||
session_parser = subparsers.add_parser("session", help="Interactive shell/REPL sessions")
|
||||
session_parser.add_argument("-s", "--shell", help="Shell/REPL to use (default: bash)")
|
||||
|
|
@ -423,7 +497,9 @@ Examples:
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "session":
|
||||
if args.command == "key":
|
||||
cmd_key(args)
|
||||
elif args.command == "session":
|
||||
cmd_session(args)
|
||||
elif args.command == "service":
|
||||
cmd_service(args)
|
||||
|
|
|
|||
80
un.r
80
un.r
|
|
@ -64,6 +64,7 @@ YELLOW <- "\033[33m"
|
|||
RESET <- "\033[0m"
|
||||
|
||||
API_BASE <- "https://api.unsandbox.com"
|
||||
PORTAL_BASE <- "https://unsandbox.com"
|
||||
|
||||
detect_language <- function(filename) {
|
||||
ext <- tolower(sub(".*(\\..*)$", "\\1", filename))
|
||||
|
|
@ -233,6 +234,73 @@ cmd_session <- function(args) {
|
|||
quit(status = 1)
|
||||
}
|
||||
|
||||
cmd_key <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
|
||||
if (!is.null(args$extend) && args$extend) {
|
||||
# First validate to get public_key
|
||||
url <- paste0(PORTAL_BASE, "/keys/validate")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", api_key)
|
||||
)
|
||||
|
||||
tryCatch({
|
||||
response <- POST(url, headers, encode = "json", timeout(10))
|
||||
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
|
||||
|
||||
if (!is.null(result$public_key)) {
|
||||
extend_url <- paste0(PORTAL_BASE, "/keys/extend?pk=", result$public_key)
|
||||
cat(sprintf("Opening: %s\n", extend_url))
|
||||
system(sprintf("xdg-open '%s' 2>/dev/null || open '%s' 2>/dev/null || start '%s'", extend_url, extend_url, extend_url))
|
||||
} else {
|
||||
cat(sprintf("%sError: Could not retrieve public key%s\n", RED, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
}, error = function(e) {
|
||||
cat(sprintf("%sError: Request failed: %s%s\n", RED, e$message, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
})
|
||||
return()
|
||||
}
|
||||
|
||||
# Validate key
|
||||
url <- paste0(PORTAL_BASE, "/keys/validate")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", api_key)
|
||||
)
|
||||
|
||||
tryCatch({
|
||||
response <- POST(url, headers, encode = "json", timeout(10))
|
||||
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
|
||||
|
||||
status <- if (!is.null(result$status)) result$status else "Unknown"
|
||||
|
||||
if (status == "valid") {
|
||||
cat(sprintf("%sValid%s\n", GREEN, RESET))
|
||||
cat(sprintf("Public Key: %s\n", if (!is.null(result$public_key)) result$public_key else "N/A"))
|
||||
cat(sprintf("Tier: %s\n", if (!is.null(result$tier)) result$tier else "N/A"))
|
||||
if (!is.null(result$expires_at)) {
|
||||
cat(sprintf("Expires: %s\n", result$expires_at))
|
||||
}
|
||||
} else if (status == "expired") {
|
||||
cat(sprintf("%sExpired%s\n", RED, RESET))
|
||||
cat(sprintf("Public Key: %s\n", if (!is.null(result$public_key)) result$public_key else "N/A"))
|
||||
cat(sprintf("Tier: %s\n", if (!is.null(result$tier)) result$tier else "N/A"))
|
||||
if (!is.null(result$expires_at)) {
|
||||
cat(sprintf("Expired: %s\n", result$expires_at))
|
||||
}
|
||||
cat(sprintf("%sTo renew: Visit https://unsandbox.com/keys/extend%s\n", YELLOW, RESET))
|
||||
} else {
|
||||
cat(sprintf("%sInvalid%s\n", RED, RESET))
|
||||
}
|
||||
}, error = function(e) {
|
||||
cat(sprintf("%sError: Request failed: %s%s\n", RED, e$message, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
})
|
||||
}
|
||||
|
||||
cmd_service <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
|
||||
|
|
@ -356,7 +424,8 @@ parse_args <- function() {
|
|||
domains = NULL,
|
||||
type = NULL,
|
||||
bootstrap = NULL,
|
||||
vcpu = NULL
|
||||
vcpu = NULL,
|
||||
extend = FALSE
|
||||
)
|
||||
|
||||
i <- 1
|
||||
|
|
@ -369,6 +438,9 @@ parse_args <- function() {
|
|||
} else if (arg == "service") {
|
||||
result$command <- "service"
|
||||
i <- i + 1
|
||||
} else if (arg == "key") {
|
||||
result$command <- "key"
|
||||
i <- i + 1
|
||||
} else if (arg %in% c("-k", "--api-key")) {
|
||||
i <- i + 1
|
||||
result$api_key <- args[i]
|
||||
|
|
@ -443,6 +515,9 @@ parse_args <- function() {
|
|||
i <- i + 1
|
||||
result$vcpu <- as.integer(args[i])
|
||||
i <- i + 1
|
||||
} else if (arg == "--extend") {
|
||||
result$extend <- TRUE
|
||||
i <- i + 1
|
||||
} else if (!startsWith(arg, "-")) {
|
||||
result$source_file <- arg
|
||||
i <- i + 1
|
||||
|
|
@ -462,12 +537,15 @@ main <- function() {
|
|||
cmd_session(args)
|
||||
} else if (!is.null(args$command) && args$command == "service") {
|
||||
cmd_service(args)
|
||||
} else if (!is.null(args$command) && args$command == "key") {
|
||||
cmd_key(args)
|
||||
} else if (!is.null(args$source_file)) {
|
||||
cmd_execute(args)
|
||||
} else {
|
||||
cat("Usage: un.r [options] <source_file>\n", file = stderr())
|
||||
cat(" un.r session [options]\n", file = stderr())
|
||||
cat(" un.r service [options]\n", file = stderr())
|
||||
cat(" un.r key [options]\n", file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
94
un.rb
94
un.rb
|
|
@ -57,6 +57,7 @@ require 'fileutils'
|
|||
require 'optparse'
|
||||
|
||||
API_BASE = 'https://api.unsandbox.com'
|
||||
PORTAL_BASE = 'https://unsandbox.com'
|
||||
BLUE = "\e[34m"
|
||||
RED = "\e[31m"
|
||||
GREEN = "\e[32m"
|
||||
|
|
@ -245,6 +246,86 @@ def cmd_session(options)
|
|||
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
||||
end
|
||||
|
||||
def validate_key(api_key)
|
||||
uri = URI("#{PORTAL_BASE}/keys/validate")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
http.read_timeout = 30
|
||||
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request['Authorization'] = "Bearer #{api_key}"
|
||||
request['Content-Type'] = 'application/json'
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
begin
|
||||
result = JSON.parse(response.body)
|
||||
rescue JSON::ParserError => e
|
||||
warn "#{RED}Error: Failed to parse response: #{e.message}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
if response.is_a?(Net::HTTPSuccess) && result['valid']
|
||||
puts "#{GREEN}Valid#{RESET}"
|
||||
puts "Public Key: #{result['public_key']}"
|
||||
puts "Tier: #{result['tier']}"
|
||||
puts "Status: #{result['status']}"
|
||||
puts "Expires: #{result['expires_at']}"
|
||||
puts "Time Remaining: #{result['time_remaining']}"
|
||||
puts "Rate Limit: #{result['rate_limit']}"
|
||||
puts "Burst: #{result['burst']}"
|
||||
puts "Concurrency: #{result['concurrency']}"
|
||||
result
|
||||
elsif result['expired']
|
||||
puts "#{RED}Expired#{RESET}"
|
||||
puts "Public Key: #{result['public_key']}"
|
||||
puts "Tier: #{result['tier']}"
|
||||
puts "Expired: #{result['expires_at']}"
|
||||
puts "#{YELLOW}To renew: Visit https://unsandbox.com/keys/extend#{RESET}"
|
||||
result
|
||||
else
|
||||
puts "#{RED}Invalid#{RESET}"
|
||||
puts "Error: #{result['error'] || result['reason'] || 'Unknown error'}"
|
||||
exit 1
|
||||
end
|
||||
rescue => e
|
||||
warn "#{RED}Error: #{e.message}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
def open_browser(url)
|
||||
case RbConfig::CONFIG['host_os']
|
||||
when /mswin|mingw|cygwin/
|
||||
system("start #{url}")
|
||||
when /darwin/
|
||||
system("open #{url}")
|
||||
when /linux|bsd/
|
||||
system("xdg-open #{url}")
|
||||
else
|
||||
puts "#{YELLOW}Please open this URL in your browser:#{RESET}"
|
||||
puts url
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_key(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
|
||||
if options[:extend]
|
||||
result = validate_key(api_key)
|
||||
public_key = result['public_key']
|
||||
if public_key
|
||||
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
|
||||
puts "#{GREEN}Opening browser to extend key...#{RESET}"
|
||||
open_browser(url)
|
||||
else
|
||||
warn "#{RED}Error: Could not retrieve public key#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
else
|
||||
validate_key(api_key)
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_service(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
|
||||
|
|
@ -366,7 +447,7 @@ def main
|
|||
wake: nil,
|
||||
destroy: nil,
|
||||
execute: nil,
|
||||
command: nil
|
||||
extend: false
|
||||
}
|
||||
|
||||
# Manual argument parsing
|
||||
|
|
@ -375,7 +456,7 @@ def main
|
|||
arg = ARGV[i]
|
||||
|
||||
case arg
|
||||
when 'session', 'service'
|
||||
when 'session', 'service', 'key'
|
||||
options[:command] = arg
|
||||
when '-e'
|
||||
i += 1
|
||||
|
|
@ -453,6 +534,8 @@ def main
|
|||
when '--command'
|
||||
i += 1
|
||||
options[:command] = ARGV[i]
|
||||
when '--extend'
|
||||
options[:extend] = true
|
||||
else
|
||||
options[:source_file] = arg unless arg.start_with?('-')
|
||||
end
|
||||
|
|
@ -465,6 +548,8 @@ def main
|
|||
cmd_session(options)
|
||||
when 'service'
|
||||
cmd_service(options)
|
||||
when 'key'
|
||||
cmd_key(options)
|
||||
else
|
||||
if options[:source_file]
|
||||
cmd_execute(options)
|
||||
|
|
@ -476,6 +561,7 @@ def main
|
|||
#{$PROGRAM_NAME} [options] <source_file>
|
||||
#{$PROGRAM_NAME} session [options]
|
||||
#{$PROGRAM_NAME} service [options]
|
||||
#{$PROGRAM_NAME} key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -510,6 +596,10 @@ def main
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
-k KEY API key (or use UNSANDBOX_API_KEY env var)
|
||||
--extend Validate key and open browser to extend
|
||||
HELP
|
||||
exit 1
|
||||
end
|
||||
|
|
|
|||
59
un.rs
59
un.rs
|
|
@ -51,6 +51,7 @@ use std::process::{self, Command};
|
|||
use std::collections::HashMap;
|
||||
|
||||
const API_BASE: &str = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE: &str = "https://unsandbox.com";
|
||||
const BLUE: &str = "\x1b[34m";
|
||||
const RED: &str = "\x1b[31m";
|
||||
const GREEN: &str = "\x1b[32m";
|
||||
|
|
@ -453,6 +454,55 @@ fn cmd_service(
|
|||
process::exit(1);
|
||||
}
|
||||
|
||||
fn cmd_key(extend: bool, api_key: &str) {
|
||||
let result = api_request("/keys/validate", "POST", Some("{}"), api_key);
|
||||
|
||||
let status = extract_json_string(&result, "status");
|
||||
let public_key = extract_json_string(&result, "public_key");
|
||||
let tier = extract_json_string(&result, "tier");
|
||||
let expired_at = extract_json_string(&result, "expired_at");
|
||||
|
||||
if extend && !public_key.is_empty() {
|
||||
let url = format!("{}/keys/extend?pk={}", PORTAL_BASE, public_key);
|
||||
println!("{}Opening browser: {}{}", YELLOW, url, RESET);
|
||||
|
||||
// Try xdg-open (Linux), open (macOS), or start (Windows)
|
||||
let _ = Command::new("xdg-open")
|
||||
.arg(&url)
|
||||
.spawn()
|
||||
.or_else(|_| Command::new("open").arg(&url).spawn())
|
||||
.or_else(|_| Command::new("cmd").args(&["/c", "start", &url]).spawn());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
match status.as_str() {
|
||||
"valid" => {
|
||||
println!("{}Valid{}", GREEN, RESET);
|
||||
println!("Public Key: {}", public_key);
|
||||
println!("Tier: {}", tier);
|
||||
if !expired_at.is_empty() {
|
||||
println!("Expires: {}", expired_at);
|
||||
}
|
||||
}
|
||||
"expired" => {
|
||||
println!("{}Expired{}", RED, RESET);
|
||||
println!("Public Key: {}", public_key);
|
||||
println!("Tier: {}", tier);
|
||||
if !expired_at.is_empty() {
|
||||
println!("Expired: {}", expired_at);
|
||||
}
|
||||
println!("{}To renew: Visit {}/keys/extend{}", YELLOW, PORTAL_BASE, RESET);
|
||||
}
|
||||
"invalid" => {
|
||||
println!("{}Invalid{}", RED, RESET);
|
||||
}
|
||||
_ => {
|
||||
println!("{}Unknown status: {}{}", YELLOW, status, RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal base64 encoding
|
||||
mod base64 {
|
||||
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
|
@ -491,6 +541,7 @@ fn main() {
|
|||
eprintln!("Usage: {} [options] <source_file>", args[0]);
|
||||
eprintln!(" {} session [options]", args[0]);
|
||||
eprintln!(" {} service [options]", args[0]);
|
||||
eprintln!(" {} key [--extend]", args[0]);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -579,6 +630,14 @@ fn main() {
|
|||
);
|
||||
return;
|
||||
}
|
||||
"key" => {
|
||||
let key = get_api_key(api_key.as_deref());
|
||||
cmd_key(
|
||||
args.contains(&"--extend".to_string()),
|
||||
&key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
if !args[i].starts_with('-') {
|
||||
source_file = Some(args[i].clone());
|
||||
|
|
|
|||
80
un.scm
80
un.scm
|
|
@ -53,6 +53,8 @@
|
|||
(define yellow "\x1b[33m")
|
||||
(define reset "\x1b[0m")
|
||||
|
||||
(define portal-base "https://unsandbox.com")
|
||||
|
||||
(define ext-map
|
||||
'((".hs" . "haskell") (".ml" . "ocaml") (".clj" . "clojure")
|
||||
(".scm" . "scheme") (".lisp" . "commonlisp") (".erl" . "erlang")
|
||||
|
|
@ -135,12 +137,86 @@
|
|||
(close-pipe port)
|
||||
output))
|
||||
|
||||
(define (curl-post-portal api-key endpoint json-data)
|
||||
(let* ((tmp-file (write-temp-file json-data))
|
||||
(cmd (format #f "curl -s -X POST ~a~a -H 'Content-Type: application/json' -H 'Authorization: Bearer ~a' -d @~a"
|
||||
portal-base endpoint api-key tmp-file))
|
||||
(port (open-input-pipe cmd))
|
||||
(output (let loop ((lines '()))
|
||||
(let ((line (read-line port)))
|
||||
(if (eof-object? line)
|
||||
(string-join (reverse lines) "\n")
|
||||
(loop (cons line lines)))))))
|
||||
(close-pipe port)
|
||||
(delete-file tmp-file)
|
||||
output))
|
||||
|
||||
(define (get-api-key)
|
||||
(or (getenv "UNSANDBOX_API_KEY")
|
||||
(begin
|
||||
(display "Error: UNSANDBOX_API_KEY not set\n" (current-error-port))
|
||||
(exit 1))))
|
||||
|
||||
(define (json-extract-string json key)
|
||||
"Extract string value for key from JSON (simple parser)"
|
||||
(let* ((pattern (format #f "\"~a\":\\s*\"([^\"]*)" key))
|
||||
(cmd (format #f "echo '~a' | grep -oP '~a' | sed 's/\"~a\":\\s*\"//'" json pattern key))
|
||||
(port (open-input-pipe cmd))
|
||||
(result (read-line port)))
|
||||
(close-pipe port)
|
||||
(if (eof-object? result) #f result)))
|
||||
|
||||
(define (json-has-field json field)
|
||||
"Check if JSON contains a field"
|
||||
(string-contains json (format #f "\"~a\"" field)))
|
||||
|
||||
(define (open-browser url)
|
||||
"Open URL in browser using xdg-open"
|
||||
(let ((cmd (format #f "xdg-open '~a' 2>/dev/null &" url)))
|
||||
(system cmd)))
|
||||
|
||||
(define (validate-key-cmd extend)
|
||||
(let* ((api-key (get-api-key))
|
||||
(response (curl-post-portal api-key "/keys/validate" "{}"))
|
||||
(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")))
|
||||
|
||||
(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))
|
||||
(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)
|
||||
(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)
|
||||
(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)
|
||||
(open-browser url))
|
||||
(format #t "~aError: No public_key in response~a\n" red reset))))
|
||||
|
||||
;; Invalid or error
|
||||
(else
|
||||
(format #t "~aInvalid~a\n" red reset)
|
||||
(display response)
|
||||
(newline)))))
|
||||
|
||||
(define (execute-cmd file)
|
||||
(let* ((api-key (get-api-key))
|
||||
(ext (get-extension file))
|
||||
|
|
@ -212,8 +288,12 @@
|
|||
(display "Usage: un.scm [options] <source_file>\n")
|
||||
(display " un.scm session [options]\n")
|
||||
(display " un.scm service [options]\n")
|
||||
(display " un.scm key [--extend]\n")
|
||||
(exit 1))
|
||||
(cond
|
||||
((equal? (car args) "key")
|
||||
(let ((extend (and (> (length args) 1) (equal? (cadr args) "--extend"))))
|
||||
(validate-key-cmd extend)))
|
||||
((equal? (car args) "session")
|
||||
(if (and (> (length args) 1) (equal? (cadr args) "--list"))
|
||||
(session-cmd "list" #f #f)
|
||||
|
|
|
|||
152
un.sh
152
un.sh
|
|
@ -52,6 +52,7 @@ set -euo pipefail
|
|||
# Requires: UNSANDBOX_API_KEY environment variable, jq, curl
|
||||
|
||||
API_BASE="https://api.unsandbox.com"
|
||||
PORTAL_BASE="https://unsandbox.com"
|
||||
BLUE="\033[34m"
|
||||
RED="\033[31m"
|
||||
GREEN="\033[32m"
|
||||
|
|
@ -591,6 +592,149 @@ cmd_service() {
|
|||
exit 1
|
||||
}
|
||||
|
||||
validate_key() {
|
||||
local api_key="$1"
|
||||
local extend_mode="$2"
|
||||
|
||||
if [[ -z "$api_key" ]]; then
|
||||
echo -e "${RED}Error: API key not provided. Use -k flag or set UNSANDBOX_API_KEY${RESET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Call portal validation endpoint
|
||||
local response
|
||||
local http_code
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "${PORTAL_BASE}/keys/validate" \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" 2>&1)
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
local body=$(echo "$response" | head -n-1)
|
||||
|
||||
if [[ "$http_code" -eq 200 ]]; then
|
||||
# Valid key - parse response
|
||||
if command -v jq &> /dev/null; then
|
||||
# Use jq for parsing
|
||||
local valid=$(echo "$body" | jq -r '.valid // false')
|
||||
local public_key=$(echo "$body" | jq -r '.public_key // "N/A"')
|
||||
local tier=$(echo "$body" | jq -r '.tier // "N/A"')
|
||||
local expires_at=$(echo "$body" | jq -r '.expires_at // "N/A"')
|
||||
local expired=$(echo "$body" | jq -r '.expired // false')
|
||||
|
||||
if [[ "$expired" == "true" ]]; then
|
||||
echo -e "${RED}Expired${RESET}"
|
||||
echo "Public Key: $public_key"
|
||||
echo "Tier: $tier"
|
||||
echo "Expired: $expires_at"
|
||||
echo -e "${YELLOW}To renew: Visit ${PORTAL_BASE}/keys/extend${RESET}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}Valid${RESET}"
|
||||
echo "Public Key: $public_key"
|
||||
echo "Tier: $tier"
|
||||
echo "Expires: $expires_at"
|
||||
|
||||
# If extend mode, open browser
|
||||
if [[ "$extend_mode" == "true" ]]; then
|
||||
local extend_url="${PORTAL_BASE}/keys/extend?pk=${public_key}"
|
||||
echo -e "\n${BLUE}Opening browser to extend key...${RESET}"
|
||||
|
||||
# Detect platform and open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "$extend_url" &> /dev/null
|
||||
elif command -v open &> /dev/null; then
|
||||
open "$extend_url" &> /dev/null
|
||||
elif command -v start &> /dev/null; then
|
||||
start "$extend_url" &> /dev/null
|
||||
else
|
||||
echo -e "${YELLOW}Cannot detect browser opener. Visit: $extend_url${RESET}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback: use grep/sed for parsing (no jq available)
|
||||
local valid=$(echo "$body" | grep -o '"valid"[[:space:]]*:[[:space:]]*[^,}]*' | sed 's/.*:[[:space:]]*//' | tr -d ' "')
|
||||
local public_key=$(echo "$body" | grep -o '"public_key"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"//' | tr -d '"')
|
||||
local tier=$(echo "$body" | grep -o '"tier"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"//' | tr -d '"')
|
||||
local expires_at=$(echo "$body" | grep -o '"expires_at"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"//' | tr -d '"')
|
||||
local expired=$(echo "$body" | grep -o '"expired"[[:space:]]*:[[:space:]]*[^,}]*' | sed 's/.*:[[:space:]]*//' | tr -d ' "')
|
||||
|
||||
[[ -z "$public_key" ]] && public_key="N/A"
|
||||
[[ -z "$tier" ]] && tier="N/A"
|
||||
[[ -z "$expires_at" ]] && expires_at="N/A"
|
||||
|
||||
if [[ "$expired" == "true" ]]; then
|
||||
echo -e "${RED}Expired${RESET}"
|
||||
echo "Public Key: $public_key"
|
||||
echo "Tier: $tier"
|
||||
echo "Expired: $expires_at"
|
||||
echo -e "${YELLOW}To renew: Visit ${PORTAL_BASE}/keys/extend${RESET}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}Valid${RESET}"
|
||||
echo "Public Key: $public_key"
|
||||
echo "Tier: $tier"
|
||||
echo "Expires: $expires_at"
|
||||
|
||||
# If extend mode, open browser
|
||||
if [[ "$extend_mode" == "true" ]]; then
|
||||
local extend_url="${PORTAL_BASE}/keys/extend?pk=${public_key}"
|
||||
echo -e "\n${BLUE}Opening browser to extend key...${RESET}"
|
||||
|
||||
# Detect platform and open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "$extend_url" &> /dev/null
|
||||
elif command -v open &> /dev/null; then
|
||||
open "$extend_url" &> /dev/null
|
||||
elif command -v start &> /dev/null; then
|
||||
start "$extend_url" &> /dev/null
|
||||
else
|
||||
echo -e "${YELLOW}Cannot detect browser opener. Visit: $extend_url${RESET}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Invalid key or error
|
||||
if command -v jq &> /dev/null; then
|
||||
local error=$(echo "$body" | jq -r '.error // "Unknown error"')
|
||||
echo -e "${RED}Invalid${RESET}"
|
||||
echo "Error: $error"
|
||||
else
|
||||
# Fallback
|
||||
local error=$(echo "$body" | grep -o '"error"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"//' | tr -d '"')
|
||||
[[ -z "$error" ]] && error="Unknown error (HTTP $http_code)"
|
||||
echo -e "${RED}Invalid${RESET}"
|
||||
echo "Error: $error"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_key() {
|
||||
local api_key="${UNSANDBOX_API_KEY}"
|
||||
local extend=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-k)
|
||||
api_key="$2"
|
||||
shift 2
|
||||
;;
|
||||
--extend)
|
||||
extend=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
validate_key "$api_key" "$extend"
|
||||
}
|
||||
|
||||
# Main
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
|
|
@ -600,6 +744,7 @@ Usage:
|
|||
$0 [options] <source_file>
|
||||
$0 session [options]
|
||||
$0 service [options]
|
||||
$0 key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -634,6 +779,10 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
-k KEY API key to validate
|
||||
--extend Validate and open browser to extend key
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -650,6 +799,9 @@ if [[ "$1" == "session" ]]; then
|
|||
elif [[ "$1" == "service" ]]; then
|
||||
shift
|
||||
cmd_service "$@"
|
||||
elif [[ "$1" == "key" ]]; then
|
||||
shift
|
||||
cmd_key "$@"
|
||||
else
|
||||
cmd_execute "$@"
|
||||
fi
|
||||
|
|
|
|||
89
un.tcl
89
un.tcl
|
|
@ -48,6 +48,7 @@ package require base64
|
|||
::http::register https 443 ::tls::socket
|
||||
|
||||
set API_BASE "https://api.unsandbox.com"
|
||||
set PORTAL_BASE "https://unsandbox.com"
|
||||
set BLUE "\033\[34m"
|
||||
set RED "\033\[31m"
|
||||
set GREEN "\033\[32m"
|
||||
|
|
@ -351,6 +352,91 @@ proc cmd_session {args} {
|
|||
puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}"
|
||||
}
|
||||
|
||||
proc cmd_key {args} {
|
||||
set api_key [get_api_key]
|
||||
set extend_mode 0
|
||||
|
||||
# Parse arguments
|
||||
for {set i 0} {$i < [llength $args]} {incr i} {
|
||||
set arg [lindex $args $i]
|
||||
switch -exact -- $arg {
|
||||
--extend {
|
||||
set extend_mode 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# POST to /keys/validate with Bearer auth
|
||||
set url "${::PORTAL_BASE}/keys/validate"
|
||||
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"]
|
||||
|
||||
set token [::http::geturl $url -method POST -headers $headers -timeout 30000]
|
||||
set status [::http::status $token]
|
||||
set ncode [::http::ncode $token]
|
||||
set body [::http::data $token]
|
||||
::http::cleanup $token
|
||||
|
||||
if {$status ne "ok"} {
|
||||
puts stderr "${::RED}Error: Failed to connect to validation endpoint${::RESET}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if {$ncode == 401 || $ncode == 403} {
|
||||
puts "${::RED}Invalid${::RESET}"
|
||||
puts "Status: Invalid API key"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if {$ncode != 200} {
|
||||
puts stderr "${::RED}Error: HTTP $ncode${::RESET}"
|
||||
puts stderr $body
|
||||
exit 1
|
||||
}
|
||||
|
||||
set result [::json::json2dict $body]
|
||||
set key_status [dict get $result status]
|
||||
set public_key [dict get $result public_key]
|
||||
set tier [dict get $result tier]
|
||||
|
||||
if {$key_status eq "valid"} {
|
||||
puts "${::GREEN}Valid${::RESET}"
|
||||
puts "Public Key: $public_key"
|
||||
puts "Tier: $tier"
|
||||
|
||||
if {[dict exists $result expires_at]} {
|
||||
set expires_at [dict get $result expires_at]
|
||||
puts "Expires: $expires_at"
|
||||
}
|
||||
|
||||
if {$extend_mode} {
|
||||
set extend_url "${::PORTAL_BASE}/keys/extend?pk=${public_key}"
|
||||
puts "${::YELLOW}Opening browser to extend key...${::RESET}"
|
||||
exec xdg-open $extend_url &
|
||||
}
|
||||
} elseif {$key_status eq "expired"} {
|
||||
puts "${::RED}Expired${::RESET}"
|
||||
puts "Public Key: $public_key"
|
||||
puts "Tier: $tier"
|
||||
|
||||
if {[dict exists $result expired_at]} {
|
||||
set expired_at [dict get $result expired_at]
|
||||
puts "Expired: $expired_at"
|
||||
}
|
||||
|
||||
puts "${::YELLOW}To renew: Visit ${::PORTAL_BASE}/keys/extend${::RESET}"
|
||||
|
||||
if {$extend_mode} {
|
||||
set extend_url "${::PORTAL_BASE}/keys/extend?pk=${public_key}"
|
||||
puts "${::YELLOW}Opening browser to extend key...${::RESET}"
|
||||
exec xdg-open $extend_url &
|
||||
}
|
||||
} else {
|
||||
puts "${::RED}Invalid${::RESET}"
|
||||
puts "Status: Unknown key status"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
proc cmd_service {args} {
|
||||
set api_key [get_api_key]
|
||||
set list_mode 0
|
||||
|
|
@ -525,6 +611,7 @@ proc main {argv} {
|
|||
puts stderr "Usage: un.tcl \[options\] <source_file>"
|
||||
puts stderr " un.tcl session \[options\]"
|
||||
puts stderr " un.tcl service \[options\]"
|
||||
puts stderr " un.tcl key \[--extend\]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
|
@ -534,6 +621,8 @@ proc main {argv} {
|
|||
cmd_session [lrange $argv 1 end]
|
||||
} elseif {$first_arg eq "service"} {
|
||||
cmd_service [lrange $argv 1 end]
|
||||
} elseif {$first_arg eq "key"} {
|
||||
cmd_key [lrange $argv 1 end]
|
||||
} else {
|
||||
cmd_execute $argv
|
||||
}
|
||||
|
|
|
|||
50
un.ts
50
un.ts
|
|
@ -56,6 +56,7 @@ import * as https from 'https';
|
|||
import * as path from 'path';
|
||||
|
||||
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";
|
||||
|
|
@ -108,6 +109,7 @@ interface Args {
|
|||
destroy: string | null;
|
||||
execute: string | null;
|
||||
command_arg: string | null;
|
||||
extend: boolean;
|
||||
}
|
||||
|
||||
function getApiKey(argsKey: string | null): string {
|
||||
|
|
@ -379,6 +381,42 @@ async function cmdService(args: Args): Promise<void> {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
async function cmdKey(args: Args): Promise<void> {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
|
||||
// Validate the key
|
||||
const result = await apiRequest("/keys/validate", "POST", {}, apiKey);
|
||||
|
||||
if (result.status === "valid") {
|
||||
console.log(`${GREEN}Valid${RESET}`);
|
||||
console.log(`Public Key: ${result.public_key || 'N/A'}`);
|
||||
console.log(`Tier: ${result.tier || '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}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
const args: Args = {
|
||||
command: null,
|
||||
|
|
@ -410,13 +448,14 @@ function parseArgs(argv: string[]): Args {
|
|||
destroy: null,
|
||||
execute: null,
|
||||
command_arg: null,
|
||||
extend: false,
|
||||
};
|
||||
|
||||
let i = 2;
|
||||
while (i < argv.length) {
|
||||
const arg = argv[i];
|
||||
|
||||
if (arg === 'session' || arg === 'service') {
|
||||
if (arg === 'session' || arg === 'service' || arg === 'key') {
|
||||
args.command = arg;
|
||||
i++;
|
||||
} else if (arg === '-e' && i + 1 < argv.length) {
|
||||
|
|
@ -500,6 +539,9 @@ function parseArgs(argv: string[]): Args {
|
|||
} else if (arg === '--command' && i + 1 < argv.length) {
|
||||
args.command_arg = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--extend') {
|
||||
args.extend = true;
|
||||
i++;
|
||||
} else if (!arg.startsWith('-')) {
|
||||
args.sourceFile = arg;
|
||||
i++;
|
||||
|
|
@ -519,6 +561,8 @@ async function main(): Promise<void> {
|
|||
await cmdSession(args);
|
||||
} else if (args.command === 'service') {
|
||||
await cmdService(args);
|
||||
} else if (args.command === 'key') {
|
||||
await cmdKey(args);
|
||||
} else if (args.sourceFile) {
|
||||
await cmdExecute(args);
|
||||
} else {
|
||||
|
|
@ -528,6 +572,7 @@ Usage:
|
|||
${process.argv[1]} [options] <source_file>
|
||||
${process.argv[1]} session [options]
|
||||
${process.argv[1]} service [options]
|
||||
${process.argv[1]} key [options]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Environment variable (multiple allowed)
|
||||
|
|
@ -562,6 +607,9 @@ Service options:
|
|||
--destroy ID Destroy service
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key expiration
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
110
un.v
110
un.v
|
|
@ -46,12 +46,13 @@
|
|||
import os
|
||||
|
||||
const (
|
||||
api_base = 'https://api.unsandbox.com'
|
||||
blue = '\x1b[34m'
|
||||
red = '\x1b[31m'
|
||||
green = '\x1b[32m'
|
||||
yellow = '\x1b[33m'
|
||||
reset = '\x1b[0m'
|
||||
api_base = 'https://api.unsandbox.com'
|
||||
portal_base = 'https://unsandbox.com'
|
||||
blue = '\x1b[34m'
|
||||
red = '\x1b[31m'
|
||||
green = '\x1b[32m'
|
||||
yellow = '\x1b[33m'
|
||||
reset = '\x1b[0m'
|
||||
)
|
||||
|
||||
fn detect_language(filename string) !string {
|
||||
|
|
@ -99,6 +100,82 @@ fn exec_curl(cmd string) string {
|
|||
return result.output
|
||||
}
|
||||
|
||||
fn extract_json_string(json string, key string) string {
|
||||
search := '"${key}":"'
|
||||
start_idx := json.index(search) or { return '' }
|
||||
start := start_idx + search.len
|
||||
|
||||
mut end := start
|
||||
for end < json.len {
|
||||
if json[end] == `"` && (end == 0 || json[end - 1] != `\\`) {
|
||||
break
|
||||
}
|
||||
end++
|
||||
}
|
||||
|
||||
if end > start {
|
||||
raw := json[start..end]
|
||||
// Unescape JSON string
|
||||
return raw.replace('\\n', '\n')
|
||||
.replace('\\r', '\r')
|
||||
.replace('\\t', '\t')
|
||||
.replace('\\"', '"')
|
||||
.replace('\\\\', '\\')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
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 '{}'"
|
||||
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')
|
||||
|
||||
if extend && public_key != '' {
|
||||
url := '${portal_base}/keys/extend?pk=${public_key}'
|
||||
println('${yellow}Opening browser: ${url}${reset}')
|
||||
|
||||
// Try xdg-open (Linux), open (macOS), or start (Windows)
|
||||
os.execute('xdg-open "${url}"') or {
|
||||
os.execute('open "${url}"') or {
|
||||
os.execute('cmd /c start "${url}"') or {
|
||||
eprintln('${red}Error: Could not open browser${reset}')
|
||||
}
|
||||
}
|
||||
}
|
||||
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}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_execute(source_file string, envs []string, artifacts bool, network string, vcpu int, api_key string) {
|
||||
lang := detect_language(source_file) or {
|
||||
eprintln('${red}Error: ${err}${reset}')
|
||||
|
|
@ -263,6 +340,7 @@ fn main() {
|
|||
eprintln('Usage: ${os.args[0]} [options] <source_file>')
|
||||
eprintln(' ${os.args[0]} session [options]')
|
||||
eprintln(' ${os.args[0]} service [options]')
|
||||
eprintln(' ${os.args[0]} key [--extend]')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +469,26 @@ fn main() {
|
|||
return
|
||||
}
|
||||
|
||||
if os.args[1] == 'key' {
|
||||
mut extend := false
|
||||
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
match os.args[i] {
|
||||
'--extend' { extend = true }
|
||||
'-k' {
|
||||
i++
|
||||
api_key = os.args[i]
|
||||
}
|
||||
else {}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
cmd_key(extend, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute mode
|
||||
mut envs := []string{}
|
||||
mut artifacts := false
|
||||
|
|
|
|||
132
un.zig
132
un.zig
|
|
@ -52,6 +52,7 @@ const process = std.process;
|
|||
const mem = std.mem;
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
|
||||
pub fn main() !u8 {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
|
@ -65,6 +66,7 @@ pub fn main() !u8 {
|
|||
std.debug.print("Usage: {s} [options] <source_file>\n", .{args[0]});
|
||||
std.debug.print(" {s} session [options]\n", .{args[0]});
|
||||
std.debug.print(" {s} service [options]\n", .{args[0]});
|
||||
std.debug.print(" {s} key [--extend]\n", .{args[0]});
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +173,136 @@ pub fn main() !u8 {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Handle key command
|
||||
if (mem.eql(u8, args[1], "key")) {
|
||||
var extend = false;
|
||||
var i: usize = 2;
|
||||
while (i < args.len) : (i += 1) {
|
||||
if (mem.eql(u8, args[i], "--extend")) {
|
||||
extend = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (extend) {
|
||||
// First validate to get the public_key
|
||||
const json_file = "/tmp/unsandbox_key_validate.json";
|
||||
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
|
||||
defer allocator.free(cmd_validate);
|
||||
_ = std.c.system(cmd_validate.ptr);
|
||||
|
||||
// Read the JSON response to extract public_key
|
||||
const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
|
||||
std.debug.print("\x1b[31mError reading validation response: {}\x1b[0m\n", .{err});
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
return 1;
|
||||
};
|
||||
defer allocator.free(json_content);
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
|
||||
// Simple JSON parsing to find public_key (looking for "public_key":"value")
|
||||
const pk_prefix = "\"public_key\":\"";
|
||||
var public_key: ?[]const u8 = null;
|
||||
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
|
||||
const value_start = start_idx + pk_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
public_key = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (public_key) |pk| {
|
||||
const url = try std.fmt.allocPrint(allocator, "{s}/keys/extend?pk={s}", .{ PORTAL_BASE, pk });
|
||||
defer allocator.free(url);
|
||||
std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{});
|
||||
const open_cmd = try std.fmt.allocPrint(allocator, "xdg-open '{s}' 2>/dev/null || open '{s}' 2>/dev/null || start '{s}' 2>/dev/null", .{ url, url, url });
|
||||
defer allocator.free(open_cmd);
|
||||
_ = std.c.system(open_cmd.ptr);
|
||||
} else {
|
||||
std.debug.print("\x1b[31mError: Could not extract public_key from response\x1b[0m\n", .{});
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
// Regular validation
|
||||
const json_file = "/tmp/unsandbox_key_validate.json";
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
||||
// Read and parse the response
|
||||
const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
|
||||
std.debug.print("\x1b[31mError reading validation response: {}\x1b[0m\n", .{err});
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
return 1;
|
||||
};
|
||||
defer allocator.free(json_content);
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
|
||||
// Simple JSON parsing (looking for specific fields)
|
||||
const status_prefix = "\"status\":\"";
|
||||
var status: ?[]const u8 = null;
|
||||
if (mem.indexOf(u8, json_content, status_prefix)) |start_idx| {
|
||||
const value_start = start_idx + status_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
status = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (status == null) {
|
||||
std.debug.print("\x1b[31mError: Invalid response from server\x1b[0m\n", .{});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Extract other fields
|
||||
var public_key: ?[]const u8 = null;
|
||||
var tier: ?[]const u8 = null;
|
||||
var expires_at: ?[]const u8 = null;
|
||||
|
||||
const pk_prefix = "\"public_key\":\"";
|
||||
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
|
||||
const value_start = start_idx + pk_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
public_key = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
const tier_prefix = "\"tier\":\"";
|
||||
if (mem.indexOf(u8, json_content, tier_prefix)) |start_idx| {
|
||||
const value_start = start_idx + tier_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
tier = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
const expires_prefix = "\"expires_at\":\"";
|
||||
if (mem.indexOf(u8, json_content, expires_prefix)) |start_idx| {
|
||||
const value_start = start_idx + expires_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
expires_at = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
// Display results based on status
|
||||
if (status) |s| {
|
||||
if (mem.eql(u8, s, "valid")) {
|
||||
std.debug.print("\x1b[32mValid\x1b[0m\n", .{});
|
||||
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
|
||||
if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp});
|
||||
} else if (mem.eql(u8, s, "expired")) {
|
||||
std.debug.print("\x1b[31mExpired\x1b[0m\n", .{});
|
||||
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
|
||||
if (expires_at) |exp| std.debug.print("Expired: {s}\n", .{exp});
|
||||
std.debug.print("\x1b[33mTo renew: Visit {s}/keys/extend\x1b[0m\n", .{PORTAL_BASE});
|
||||
} else if (mem.eql(u8, s, "invalid")) {
|
||||
std.debug.print("\x1b[31mInvalid\x1b[0m\n", .{});
|
||||
} else {
|
||||
std.debug.print("Status: {s}\n", .{s});
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Execute mode - find source file
|
||||
var source_file: ?[]const u8 = null;
|
||||
for (args[1..]) |arg| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue