Add key subcommand to all 31 implementations

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

8
.gitignore vendored
View file

@ -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/

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

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

148
un.ex Normal file → Executable file
View 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
View file

@ -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
View file

@ -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
View 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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View 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
View file

@ -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
View 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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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| {