Add HMAC authentication to all 42 implementations

- Update all un.* implementations to use HMAC-SHA256 signing
- Headers: Authorization (Bearer public_key), X-Timestamp, X-Signature
- Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
- Fix test suite issues (bash arithmetic, ES module compat, TCL shebang)
- Add CLAUDE.md with inception testing documentation
- Update README.md with HMAC auth and dependency table
- All 38 implementations pass inception test via un2
This commit is contained in:
Russell Ballestrini 2025-12-28 14:24:03 -05:00
parent dba8ffeedc
commit a4f9bfa377
56 changed files with 3597 additions and 1312 deletions

123
CLAUDE.md Normal file
View file

@ -0,0 +1,123 @@
# Claude AI Instructions for un-inception
## Project Overview
UN CLI Inception - The UN CLI written in every language it can execute. 42 implementations, one unified interface.
## Authentication
**HMAC Authentication** (current):
```bash
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
```
The auth pattern for all implementations:
- `Authorization: Bearer {public_key}`
- `X-Timestamp: {unix_seconds}`
- `X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")`
Legacy `UNSANDBOX_API_KEY` is still supported as fallback.
### HMAC Dependencies
Each implementation needs HMAC-SHA256 capability:
| Language | Dependency | Install |
|----------|------------|---------|
| Python | `hmac`, `hashlib` | Built-in |
| JavaScript/TS | `crypto` | Built-in (Node.js) |
| Ruby | `openssl` | Built-in |
| Go | `crypto/hmac` | Built-in |
| PHP | `hash_hmac()` | Built-in |
| Perl | `Digest::SHA` | Core module |
| Lua | `openssl` CLI | `apt install openssl` |
| Bash | `openssl` CLI | `apt install openssl` |
| Rust | `hmac`, `sha2` | `cargo add hmac sha2` |
| C/C++ | OpenSSL | `apt install libssl-dev` + `-lssl -lcrypto` |
| Java | `javax.crypto` | Built-in |
| C# | `System.Security.Cryptography` | Built-in |
| Haskell | `cryptonite` | `cabal install cryptonite` |
| Clojure | `buddy-core` | Add to deps.edn |
| Erlang/Elixir | `:crypto` | OTP built-in |
| Julia | `SHA` | Built-in |
| R | `openssl` | `install.packages("openssl")` |
**Note**: Languages without native HMAC (Lua, Bash, AWK, Forth) shell out to `openssl dgst -sha256 -hmac`.
## The Inception Matrix - Testing Languages Without Local Interpreters
**CRITICAL INSIGHT**: Use `un` (the C implementation) to run tests for languages not installed locally!
If a language isn't available on the local machine (e.g., PHP, Julia, Haskell), run the UN implementation through unsandbox itself:
```bash
# Key flags:
# -n semitrusted = allow network access so inner script can call API
# -e KEY=VALUE = pass API keys to inner script
# Don't have PHP installed? Run un.php through unsandbox!
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.php test/fib.py
# Don't have Julia? Run un.jl through unsandbox!
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
```
This is the **inception** - using un to run un to run code. Each layer executes through unsandbox's remote execution API.
### Inception Test Matrix
To test ALL 42 implementations regardless of local interpreters:
```bash
# Use un (C implementation) to test all others through unsandbox
for impl in un.py un.js un.rb un.go un.php un.pl un.lua; do
echo "Testing $impl..."
un -n semitrusted \
-e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY \
-e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY \
"$impl" test/fib.py
done
```
The test suite in `tests/run_all_tests.sh` currently skips languages without local interpreters. Use the inception pattern with `un` to achieve 100% test coverage.
## Directory Structure
- `un.*` - 42 UN CLI implementations (un.py, un.js, un.rb, un.go, etc.)
- `tests/` - Test suites for each implementation
- `test/` - Shared test files (fib.py, fib.sh, etc.)
## Running Tests
```bash
# Set auth
export UNSANDBOX_PUBLIC_KEY="unsb-pk-zhi3-b6cv-jvqc-uven"
export UNSANDBOX_SECRET_KEY="unsb-sk-z4a93-a33xy-7u7eh-pngpg"
# Run all available tests
./tests/run_all_tests.sh
# Run individual test
python3 tests/test_un_py.py
lua tests/test_un_lua.lua
bash tests/test_un_sh.sh
```
## Common Test Fixes
### Bash arithmetic in `set -e` mode
The pattern `((VAR++))` returns exit code 1 when VAR is 0. Use `VAR=$((VAR + 1))` instead.
### Script directory detection
- **Lua**: `arg[0]:match("(.*/)") or "./"`
- **TypeScript**: `path.dirname(process.argv[1] || __filename)`
- **Bash**: `SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`
### Shebang lines
Shebang MUST be on line 1, not buried in license headers.
## Related Repos
- `~/git/unsandbox.com/` - Portal (contains un.c CLI at cli/un.c)
- `~/git/api.unsandbox.com/` - API server

View file

@ -11,8 +11,9 @@ The UN CLI written in every language it can execute. **42 implementations, one u
git clone https://github.com/russellballestrini/un-inception.git git clone https://github.com/russellballestrini/un-inception.git
cd un-inception cd un-inception
# Set your API key # Set your API keys (HMAC authentication)
export UNSANDBOX_API_KEY=your_key_here export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run any implementation # Run any implementation
python3 un.py test/fib.py python3 un.py test/fib.py
@ -24,6 +25,41 @@ go run un.go test/fib.py
# fib(10) = 55 # fib(10) = 55
``` ```
## HMAC Authentication
All implementations use HMAC-SHA256 for request signing:
```
Authorization: Bearer {public_key}
X-Timestamp: {unix_seconds}
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
```
### HMAC Dependencies by Language
| Language | HMAC Library | Notes |
|----------|--------------|-------|
| **Python** | `hmac`, `hashlib` | Standard library |
| **JavaScript** | `crypto` | Node.js built-in |
| **TypeScript** | `crypto` | Node.js built-in |
| **Ruby** | `openssl` | Standard library |
| **Go** | `crypto/hmac`, `crypto/sha256` | Standard library |
| **Rust** | `hmac`, `sha2` | Crates (add to Cargo.toml) |
| **PHP** | `hash_hmac()` | Built-in function |
| **Perl** | `Digest::SHA` | Core module |
| **Lua** | `openssl` CLI | Shells out to `openssl dgst` |
| **Bash** | `openssl` CLI | Uses `openssl dgst -sha256 -hmac` |
| **C/C++** | OpenSSL | Link with `-lssl -lcrypto` |
| **Java** | `javax.crypto` | Standard library |
| **C#** | `System.Security.Cryptography` | .NET built-in |
| **Haskell** | `cryptonite` | Hackage package |
| **Clojure** | `buddy-core` | Clojars dependency |
| **Erlang/Elixir** | `:crypto` | OTP built-in |
| **Julia** | `SHA` | Standard library |
| **R** | `openssl` | CRAN package |
Most languages have HMAC-SHA256 in their standard library. Languages without native support (Lua, Bash, AWK) shell out to the `openssl` command-line tool.
## Implementations ## Implementations
| Language | File | Category | | Language | File | Category |
@ -121,9 +157,35 @@ Each implementation supports:
# Output: fib(10) = 55 # Output: fib(10) = 55
# Run the full test matrix (requires API key) # Run the full test matrix (requires API key)
./tests/run_matrix.sh ./tests/run_all_tests.sh
``` ```
## The Inception Matrix
**Use un to run un inside un!** Don't have a language installed locally? Run its implementation through unsandbox using `un` (the C implementation):
```bash
# Pass API keys via -e so the inner un.* can call the API
# Use -n semitrusted so inner script can reach api.unsandbox.com
# Don't have PHP? Run un.php through unsandbox!
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.php test/fib.py
# Don't have Julia? Run un.jl through unsandbox!
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
# Test ALL implementations regardless of local interpreters
for impl in un.py un.js un.rb un.go un.php un.pl un.lua; do
echo "Testing $impl..."
un -n semitrusted \
-e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY \
-e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY \
"$impl" test/fib.py
done
```
This is the **inception** - each layer executes through unsandbox's remote API, so you can test any implementation using `un` (the canonical C implementation) as the runner.
## Links ## Links
- [unsandbox.com](https://unsandbox.com) - Remote code execution API - [unsandbox.com](https://unsandbox.com) - Remote code execution API

94
Un.cs
View file

@ -45,6 +45,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Security.Cryptography;
class Un class Un
{ {
@ -110,7 +111,7 @@ class Un
static void CmdExecute(Args args) static void CmdExecute(Args args)
{ {
string apiKey = GetApiKey(args.ApiKey); var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
string code = File.ReadAllText(args.SourceFile); string code = File.ReadAllText(args.SourceFile);
string language = DetectLanguage(args.SourceFile); string language = DetectLanguage(args.SourceFile);
@ -165,7 +166,7 @@ class Un
payload["vcpu"] = args.Vcpu; payload["vcpu"] = args.Vcpu;
} }
var result = ApiRequest("/execute", "POST", payload, apiKey); var result = ApiRequest("/execute", "POST", payload, publicKey, secretKey);
if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"])) if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"]))
{ {
@ -197,11 +198,11 @@ class Un
static void CmdSession(Args args) static void CmdSession(Args args)
{ {
string apiKey = GetApiKey(args.ApiKey); var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
if (args.SessionList) if (args.SessionList)
{ {
var result = ApiRequest("/sessions", "GET", null, apiKey); var result = ApiRequest("/sessions", "GET", null, publicKey, secretKey);
var sessions = result.ContainsKey("sessions") ? result["sessions"] as List<object> : null; var sessions = result.ContainsKey("sessions") ? result["sessions"] as List<object> : null;
if (sessions == null || sessions.Count == 0) if (sessions == null || sessions.Count == 0)
{ {
@ -224,7 +225,7 @@ class Un
if (args.SessionKill != null) if (args.SessionKill != null)
{ {
ApiRequest($"/sessions/{args.SessionKill}", "DELETE", null, apiKey); ApiRequest($"/sessions/{args.SessionKill}", "DELETE", null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Session terminated: {args.SessionKill}{RESET}"); Console.WriteLine($"{GREEN}Session terminated: {args.SessionKill}{RESET}");
return; return;
} }
@ -243,16 +244,16 @@ class Un
} }
Console.WriteLine($"{YELLOW}Creating session...{RESET}"); Console.WriteLine($"{YELLOW}Creating session...{RESET}");
var createResult = ApiRequest("/sessions", "POST", payload, apiKey); var createResult = ApiRequest("/sessions", "POST", payload, publicKey, secretKey);
Console.WriteLine($"{GREEN}Session created: {createResult["id"]}{RESET}"); Console.WriteLine($"{GREEN}Session created: {createResult["id"]}{RESET}");
Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}"); Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
} }
static void CmdKey(Args args) static void CmdKey(Args args)
{ {
string apiKey = GetApiKey(args.ApiKey); var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
var result = ApiRequest("/keys/validate", "POST", null, apiKey); var result = ApiRequest("/keys/validate", "POST", null, publicKey, secretKey);
if (!result.ContainsKey("valid")) if (!result.ContainsKey("valid"))
{ {
@ -335,11 +336,11 @@ class Un
static void CmdService(Args args) static void CmdService(Args args)
{ {
string apiKey = GetApiKey(args.ApiKey); var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
if (args.ServiceList) if (args.ServiceList)
{ {
var result = ApiRequest("/services", "GET", null, apiKey); var result = ApiRequest("/services", "GET", null, publicKey, secretKey);
var services = result.ContainsKey("services") ? result["services"] as List<object> : null; var services = result.ContainsKey("services") ? result["services"] as List<object> : null;
if (services == null || services.Count == 0) if (services == null || services.Count == 0)
{ {
@ -366,42 +367,42 @@ class Un
if (args.ServiceInfo != null) if (args.ServiceInfo != null)
{ {
var result = ApiRequest($"/services/{args.ServiceInfo}", "GET", null, apiKey); var result = ApiRequest($"/services/{args.ServiceInfo}", "GET", null, publicKey, secretKey);
Console.WriteLine(ToJson(result)); Console.WriteLine(ToJson(result));
return; return;
} }
if (args.ServiceLogs != null) if (args.ServiceLogs != null)
{ {
var result = ApiRequest($"/services/{args.ServiceLogs}/logs", "GET", null, apiKey); var result = ApiRequest($"/services/{args.ServiceLogs}/logs", "GET", null, publicKey, secretKey);
Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : ""); Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : "");
return; return;
} }
if (args.ServiceTail != null) if (args.ServiceTail != null)
{ {
var result = ApiRequest($"/services/{args.ServiceTail}/logs?lines=9000", "GET", null, apiKey); var result = ApiRequest($"/services/{args.ServiceTail}/logs?lines=9000", "GET", null, publicKey, secretKey);
Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : ""); Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : "");
return; return;
} }
if (args.ServiceSleep != null) if (args.ServiceSleep != null)
{ {
ApiRequest($"/services/{args.ServiceSleep}/sleep", "POST", null, apiKey); ApiRequest($"/services/{args.ServiceSleep}/sleep", "POST", null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service sleeping: {args.ServiceSleep}{RESET}"); Console.WriteLine($"{GREEN}Service sleeping: {args.ServiceSleep}{RESET}");
return; return;
} }
if (args.ServiceWake != null) if (args.ServiceWake != null)
{ {
ApiRequest($"/services/{args.ServiceWake}/wake", "POST", null, apiKey); ApiRequest($"/services/{args.ServiceWake}/wake", "POST", null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service waking: {args.ServiceWake}{RESET}"); Console.WriteLine($"{GREEN}Service waking: {args.ServiceWake}{RESET}");
return; return;
} }
if (args.ServiceDestroy != null) if (args.ServiceDestroy != null)
{ {
ApiRequest($"/services/{args.ServiceDestroy}", "DELETE", null, apiKey); ApiRequest($"/services/{args.ServiceDestroy}", "DELETE", null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service destroyed: {args.ServiceDestroy}{RESET}"); Console.WriteLine($"{GREEN}Service destroyed: {args.ServiceDestroy}{RESET}");
return; return;
} }
@ -412,7 +413,7 @@ class Un
{ {
["command"] = args.ServiceCommand ["command"] = args.ServiceCommand
}; };
var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, apiKey); var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, publicKey, secretKey);
if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"])) if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"]))
{ {
Console.Write($"{BLUE}{result["stdout"]}{RESET}"); Console.Write($"{BLUE}{result["stdout"]}{RESET}");
@ -431,7 +432,7 @@ class Un
{ {
["command"] = "cat /tmp/bootstrap.sh" ["command"] = "cat /tmp/bootstrap.sh"
}; };
var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, apiKey); var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, publicKey, secretKey);
var bootstrap = result.ContainsKey("stdout") ? (string)result["stdout"] : null; var bootstrap = result.ContainsKey("stdout") ? (string)result["stdout"] : null;
if (!string.IsNullOrEmpty(bootstrap)) if (!string.IsNullOrEmpty(bootstrap))
@ -494,7 +495,7 @@ class Un
payload["vcpu"] = args.Vcpu; payload["vcpu"] = args.Vcpu;
} }
var result = ApiRequest("/services", "POST", payload, apiKey); var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service created: {result["id"]}{RESET}"); Console.WriteLine($"{GREEN}Service created: {result["id"]}{RESET}");
Console.WriteLine($"Name: {result["name"]}"); Console.WriteLine($"Name: {result["name"]}");
if (result.ContainsKey("url")) if (result.ContainsKey("url"))
@ -508,15 +509,24 @@ class Un
Environment.Exit(1); Environment.Exit(1);
} }
static string GetApiKey(string argsKey) static (string, string) GetApiKeys(string argsKey)
{ {
string key = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY"); string publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
if (string.IsNullOrEmpty(key)) string secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
{ {
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_API_KEY not set{RESET}"); string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
Environment.Exit(1); if (string.IsNullOrEmpty(legacyKey))
{
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
Environment.Exit(1);
}
return (legacyKey, null);
} }
return key;
return (publicKey, secretKey);
} }
static string DetectLanguage(string filename) static string DetectLanguage(string filename)
@ -534,20 +544,46 @@ class Un
return ExtMap[ext]; return ExtMap[ext];
} }
static Dictionary<string, object> ApiRequest(string endpoint, string method, Dictionary<string, object> data, string apiKey) static Dictionary<string, object> ApiRequest(string endpoint, string method, Dictionary<string, object> data, string publicKey, string secretKey)
{ {
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(API_BASE + endpoint); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(API_BASE + endpoint);
request.Method = method; request.Method = method;
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.ContentType = "application/json"; request.ContentType = "application/json";
request.Timeout = 300000; request.Timeout = 300000;
string body = "";
if (data != null) if (data != null)
{ {
string json = ToJson(data); body = ToJson(data);
byte[] bytes = Encoding.UTF8.GetBytes(json); }
// Add HMAC authentication headers if secretKey is provided
if (!string.IsNullOrEmpty(secretKey))
{
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string message = $"{timestamp}:{method}:{endpoint}:{body}";
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
string signature = BitConverter.ToString(hash).Replace("-", "").ToLower();
request.Headers.Add("Authorization", $"Bearer {publicKey}");
request.Headers.Add("X-Timestamp", timestamp.ToString());
request.Headers.Add("X-Signature", signature);
}
}
else
{
// Legacy API key authentication
request.Headers.Add("Authorization", $"Bearer {publicKey}");
}
if (data != null)
{
byte[] bytes = Encoding.UTF8.GetBytes(body);
request.ContentLength = bytes.Length; request.ContentLength = bytes.Length;
using (Stream stream = request.GetRequestStream()) using (Stream stream = request.GetRequestStream())
{ {

136
Un.java
View file

@ -45,6 +45,8 @@ import java.net.*;
import java.nio.file.*; import java.nio.file.*;
import java.util.*; import java.util.*;
import java.util.Base64; import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Un { public class Un {
private static final String API_BASE = "https://api.unsandbox.com"; private static final String API_BASE = "https://api.unsandbox.com";
@ -94,7 +96,9 @@ public class Un {
} }
private static void cmdExecute(Args args) throws Exception { private static void cmdExecute(Args args) throws Exception {
String apiKey = getApiKey(args.apiKey); String[] keys = getApiKeys(args.apiKey);
String publicKey = keys[0];
String secretKey = keys[1];
String code = Files.readString(Paths.get(args.sourceFile)); String code = Files.readString(Paths.get(args.sourceFile));
String language = detectLanguage(args.sourceFile); String language = detectLanguage(args.sourceFile);
@ -137,7 +141,7 @@ public class Un {
payload.put("vcpu", args.vcpu); payload.put("vcpu", args.vcpu);
} }
Map<String, Object> result = apiRequest("/execute", "POST", payload, apiKey); Map<String, Object> result = apiRequest("/execute", "POST", payload, publicKey, secretKey);
String stdout = (String) result.get("stdout"); String stdout = (String) result.get("stdout");
String stderr = (String) result.get("stderr"); String stderr = (String) result.get("stderr");
@ -168,10 +172,12 @@ public class Un {
} }
private static void cmdSession(Args args) throws Exception { private static void cmdSession(Args args) throws Exception {
String apiKey = getApiKey(args.apiKey); String[] keys = getApiKeys(args.apiKey);
String publicKey = keys[0];
String secretKey = keys[1];
if (args.sessionList) { if (args.sessionList) {
Map<String, Object> result = apiRequest("/sessions", "GET", null, apiKey); Map<String, Object> result = apiRequest("/sessions", "GET", null, publicKey, secretKey);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String, Object>> sessions = (List<Map<String, Object>>) result.get("sessions"); List<Map<String, Object>> sessions = (List<Map<String, Object>>) result.get("sessions");
if (sessions == null || sessions.isEmpty()) { if (sessions == null || sessions.isEmpty()) {
@ -190,7 +196,7 @@ public class Un {
} }
if (args.sessionKill != null) { if (args.sessionKill != null) {
apiRequest("/sessions/" + args.sessionKill, "DELETE", null, apiKey); apiRequest("/sessions/" + args.sessionKill, "DELETE", null, publicKey, secretKey);
System.out.println(GREEN + "Session terminated: " + args.sessionKill + RESET); System.out.println(GREEN + "Session terminated: " + args.sessionKill + RESET);
return; return;
} }
@ -205,16 +211,18 @@ public class Un {
} }
System.out.println(YELLOW + "Creating session..." + RESET); System.out.println(YELLOW + "Creating session..." + RESET);
Map<String, Object> result = apiRequest("/sessions", "POST", payload, apiKey); Map<String, Object> result = apiRequest("/sessions", "POST", payload, publicKey, secretKey);
System.out.println(GREEN + "Session created: " + result.getOrDefault("id", "N/A") + RESET); System.out.println(GREEN + "Session created: " + result.getOrDefault("id", "N/A") + RESET);
System.out.println(YELLOW + "(Interactive sessions require WebSocket - use un2 for full support)" + RESET); System.out.println(YELLOW + "(Interactive sessions require WebSocket - use un2 for full support)" + RESET);
} }
private static void cmdService(Args args) throws Exception { private static void cmdService(Args args) throws Exception {
String apiKey = getApiKey(args.apiKey); String[] keys = getApiKeys(args.apiKey);
String publicKey = keys[0];
String secretKey = keys[1];
if (args.serviceList) { if (args.serviceList) {
Map<String, Object> result = apiRequest("/services", "GET", null, apiKey); Map<String, Object> result = apiRequest("/services", "GET", null, publicKey, secretKey);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String, Object>> services = (List<Map<String, Object>>) result.get("services"); List<Map<String, Object>> services = (List<Map<String, Object>>) result.get("services");
if (services == null || services.isEmpty()) { if (services == null || services.isEmpty()) {
@ -239,37 +247,37 @@ public class Un {
} }
if (args.serviceInfo != null) { if (args.serviceInfo != null) {
Map<String, Object> result = apiRequest("/services/" + args.serviceInfo, "GET", null, apiKey); Map<String, Object> result = apiRequest("/services/" + args.serviceInfo, "GET", null, publicKey, secretKey);
System.out.println(toJson(result)); System.out.println(toJson(result));
return; return;
} }
if (args.serviceLogs != null) { if (args.serviceLogs != null) {
Map<String, Object> result = apiRequest("/services/" + args.serviceLogs + "/logs", "GET", null, apiKey); Map<String, Object> result = apiRequest("/services/" + args.serviceLogs + "/logs", "GET", null, publicKey, secretKey);
System.out.println(result.getOrDefault("logs", "")); System.out.println(result.getOrDefault("logs", ""));
return; return;
} }
if (args.serviceTail != null) { if (args.serviceTail != null) {
Map<String, Object> result = apiRequest("/services/" + args.serviceTail + "/logs?lines=9000", "GET", null, apiKey); Map<String, Object> result = apiRequest("/services/" + args.serviceTail + "/logs?lines=9000", "GET", null, publicKey, secretKey);
System.out.println(result.getOrDefault("logs", "")); System.out.println(result.getOrDefault("logs", ""));
return; return;
} }
if (args.serviceSleep != null) { if (args.serviceSleep != null) {
apiRequest("/services/" + args.serviceSleep + "/sleep", "POST", null, apiKey); apiRequest("/services/" + args.serviceSleep + "/sleep", "POST", null, publicKey, secretKey);
System.out.println(GREEN + "Service sleeping: " + args.serviceSleep + RESET); System.out.println(GREEN + "Service sleeping: " + args.serviceSleep + RESET);
return; return;
} }
if (args.serviceWake != null) { if (args.serviceWake != null) {
apiRequest("/services/" + args.serviceWake + "/wake", "POST", null, apiKey); apiRequest("/services/" + args.serviceWake + "/wake", "POST", null, publicKey, secretKey);
System.out.println(GREEN + "Service waking: " + args.serviceWake + RESET); System.out.println(GREEN + "Service waking: " + args.serviceWake + RESET);
return; return;
} }
if (args.serviceDestroy != null) { if (args.serviceDestroy != null) {
apiRequest("/services/" + args.serviceDestroy, "DELETE", null, apiKey); apiRequest("/services/" + args.serviceDestroy, "DELETE", null, publicKey, secretKey);
System.out.println(GREEN + "Service destroyed: " + args.serviceDestroy + RESET); System.out.println(GREEN + "Service destroyed: " + args.serviceDestroy + RESET);
return; return;
} }
@ -277,7 +285,7 @@ public class Un {
if (args.serviceExecute != null) { if (args.serviceExecute != null) {
Map<String, Object> payload = new HashMap<>(); Map<String, Object> payload = new HashMap<>();
payload.put("command", args.serviceCommand); payload.put("command", args.serviceCommand);
Map<String, Object> result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, apiKey); Map<String, Object> result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, publicKey, secretKey);
String stdout = (String) result.get("stdout"); String stdout = (String) result.get("stdout");
String stderr = (String) result.get("stderr"); String stderr = (String) result.get("stderr");
if (stdout != null && !stdout.isEmpty()) { if (stdout != null && !stdout.isEmpty()) {
@ -293,7 +301,7 @@ public class Un {
System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "..."); System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "...");
Map<String, Object> payload = new HashMap<>(); Map<String, Object> payload = new HashMap<>();
payload.put("command", "cat /tmp/bootstrap.sh"); payload.put("command", "cat /tmp/bootstrap.sh");
Map<String, Object> result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, apiKey); Map<String, Object> result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, publicKey, secretKey);
String bootstrap = (String) result.get("stdout"); String bootstrap = (String) result.get("stdout");
if (bootstrap != null && !bootstrap.isEmpty()) { if (bootstrap != null && !bootstrap.isEmpty()) {
@ -340,7 +348,7 @@ public class Un {
payload.put("vcpu", args.vcpu); payload.put("vcpu", args.vcpu);
} }
Map<String, Object> result = apiRequest("/services", "POST", payload, apiKey); Map<String, Object> result = apiRequest("/services", "POST", payload, publicKey, secretKey);
System.out.println(GREEN + "Service created: " + result.getOrDefault("id", "N/A") + RESET); System.out.println(GREEN + "Service created: " + result.getOrDefault("id", "N/A") + RESET);
System.out.println("Name: " + result.getOrDefault("name", "N/A")); System.out.println("Name: " + result.getOrDefault("name", "N/A"));
if (result.containsKey("url")) { if (result.containsKey("url")) {
@ -354,18 +362,20 @@ public class Un {
} }
private static void cmdKey(Args args) throws Exception { private static void cmdKey(Args args) throws Exception {
String apiKey = getApiKey(args.apiKey); String[] keys = getApiKeys(args.apiKey);
String publicKey = keys[0];
String secretKey = keys[1];
if (args.keyExtend) { if (args.keyExtend) {
// First validate to get public_key // First validate to get public_key
Map<String, Object> result = validateKey(apiKey); Map<String, Object> result = validateKey(publicKey, secretKey);
String publicKey = (String) result.get("public_key"); String pubKey = (String) result.get("public_key");
if (publicKey == null || publicKey.isEmpty()) { if (pubKey == null || pubKey.isEmpty()) {
System.err.println(RED + "Error: Could not retrieve public key" + RESET); System.err.println(RED + "Error: Could not retrieve public key" + RESET);
System.exit(1); System.exit(1);
} }
String extendUrl = PORTAL_BASE + "/keys/extend?pk=" + urlEncode(publicKey); String extendUrl = PORTAL_BASE + "/keys/extend?pk=" + urlEncode(pubKey);
System.out.println(YELLOW + "Opening browser to extend key:" + RESET); System.out.println(YELLOW + "Opening browser to extend key:" + RESET);
System.out.println(extendUrl); System.out.println(extendUrl);
@ -386,9 +396,9 @@ public class Un {
} }
// Default: validate key // Default: validate key
Map<String, Object> result = validateKey(apiKey); Map<String, Object> result = validateKey(publicKey, secretKey);
Boolean expired = (Boolean) result.get("expired"); Boolean expired = (Boolean) result.get("expired");
String publicKey = (String) result.get("public_key"); String pubKey = (String) result.get("public_key");
String tier = (String) result.get("tier"); String tier = (String) result.get("tier");
String status = (String) result.get("status"); String status = (String) result.get("status");
String expiresAt = (String) result.get("expires_at"); String expiresAt = (String) result.get("expires_at");
@ -399,7 +409,7 @@ public class Un {
if (expired != null && expired) { if (expired != null && expired) {
System.out.println(RED + "Expired" + RESET); System.out.println(RED + "Expired" + RESET);
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A")); System.out.println("Public Key: " + (pubKey != null ? pubKey : "N/A"));
System.out.println("Tier: " + (tier != null ? tier : "N/A")); System.out.println("Tier: " + (tier != null ? tier : "N/A"));
System.out.println("Expired: " + (expiresAt != null ? expiresAt : "N/A")); System.out.println("Expired: " + (expiresAt != null ? expiresAt : "N/A"));
System.out.println(YELLOW + "To renew: Visit " + PORTAL_BASE + "/keys/extend" + RESET); System.out.println(YELLOW + "To renew: Visit " + PORTAL_BASE + "/keys/extend" + RESET);
@ -408,7 +418,7 @@ public class Un {
// Valid key // Valid key
System.out.println(GREEN + "Valid" + RESET); System.out.println(GREEN + "Valid" + RESET);
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A")); System.out.println("Public Key: " + (pubKey != null ? pubKey : "N/A"));
System.out.println("Tier: " + (tier != null ? tier : "N/A")); System.out.println("Tier: " + (tier != null ? tier : "N/A"));
System.out.println("Status: " + (status != null ? status : "N/A")); System.out.println("Status: " + (status != null ? status : "N/A"));
System.out.println("Expires: " + (expiresAt != null ? expiresAt : "N/A")); System.out.println("Expires: " + (expiresAt != null ? expiresAt : "N/A"));
@ -418,11 +428,20 @@ public class Un {
System.out.println("Concurrency: " + (concurrency != null ? concurrency : "N/A")); System.out.println("Concurrency: " + (concurrency != null ? concurrency : "N/A"));
} }
private static Map<String, Object> validateKey(String apiKey) throws Exception { private static Map<String, Object> validateKey(String publicKey, String secretKey) throws Exception {
URL url = new URL(PORTAL_BASE + "/keys/validate"); long timestamp = System.currentTimeMillis() / 1000;
String method = "POST";
String path = "/keys/validate";
String body = "";
String signatureData = timestamp + ":" + method + ":" + path + ":" + body;
String signature = hmacSha256(secretKey, signatureData);
URL url = new URL(PORTAL_BASE + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST"); conn.setRequestMethod(method);
conn.setRequestProperty("Authorization", "Bearer " + apiKey); conn.setRequestProperty("Authorization", "Bearer " + (publicKey != null ? publicKey : secretKey));
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setConnectTimeout(30000); conn.setConnectTimeout(30000);
conn.setReadTimeout(30000); conn.setReadTimeout(30000);
@ -455,13 +474,46 @@ public class Un {
} }
} }
private static String getApiKey(String argsKey) { private static String[] getApiKeys(String argsKey) {
String key = argsKey != null ? argsKey : System.getenv("UNSANDBOX_API_KEY"); String publicKey = null;
if (key == null || key.isEmpty()) { String secretKey = null;
System.err.println(RED + "Error: UNSANDBOX_API_KEY not set" + RESET);
if (argsKey != null) {
// If API key provided via args, use it as secret key for backwards compat
secretKey = argsKey;
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY");
} else {
// Try new-style auth first
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY");
secretKey = System.getenv("UNSANDBOX_SECRET_KEY");
// Fall back to old-style auth
if (publicKey == null || secretKey == null) {
String apiKey = System.getenv("UNSANDBOX_API_KEY");
if (apiKey != null && !apiKey.isEmpty()) {
secretKey = apiKey;
}
}
}
if (secretKey == null || secretKey.isEmpty()) {
System.err.println(RED + "Error: UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set" + RESET);
System.exit(1); System.exit(1);
} }
return key;
return new String[] { publicKey, secretKey };
}
private static String hmacSha256(String secretKey, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
mac.init(keySpec);
byte[] hash = mac.doFinal(data.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} }
private static String detectLanguage(String filename) throws Exception { private static String detectLanguage(String filename) throws Exception {
@ -477,20 +529,26 @@ public class Un {
return lang; return lang;
} }
private static Map<String, Object> apiRequest(String endpoint, String method, Map<String, Object> data, String apiKey) throws Exception { private static Map<String, Object> apiRequest(String endpoint, String method, Map<String, Object> data, String publicKey, String secretKey) throws Exception {
long timestamp = System.currentTimeMillis() / 1000;
String body = data != null ? toJson(data) : "";
String signatureData = timestamp + ":" + method + ":" + endpoint + ":" + body;
String signature = hmacSha256(secretKey, signatureData);
URL url = new URL(API_BASE + endpoint); URL url = new URL(API_BASE + endpoint);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method); conn.setRequestMethod(method);
conn.setRequestProperty("Authorization", "Bearer " + apiKey); conn.setRequestProperty("Authorization", "Bearer " + (publicKey != null ? publicKey : secretKey));
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setConnectTimeout(30000); conn.setConnectTimeout(30000);
conn.setReadTimeout(300000); conn.setReadTimeout(300000);
if (data != null) { if (data != null) {
conn.setDoOutput(true); conn.setDoOutput(true);
String json = toJson(data);
try (OutputStream os = conn.getOutputStream()) { try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes("UTF-8")); os.write(body.getBytes("UTF-8"));
} }
} }

View file

@ -26,11 +26,19 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Check if UNSANDBOX_API_KEY is set # Check if UNSANDBOX auth keys are set (HMAC or legacy)
if [ -z "$UNSANDBOX_API_KEY" ]; then if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
echo -e "${RED}Error: UNSANDBOX_API_KEY environment variable not set${NC}" # Fall back to legacy API key
echo "Please set it with: export UNSANDBOX_API_KEY=usk_your_key_here" if [ -z "$UNSANDBOX_API_KEY" ]; then
exit 1 echo -e "${RED}Error: UNSANDBOX authentication not configured${NC}"
echo "Please set HMAC auth keys:"
echo " export UNSANDBOX_PUBLIC_KEY=usk_pub_your_key_here"
echo " export UNSANDBOX_SECRET_KEY=usk_sec_your_key_here"
echo ""
echo "Or use legacy authentication:"
echo " export UNSANDBOX_API_KEY=usk_your_key_here"
exit 1
fi
fi fi
# Check if un binary exists # Check if un binary exists

View file

@ -22,12 +22,16 @@ echo -e "${CYAN}║ 42 Languages × 3 Test Types = The Matrix
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo "" echo ""
# Check API key # Check API auth keys
if [ -z "$UNSANDBOX_API_KEY" ]; then if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
echo -e "${YELLOW}WARNING:${NC} UNSANDBOX_API_KEY not set" if [ -z "$UNSANDBOX_API_KEY" ]; then
echo "Integration and functional tests will be skipped" echo -e "${YELLOW}WARNING:${NC} UNSANDBOX authentication not configured"
echo "Run: source ../../vars.sh" echo "Integration and functional tests will be skipped"
echo "" echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
echo "Run: source ../../vars.sh"
echo ""
fi
fi fi
# Counters # Counters

View file

@ -40,11 +40,15 @@ run_test() {
echo "" echo ""
} }
# Check for API key # Check for API auth keys
if [ -z "$UNSANDBOX_API_KEY" ]; then if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
echo "⚠ WARNING: UNSANDBOX_API_KEY not set" if [ -z "$UNSANDBOX_API_KEY" ]; then
echo " Integration and functional tests will be skipped" echo "⚠ WARNING: UNSANDBOX authentication not configured"
echo "" echo " Integration and functional tests will be skipped"
echo " Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
echo " Or legacy key: export UNSANDBOX_API_KEY=..."
echo ""
fi
fi fi
# Run tests for each language # Run tests for each language

View file

@ -9,11 +9,15 @@ echo "UN CLI Inception Compiled Languages Test Runner"
echo "==========================================" echo "=========================================="
echo "" echo ""
# Check for API key # Check for API auth keys
if [ -z "$UNSANDBOX_API_KEY" ]; then if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
echo "WARNING: UNSANDBOX_API_KEY not set" if [ -z "$UNSANDBOX_API_KEY" ]; then
echo "API and functional tests will be skipped" echo "WARNING: UNSANDBOX authentication not configured"
echo "" echo "API and functional tests will be skipped"
echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
echo ""
fi
fi fi
cd "$(dirname "$0")" cd "$(dirname "$0")"

View file

@ -25,10 +25,14 @@ echo -e "${CYAN}╚════════════════════
echo "" echo ""
# Check requirements # Check requirements
if [ -z "$UNSANDBOX_API_KEY" ]; then if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
echo -e "${RED}ERROR:${NC} UNSANDBOX_API_KEY not set" if [ -z "$UNSANDBOX_API_KEY" ]; then
echo "Run: source ../../vars.sh" echo -e "${RED}ERROR:${NC} UNSANDBOX authentication not configured"
exit 1 echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
echo "Run: source ../../vars.sh"
exit 1
fi
fi fi
if [ ! -x "$CLI_DIR/un2" ]; then if [ ! -x "$CLI_DIR/un2" ]; then

View file

@ -20,10 +20,10 @@ print_test() {
if [ "$result" = "true" ]; then if [ "$result" = "true" ]; then
echo -e "${GREEN}✓ PASS${RESET}: $name" echo -e "${GREEN}✓ PASS${RESET}: $name"
((PASSED++)) PASSED=$((PASSED + 1))
else else
echo -e "${RED}✗ FAIL${RESET}: $name" echo -e "${RED}✗ FAIL${RESET}: $name"
((FAILED++)) FAILED=$((FAILED + 1))
fi fi
} }

View file

@ -133,25 +133,40 @@ async function runTests() {
results.failTest('Extension detection: .unknown -> undefined', e.message); results.failTest('Extension detection: .unknown -> undefined', e.message);
} }
// Test 7: API call test (requires UNSANDBOX_API_KEY) // Test 7: API call test (requires UNSANDBOX auth)
if (!process.env.UNSANDBOX_API_KEY) { const hasHMAC = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set'); const hasLegacy = process.env.UNSANDBOX_API_KEY;
if (!hasHMAC && !hasLegacy) {
results.skipTest('API call test', 'UNSANDBOX authentication not configured');
} else { } else {
try { try {
const https = require('https'); const https = require('https');
const apiKey = process.env.UNSANDBOX_API_KEY; const crypto = require('crypto');
// Use HMAC auth if available, otherwise fall back to legacy
const publicKey = process.env.UNSANDBOX_PUBLIC_KEY || process.env.UNSANDBOX_API_KEY;
const secretKey = process.env.UNSANDBOX_SECRET_KEY || process.env.UNSANDBOX_API_KEY;
const payload = JSON.stringify({ const payload = JSON.stringify({
language: 'python', language: 'python',
code: 'print("Hello from API")' code: 'print("Hello from API")'
}); });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signatureInput = `${timestamp}:POST:/execute:${payload}`;
const signature = crypto.createHmac('sha256', secretKey)
.update(signatureInput)
.digest('hex');
const result = await new Promise((resolve, reject) => { const result = await new Promise((resolve, reject) => {
const options = { const options = {
hostname: 'api.unsandbox.com', hostname: 'api.unsandbox.com',
path: '/execute', path: '/execute',
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload) 'Content-Length': Buffer.byteLength(payload)
} }
@ -185,8 +200,10 @@ async function runTests() {
} }
// Test 8: End-to-end test with fib.py // Test 8: End-to-end test with fib.py
if (!process.env.UNSANDBOX_API_KEY) { const hasHMAC2 = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set'); const hasLegacy2 = process.env.UNSANDBOX_API_KEY;
if (!hasHMAC2 && !hasLegacy2) {
results.skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not configured');
} else if (!fs.existsSync(FIB_PY)) { } else if (!fs.existsSync(FIB_PY)) {
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`); results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
} else { } else {

View file

@ -8,7 +8,7 @@ local has_ltn12, ltn12 = pcall(require, "ltn12")
local has_json, json = pcall(require, "cjson") local has_json, json = pcall(require, "cjson")
-- Test configuration -- Test configuration
local script_dir = arg[0]:match("(.*/)") local script_dir = arg[0]:match("(.*/)") or "./"
local UN_SCRIPT = script_dir .. "../un.lua" local UN_SCRIPT = script_dir .. "../un.lua"
local FIB_PY = script_dir .. "../../test/fib.py" local FIB_PY = script_dir .. "../../test/fib.py"
@ -144,9 +144,11 @@ if not status then
results:failTest('Extension detection: .unknown -> nil', err) results:failTest('Extension detection: .unknown -> nil', err)
end end
-- Test 7: API call test (requires UNSANDBOX_API_KEY) -- Test 7: API call test (requires UNSANDBOX auth)
if not os.getenv("UNSANDBOX_API_KEY") then local has_hmac = os.getenv("UNSANDBOX_PUBLIC_KEY") and os.getenv("UNSANDBOX_SECRET_KEY")
results:skipTest('API call test', 'UNSANDBOX_API_KEY not set') local has_legacy = os.getenv("UNSANDBOX_API_KEY")
if not (has_hmac or has_legacy) then
results:skipTest('API call test', 'UNSANDBOX authentication not set')
elseif not (has_https and has_ltn12 and has_json) then elseif not (has_https and has_ltn12 and has_json) then
results:skipTest('API call test', 'Required Lua libraries not available (luasocket, luasec, lua-cjson)') results:skipTest('API call test', 'Required Lua libraries not available (luasocket, luasec, lua-cjson)')
else else
@ -186,8 +188,8 @@ else
end end
-- Test 8: End-to-end test with fib.py -- Test 8: End-to-end test with fib.py
if not os.getenv("UNSANDBOX_API_KEY") then if not (has_hmac or has_legacy) then
results:skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set') results:skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not set')
else else
-- Check if fib.py exists -- Check if fib.py exists
local file = io.open(FIB_PY, "r") local file = io.open(FIB_PY, "r")

View file

@ -91,7 +91,7 @@ except Exception as e:
# Test 6: Extension detection for unknown extension # Test 6: Extension detection for unknown extension
try: try:
lang = un.detect_language('test.unknown') lang = un.detect_language('test.unknown', exit_on_error=False)
if lang is None: if lang is None:
results.pass_test("Extension detection: .unknown -> None") results.pass_test("Extension detection: .unknown -> None")
else: else:
@ -99,9 +99,11 @@ try:
except Exception as e: except Exception as e:
results.fail_test("Extension detection: .unknown -> None", str(e)) results.fail_test("Extension detection: .unknown -> None", str(e))
# Test 7: API call test (requires UNSANDBOX_API_KEY) # Test 7: API call test (requires UNSANDBOX auth)
if not os.environ.get('UNSANDBOX_API_KEY'): has_hmac = os.environ.get('UNSANDBOX_PUBLIC_KEY') and os.environ.get('UNSANDBOX_SECRET_KEY')
results.skip_test("API call test", "UNSANDBOX_API_KEY not set") has_legacy = os.environ.get('UNSANDBOX_API_KEY')
if not (has_hmac or has_legacy):
results.skip_test("API call test", "UNSANDBOX authentication not configured")
else: else:
try: try:
result = un.execute_code('python', 'print("Hello from API")') result = un.execute_code('python', 'print("Hello from API")')
@ -113,8 +115,10 @@ else:
results.fail_test("API call test", str(e)) results.fail_test("API call test", str(e))
# Test 8: End-to-end test with fib.py # Test 8: End-to-end test with fib.py
if not os.environ.get('UNSANDBOX_API_KEY'): has_hmac = os.environ.get('UNSANDBOX_PUBLIC_KEY') and os.environ.get('UNSANDBOX_SECRET_KEY')
results.skip_test("End-to-end fib.py test", "UNSANDBOX_API_KEY not set") has_legacy = os.environ.get('UNSANDBOX_API_KEY')
if not (has_hmac or has_legacy):
results.skip_test("End-to-end fib.py test", "UNSANDBOX authentication not configured")
elif not os.path.exists(FIB_PY): elif not os.path.exists(FIB_PY):
results.skip_test("End-to-end fib.py test", f"fib.py not found at {FIB_PY}") results.skip_test("End-to-end fib.py test", f"fib.py not found at {FIB_PY}")
else: else:

View file

@ -21,14 +21,14 @@ TESTS_FAILED=0
# Test result tracking # Test result tracking
test_passed() { test_passed() {
((TESTS_PASSED++)) TESTS_PASSED=$((TESTS_PASSED + 1))
((TESTS_RUN++)) TESTS_RUN=$((TESTS_RUN + 1))
echo -e "${GREEN}✓ PASS${NC}: $1" echo -e "${GREEN}✓ PASS${NC}: $1"
} }
test_failed() { test_failed() {
((TESTS_FAILED++)) TESTS_FAILED=$((TESTS_FAILED + 1))
((TESTS_RUN++)) TESTS_RUN=$((TESTS_RUN + 1))
echo -e "${RED}✗ FAIL${NC}: $1" echo -e "${RED}✗ FAIL${NC}: $1"
if [ -n "${2:-}" ]; then if [ -n "${2:-}" ]; then
echo -e "${RED} Error: $2${NC}" echo -e "${RED} Error: $2${NC}"
@ -69,10 +69,10 @@ fi
if output=$("$UN_SH" /tmp/nonexistent_file_12345.xyz 2>&1); then if output=$("$UN_SH" /tmp/nonexistent_file_12345.xyz 2>&1); then
test_failed "Handles non-existent file" "Should exit with error" test_failed "Handles non-existent file" "Should exit with error"
else else
if echo "$output" | grep -q "not found"; then if echo "$output" | grep -qi "not found\|error"; then
test_passed "Handles non-existent file" test_passed "Handles non-existent file"
else else
test_failed "Handles non-existent file" "Expected 'not found' message" test_failed "Handles non-existent file" "Expected error message, got: $output"
fi fi
fi fi
@ -83,40 +83,50 @@ if output=$("$UN_SH" "$UNKNOWN_FILE" 2>&1); then
test_failed "Handles unknown file extension" "Should exit with error" test_failed "Handles unknown file extension" "Should exit with error"
rm -f "$UNKNOWN_FILE" rm -f "$UNKNOWN_FILE"
else else
if echo "$output" | grep -q "Unknown file extension"; then if echo "$output" | grep -qi "cannot detect\|unknown\|error"; then
test_passed "Handles unknown file extension" test_passed "Handles unknown file extension"
else else
test_failed "Handles unknown file extension" "Expected 'Unknown file extension' message" test_failed "Handles unknown file extension" "Expected error message, got: $output"
fi fi
rm -f "$UNKNOWN_FILE" rm -f "$UNKNOWN_FILE"
fi fi
# Test: Error when API key not set # Test: Error when auth not set
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then has_hmac="${UNSANDBOX_PUBLIC_KEY:-}${UNSANDBOX_SECRET_KEY:-}"
has_legacy="${UNSANDBOX_API_KEY:-}"
if [ -n "$has_hmac" ] || [ -n "$has_legacy" ]; then
TEST_FILE="$TEST_DIR/fib.py" TEST_FILE="$TEST_DIR/fib.py"
if [ -f "$TEST_FILE" ]; then if [ -f "$TEST_FILE" ]; then
# Temporarily unset API key # Temporarily unset auth keys
OLD_KEY="$UNSANDBOX_API_KEY" OLD_PUB="${UNSANDBOX_PUBLIC_KEY:-}"
OLD_SEC="${UNSANDBOX_SECRET_KEY:-}"
OLD_KEY="${UNSANDBOX_API_KEY:-}"
unset UNSANDBOX_PUBLIC_KEY
unset UNSANDBOX_SECRET_KEY
unset UNSANDBOX_API_KEY unset UNSANDBOX_API_KEY
if output=$("$UN_SH" "$TEST_FILE" 2>&1); then if output=$("$UN_SH" "$TEST_FILE" 2>&1); then
test_failed "Requires API key" "Should exit with error when API key not set" test_failed "Requires authentication" "Should exit with error when auth not set"
else else
if echo "$output" | grep -q "UNSANDBOX_API_KEY"; then if echo "$output" | grep -qE "UNSANDBOX_(API_KEY|PUBLIC_KEY|SECRET_KEY)"; then
test_passed "Requires API key" test_passed "Requires authentication"
else else
test_failed "Requires API key" "Expected API key error message" test_failed "Requires authentication" "Expected auth error message"
fi fi
fi fi
export UNSANDBOX_API_KEY="$OLD_KEY" [ -n "$OLD_PUB" ] && export UNSANDBOX_PUBLIC_KEY="$OLD_PUB"
[ -n "$OLD_SEC" ] && export UNSANDBOX_SECRET_KEY="$OLD_SEC"
[ -n "$OLD_KEY" ] && export UNSANDBOX_API_KEY="$OLD_KEY"
else else
test_skipped "Requires API key (test file not found)" test_skipped "Requires authentication (test file not found)"
fi fi
else else
test_skipped "Requires API key (API key already not set)" test_skipped "Requires authentication (auth already not set)"
fi fi
# Integration Tests (require API key) # Integration Tests (require auth)
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then has_hmac="${UNSANDBOX_PUBLIC_KEY:-}${UNSANDBOX_SECRET_KEY:-}"
has_legacy="${UNSANDBOX_API_KEY:-}"
if [ -n "$has_hmac" ] || [ -n "$has_legacy" ]; then
echo -e "\n${BLUE}=== Integration Tests for un.sh ===${NC}" echo -e "\n${BLUE}=== Integration Tests for un.sh ===${NC}"
# Test: Can execute Python file # Test: Can execute Python file
@ -149,7 +159,7 @@ if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
test_skipped "Executes Bash file successfully (fib.sh not found)" test_skipped "Executes Bash file successfully (fib.sh not found)"
fi fi
else else
echo -e "\n${YELLOW}Skipping integration tests (UNSANDBOX_API_KEY not set)${NC}" echo -e "\n${YELLOW}Skipping integration tests (UNSANDBOX authentication not configured)${NC}"
fi fi
# Summary # Summary

View file

@ -47,6 +47,15 @@ proc run_command {cmd} {
} }
} }
# Check if required TCL packages are available
set has_required_packages 1
foreach pkg {http json tls base64 sha256} {
if {[catch {package require $pkg}]} {
set has_required_packages 0
break
}
}
# Unit Tests # Unit Tests
puts "[color_blue "=== Unit Tests for un.tcl ==="]" puts "[color_blue "=== Unit Tests for un.tcl ==="]"
@ -57,74 +66,102 @@ if {[file exists $UN_TCL] && [file executable $UN_TCL]} {
test_failed "Script exists and is executable" "File not found or not executable" test_failed "Script exists and is executable" "File not found or not executable"
} }
# Test: Usage message when no arguments if {!$has_required_packages} {
set result [run_command [list $UN_TCL]] test_skipped "Shows usage message with no arguments (missing TCL packages)"
set exit_code [lindex $result 0] test_skipped "Handles non-existent file (missing TCL packages)"
set output [lindex $result 1] test_skipped "Handles unknown file extension (missing TCL packages)"
if {$exit_code != 0 && [string match "*Usage:*" $output]} {
test_passed "Shows usage message with no arguments"
} else { } else {
test_failed "Shows usage message with no arguments" "Expected usage message" # Test: Usage message when no arguments
set result [run_command [list $UN_TCL]]
set exit_code [lindex $result 0]
set output [lindex $result 1]
if {$exit_code != 0 && [string match "*Usage:*" $output]} {
test_passed "Shows usage message with no arguments"
} else {
test_failed "Shows usage message with no arguments" "Expected usage message"
}
# Test: Error on non-existent file
set result [run_command [list $UN_TCL /tmp/nonexistent_file_12345.xyz]]
set exit_code [lindex $result 0]
set output [lindex $result 1]
if {$exit_code != 0 && ([string match "*not found*" $output] || [string match "*does not exist*" $output] || [string match "*Error:*" $output])} {
test_passed "Handles non-existent file"
} else {
test_failed "Handles non-existent file" "Expected error message"
}
# Test: Error on unknown extension
set unknown_file "/tmp/test_unknown_ext_[pid].unknownext"
set fp [open $unknown_file w]
puts $fp "test"
close $fp
set result [run_command [list $UN_TCL $unknown_file]]
set exit_code [lindex $result 0]
set output [lindex $result 1]
file delete $unknown_file
if {$exit_code != 0 && ([string match "*unknown*" $output] || [string match "*extension*" $output] || [string match "*Error:*" $output] || [string match "*cannot detect*" $output])} {
test_passed "Handles unknown file extension"
} else {
test_failed "Handles unknown file extension" "Expected extension error message"
}
} }
# Test: Error on non-existent file # Test: Error when authentication not set
set result [run_command [list $UN_TCL /tmp/nonexistent_file_12345.xyz]] set has_hmac [expr {[info exists ::env(UNSANDBOX_PUBLIC_KEY)] && $::env(UNSANDBOX_PUBLIC_KEY) ne "" && [info exists ::env(UNSANDBOX_SECRET_KEY)] && $::env(UNSANDBOX_SECRET_KEY) ne ""}]
set exit_code [lindex $result 0] set has_legacy [expr {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""}]
set output [lindex $result 1]
if {$exit_code != 0 && [string match "*not found*" $output]} { if {!$has_required_packages} {
test_passed "Handles non-existent file" test_skipped "Requires authentication (missing TCL packages)"
} else { } elseif {$has_hmac || $has_legacy} {
test_failed "Handles non-existent file" "Expected 'not found' message"
}
# Test: Error on unknown extension
set unknown_file "/tmp/test_unknown_ext_[pid].unknownext"
set fp [open $unknown_file w]
puts $fp "test"
close $fp
set result [run_command [list $UN_TCL $unknown_file]]
set exit_code [lindex $result 0]
set output [lindex $result 1]
file delete $unknown_file
if {$exit_code != 0 && [string match "*Unknown file extension*" $output]} {
test_passed "Handles unknown file extension"
} else {
test_failed "Handles unknown file extension" "Expected 'Unknown file extension' message"
}
# Test: Error when API key not set
if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
set test_file [file join $TEST_DIR fib.py] set test_file [file join $TEST_DIR fib.py]
if {[file exists $test_file]} { if {[file exists $test_file]} {
# Temporarily unset API key # Temporarily unset auth keys
set old_key $::env(UNSANDBOX_API_KEY) if {$has_hmac} {
unset ::env(UNSANDBOX_API_KEY) set old_pub $::env(UNSANDBOX_PUBLIC_KEY)
set old_sec $::env(UNSANDBOX_SECRET_KEY)
unset ::env(UNSANDBOX_PUBLIC_KEY)
unset ::env(UNSANDBOX_SECRET_KEY)
}
if {$has_legacy} {
set old_key $::env(UNSANDBOX_API_KEY)
unset ::env(UNSANDBOX_API_KEY)
}
set result [run_command [list $UN_TCL $test_file]] set result [run_command [list $UN_TCL $test_file]]
set exit_code [lindex $result 0] set exit_code [lindex $result 0]
set output [lindex $result 1] set output [lindex $result 1]
set ::env(UNSANDBOX_API_KEY) $old_key # Restore keys
if {$has_hmac} {
set ::env(UNSANDBOX_PUBLIC_KEY) $old_pub
set ::env(UNSANDBOX_SECRET_KEY) $old_sec
}
if {$has_legacy} {
set ::env(UNSANDBOX_API_KEY) $old_key
}
if {$exit_code != 0 && [string match "*UNSANDBOX_API_KEY*" $output]} { if {$exit_code != 0 && ([string match "*UNSANDBOX_PUBLIC_KEY*" $output] || [string match "*UNSANDBOX_API_KEY*" $output])} {
test_passed "Requires API key" test_passed "Requires authentication"
} else { } else {
test_failed "Requires API key" "Expected API key error message" test_failed "Requires authentication" "Expected auth error message"
} }
} else { } else {
test_skipped "Requires API key (test file not found)" test_skipped "Requires authentication (test file not found)"
} }
} else { } else {
test_skipped "Requires API key (API key already not set)" test_skipped "Requires authentication (auth already not set)"
} }
# Integration Tests (require API key) # Integration Tests (require authentication and packages)
if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} { if {!$has_required_packages} {
puts "\n[color_yellow "Skipping integration tests (missing TCL packages)"]"
} elseif {$has_hmac || $has_legacy} {
puts "\n[color_blue "=== Integration Tests for un.tcl ==="]" puts "\n[color_blue "=== Integration Tests for un.tcl ==="]"
# Test: Can execute Python file # Test: Can execute Python file
@ -159,7 +196,7 @@ if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
test_skipped "Executes Bash file successfully (fib.sh not found)" test_skipped "Executes Bash file successfully (fib.sh not found)"
} }
} else { } else {
puts "\n[color_yellow "Skipping integration tests (UNSANDBOX_API_KEY not set)"]" puts "\n[color_yellow "Skipping integration tests (UNSANDBOX authentication not configured)"]"
} }
# Summary # Summary

View file

@ -11,12 +11,16 @@ import * as path from 'path';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import * as https from 'https'; import * as https from 'https';
import * as crypto from 'crypto';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
// Get script directory - works in both CommonJS and ES modules
const SCRIPT_DIR = path.dirname(process.argv[1] || __filename);
// Test configuration // Test configuration
const UN_SCRIPT = path.join(__dirname, '..', 'un.ts'); const UN_SCRIPT = path.join(SCRIPT_DIR, '..', 'un.ts');
const FIB_PY = path.join(__dirname, '..', '..', 'test', 'fib.py'); const FIB_PY = path.join(SCRIPT_DIR, '..', '..', 'test', 'fib.py');
class TestResults { class TestResults {
passed: number = 0; passed: number = 0;
@ -140,26 +144,37 @@ async function runTests(): Promise<void> {
results.failTest('Extension detection: .unknown -> undefined', (e as Error).message); results.failTest('Extension detection: .unknown -> undefined', (e as Error).message);
} }
// Test 7: API call test (requires UNSANDBOX_API_KEY) // Test 7: API call test (requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY)
if (!process.env.UNSANDBOX_API_KEY) { const hasHmac = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set'); const hasLegacy = process.env.UNSANDBOX_API_KEY;
if (!hasHmac && !hasLegacy) {
results.skipTest('API call test', 'UNSANDBOX authentication not set');
} else { } else {
try { try {
const apiKey = process.env.UNSANDBOX_API_KEY; const publicKey = process.env.UNSANDBOX_PUBLIC_KEY || process.env.UNSANDBOX_API_KEY || '';
const secretKey = process.env.UNSANDBOX_SECRET_KEY || '';
const payload = JSON.stringify({ const payload = JSON.stringify({
language: 'python', language: 'python',
code: 'print("Hello from API")' code: 'print("Hello from API")'
}); });
const timestamp = Math.floor(Date.now() / 1000).toString();
const method = 'POST';
const apiPath = '/execute';
const signatureData = `${timestamp}:${method}:${apiPath}:${payload}`;
const signature = crypto.createHmac('sha256', secretKey).update(signatureData).digest('hex');
const result: ExecuteResult = await new Promise((resolve, reject) => { const result: ExecuteResult = await new Promise((resolve, reject) => {
const options: https.RequestOptions = { const options: https.RequestOptions = {
hostname: 'api.unsandbox.com', hostname: 'api.unsandbox.com',
path: '/execute', path: apiPath,
method: 'POST', method: method,
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${publicKey}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload) 'Content-Length': Buffer.byteLength(payload),
'X-Timestamp': timestamp,
'X-Signature': signature
} }
}; };
@ -191,8 +206,8 @@ async function runTests(): Promise<void> {
} }
// Test 8: End-to-end test with fib.py // Test 8: End-to-end test with fib.py
if (!process.env.UNSANDBOX_API_KEY) { if (!hasHmac && !hasLegacy) {
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set'); results.skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not set');
} else if (!fs.existsSync(FIB_PY)) { } else if (!fs.existsSync(FIB_PY)) {
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`); results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
} else { } else {

174
un.awk
View file

@ -60,15 +60,32 @@ BEGIN {
RESET = "\033[0m" RESET = "\033[0m"
} }
function get_api_key() { function get_api_keys( public_key, secret_key, cmd) {
cmd = "echo $UNSANDBOX_API_KEY" # Get public key
cmd | getline api_key cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"
cmd | getline public_key
close(cmd) close(cmd)
if (api_key == "") {
print RED "Error: UNSANDBOX_API_KEY not set" RESET > "/dev/stderr" # Get secret key
cmd = "echo -n $UNSANDBOX_SECRET_KEY"
cmd | getline secret_key
close(cmd)
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if (public_key == "") {
cmd = "echo -n $UNSANDBOX_API_KEY"
cmd | getline public_key
close(cmd)
secret_key = ""
}
if (public_key == "") {
print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr"
exit 1 exit 1
} }
return api_key
GLOBAL_PUBLIC_KEY = public_key
GLOBAL_SECRET_KEY = secret_key
} }
function get_extension(filename) { function get_extension(filename) {
@ -88,8 +105,9 @@ function escape_json(s) {
return s return s
} }
function execute(filename) { function execute(filename , api_key) {
api_key = get_api_key() get_api_keys()
api_key = GLOBAL_PUBLIC_KEY
# Get extension and language # Get extension and language
ext = get_extension(filename) ext = get_extension(filename)
@ -119,10 +137,27 @@ function execute(filename) {
print json > tmp print json > tmp
close(tmp) close(tmp)
# Build HMAC signature if secret key exists
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
# HMAC signature: timestamp:METHOD:path:body
sig_input = timestamp ":POST:/execute:" json
sig_tmp = "/tmp/un_awk_sig_" PROCINFO["pid"]
print sig_input > sig_tmp
close(sig_tmp)
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
system("rm -f " sig_tmp)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
}
# Call curl # Call curl
cmd = "curl -s -X POST '" API_BASE "/execute' " \ cmd = "curl -s -X POST '" API_BASE "/execute' " \
"-H 'Content-Type: application/json' " \ "-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \ "-H 'Authorization: Bearer " api_key "' " \
sig_headers \
"-d '@" tmp "'" "-d '@" tmp "'"
response = "" response = ""
@ -160,43 +195,94 @@ function execute(filename) {
} }
} }
function session_list() { function session_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
api_key = get_api_key() get_api_keys()
cmd = "curl -s '" API_BASE "/sessions' -H 'Authorization: Bearer " api_key "'" timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":GET:/sessions:"
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
}
cmd = "curl -s '" API_BASE "/sessions' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
while ((cmd | getline line) > 0) print line while ((cmd | getline line) > 0) print line
close(cmd) close(cmd)
} }
function session_kill(id) { function session_kill(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
api_key = get_api_key() get_api_keys()
cmd = "curl -s -X DELETE '" API_BASE "/sessions/" id "' -H 'Authorization: Bearer " api_key "'" endpoint = "/sessions/" id
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":DELETE:" endpoint ":"
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
}
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
system(cmd) system(cmd)
print GREEN "Session terminated: " id RESET print GREEN "Session terminated: " id RESET
} }
function service_list() { function service_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
api_key = get_api_key() get_api_keys()
cmd = "curl -s '" API_BASE "/services' -H 'Authorization: Bearer " api_key "'" timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":GET:/services:"
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
}
cmd = "curl -s '" API_BASE "/services' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
while ((cmd | getline line) > 0) print line while ((cmd | getline line) > 0) print line
close(cmd) close(cmd)
} }
function service_destroy(id) { function service_destroy(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
api_key = get_api_key() get_api_keys()
cmd = "curl -s -X DELETE '" API_BASE "/services/" id "' -H 'Authorization: Bearer " api_key "'" endpoint = "/services/" id
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":DELETE:" endpoint ":"
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
}
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
system(cmd) system(cmd)
print GREEN "Service destroyed: " id RESET print GREEN "Service destroyed: " id RESET
} }
function service_dump_bootstrap(id, dump_file) { function service_dump_bootstrap(id, dump_file , endpoint, json_body, timestamp, sig_headers, signature, sig_input, sig_cmd) {
api_key = get_api_key() get_api_keys()
print "Fetching bootstrap script from " id "..." > "/dev/stderr" print "Fetching bootstrap script from " id "..." > "/dev/stderr"
endpoint = "/services/" id "/execute"
json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":POST:" endpoint ":" json_body
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
}
# Build the curl command to execute on the service # Build the curl command to execute on the service
cmd = "curl -s -X POST '" API_BASE "/services/" id "/execute' " \ cmd = "curl -s -X POST '" API_BASE endpoint "' " \
"-H 'Content-Type: application/json' " \ "-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \ "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'" sig_headers \
"-d '" json_body "'"
response = "" response = ""
while ((cmd | getline line) > 0) { while ((cmd | getline line) > 0) {
@ -229,8 +315,8 @@ function service_dump_bootstrap(id, dump_file) {
} }
} }
function service_create(name, ports, domains, service_type, bootstrap) { function service_create(name, ports, domains, service_type, bootstrap , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) {
api_key = get_api_key() get_api_keys()
# Build JSON payload # Build JSON payload
json = "{\"name\":\"" escape_json(name) "\"" json = "{\"name\":\"" escape_json(name) "\""
@ -265,10 +351,22 @@ function service_create(name, ports, domains, service_type, bootstrap) {
print json > tmp print json > tmp
close(tmp) close(tmp)
# Build HMAC signature if secret key exists
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":POST:/services:" json
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
}
# Call curl # Call curl
cmd = "curl -s -X POST '" API_BASE "/services' " \ cmd = "curl -s -X POST '" API_BASE "/services' " \
"-H 'Content-Type: application/json' " \ "-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \ "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers \
"-d '@" tmp "'" "-d '@" tmp "'"
response = "" response = ""
@ -284,11 +382,24 @@ function service_create(name, ports, domains, service_type, bootstrap) {
print response print response
} }
function validate_key(api_key, do_extend) { function validate_key(do_extend , timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":POST:/keys/validate:"
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
sig_cmd | getline signature
close(sig_cmd)
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
}
# Call curl to validate key # Call curl to validate key
cmd = "curl -s -X POST '" PORTAL_BASE "/keys/validate' " \ cmd = "curl -s -X POST '" PORTAL_BASE "/keys/validate' " \
"-H 'Content-Type: application/json' " \ "-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "'" "-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers
response = "" response = ""
while ((cmd | getline line) > 0) { while ((cmd | getline line) > 0) {
@ -366,8 +477,7 @@ function validate_key(api_key, do_extend) {
} }
function cmd_key(do_extend) { function cmd_key(do_extend) {
api_key = get_api_key() validate_key(do_extend)
validate_key(api_key, do_extend)
} }
function show_help() { function show_help() {

85
un.clj
View file

@ -101,41 +101,82 @@
(or (second (re-find pattern-str json-str)) (or (second (re-find pattern-str json-str))
(second (re-find pattern-num json-str))))) (second (re-find pattern-num json-str)))))
(defn get-api-keys []
(let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY")
secret-key (System/getenv "UNSANDBOX_SECRET_KEY")
api-key (System/getenv "UNSANDBOX_API_KEY")]
(cond
(and public-key secret-key) [public-key secret-key]
api-key [api-key nil]
:else (do
(binding [*out* *err*]
(println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"))
(System/exit 1)))))
(defn get-api-key [] (defn get-api-key []
(or (System/getenv "UNSANDBOX_API_KEY") (first (get-api-keys)))
(do (binding [*out* *err*]
(println "Error: UNSANDBOX_API_KEY not set")) (defn hmac-sha256 [secret message]
(System/exit 1)))) (let [mac (javax.crypto.Mac/getInstance "HmacSHA256")
secret-key (javax.crypto.spec.SecretKeySpec. (.getBytes secret "UTF-8") "HmacSHA256")]
(.init mac secret-key)
(let [bytes (.doFinal mac (.getBytes message "UTF-8"))]
(apply str (map #(format "%02x" %) bytes)))))
(defn make-signature [secret-key timestamp method path body]
(let [message (str timestamp ":" method ":" path ":" body)]
(hmac-sha256 secret-key message)))
(defn build-auth-headers [public-key secret-key method path body]
(if secret-key
(let [timestamp (str (quot (System/currentTimeMillis) 1000))
signature (make-signature secret-key timestamp method path body)]
["-H" (str "Authorization: Bearer " public-key)
"-H" (str "X-Timestamp: " timestamp)
"-H" (str "X-Signature: " signature)])
["-H" (str "Authorization: Bearer " public-key)]))
(defn curl-post [api-key endpoint json-data] (defn curl-post [api-key endpoint json-data]
(let [tmp-file (str "/tmp/un_clj_" (rand-int 999999) ".json")] (let [tmp-file (str "/tmp/un_clj_" (rand-int 999999) ".json")
[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)]
(spit tmp-file json-data) (spit tmp-file json-data)
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST" (let [args (concat ["curl" "-s" "-X" "POST"
(str "https://api.unsandbox.com" endpoint) (str "https://api.unsandbox.com" endpoint)
"-H" "Content-Type: application/json" "-H" "Content-Type: application/json"]
"-H" (str "Authorization: Bearer " api-key) auth-headers
"-d" (str "@" tmp-file))] ["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true) (io/delete-file tmp-file true)
out))) out)))
(defn curl-get [api-key endpoint] (defn curl-get [api-key endpoint]
(:out (sh "curl" "-s" (let [[public-key secret-key] (get-api-keys)
(str "https://api.unsandbox.com" endpoint) auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")
"-H" (str "Authorization: Bearer " api-key)))) args (concat ["curl" "-s"
(str "https://api.unsandbox.com" endpoint)]
auth-headers)]
(:out (apply sh args))))
(defn curl-delete [api-key endpoint] (defn curl-delete [api-key endpoint]
(:out (sh "curl" "-s" "-X" "DELETE" (let [[public-key secret-key] (get-api-keys)
(str "https://api.unsandbox.com" endpoint) auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")
"-H" (str "Authorization: Bearer " api-key)))) args (concat ["curl" "-s" "-X" "DELETE"
(str "https://api.unsandbox.com" endpoint)]
auth-headers)]
(:out (apply sh args))))
(defn curl-portal-post [api-key endpoint json-data] (defn curl-portal-post [api-key endpoint json-data]
(let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")] (let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")
[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)]
(spit tmp-file json-data) (spit tmp-file json-data)
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST" (let [args (concat ["curl" "-s" "-X" "POST"
(str portal-base endpoint) (str portal-base endpoint)
"-H" "Content-Type: application/json" "-H" "Content-Type: application/json"]
"-H" (str "Authorization: Bearer " api-key) auth-headers
"-d" (str "@" tmp-file))] ["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true) (io/delete-file tmp-file true)
out))) out)))

44
un.cob
View file

@ -55,6 +55,8 @@
01 WS-FILENAME PIC X(256). 01 WS-FILENAME PIC X(256).
01 WS-FILE-STATUS PIC XX. 01 WS-FILE-STATUS PIC XX.
01 WS-API-KEY PIC X(256). 01 WS-API-KEY PIC X(256).
01 WS-PUBLIC-KEY PIC X(256).
01 WS-SECRET-KEY PIC X(256).
01 WS-LANGUAGE PIC X(32). 01 WS-LANGUAGE PIC X(32).
01 WS-EXTENSION PIC X(16). 01 WS-EXTENSION PIC X(16).
01 WS-CURL-CMD PIC X(4096). 01 WS-CURL-CMD PIC X(4096).
@ -259,16 +261,46 @@
END-IF. END-IF.
MAKE-EXECUTE-REQUEST. MAKE-EXECUTE-REQUEST.
* Build curl command using shell * Get public/secret keys with fallback
STRING "curl -s -X POST https://api.unsandbox.com/execute " ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
"-H 'Content-Type: application/json' " IF WS-PUBLIC-KEY NOT = SPACES
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY) ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
"' --data-binary @- -o /tmp/unsandbox_resp.json " IF WS-SECRET-KEY = SPACES
"< <(jq -Rs '{language: """ DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
ELSE
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
IF WS-PUBLIC-KEY = SPACES
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
"UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
MOVE WS-PUBLIC-KEY TO WS-SECRET-KEY
END-IF.
* Build curl command using shell with HMAC signature
STRING "TS=$(date +%s); "
"BODY=$(jq -Rs '{language: """
FUNCTION TRIM(WS-LANGUAGE) FUNCTION TRIM(WS-LANGUAGE)
""", code: .}' < '" """, code: .}' < '"
FUNCTION TRIM(WS-FILENAME) FUNCTION TRIM(WS-FILENAME)
"'); " "'); "
"SIG=$(echo -n \"$TS:POST:/execute:$BODY\" | "
"openssl dgst -sha256 -hmac '"
FUNCTION TRIM(WS-SECRET-KEY)
"' | cut -d' ' -f2); "
"curl -s -X POST https://api.unsandbox.com/execute "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer "
FUNCTION TRIM(WS-PUBLIC-KEY)
"' "
"-H 'X-Timestamp: '$TS "
"-H 'X-Signature: '$SIG "
"--data-binary \"$BODY\" -o /tmp/unsandbox_resp.json; "
"jq -r '.stdout // empty' /tmp/unsandbox_resp.json | " "jq -r '.stdout // empty' /tmp/unsandbox_resp.json | "
"sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; " "sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; "
"jq -r '.stderr // empty' /tmp/unsandbox_resp.json | " "jq -r '.stderr // empty' /tmp/unsandbox_resp.json | "

111
un.cpp
View file

@ -50,6 +50,7 @@
#include <vector> #include <vector>
#include <map> #include <map>
#include <cstdlib> #include <cstdlib>
#include <ctime>
#include <sys/stat.h> #include <sys/stat.h>
using namespace std; using namespace std;
@ -111,7 +112,37 @@ string exec_curl(const string& cmd) {
return result; return result;
} }
void cmd_execute(const string& source_file, const vector<string>& envs, const vector<string>& files, bool artifacts, const string& network, int vcpu, const string& api_key) { string compute_hmac(const string& secret_key, const string& message) {
string cmd = "echo -n '" + message + "' | openssl dgst -sha256 -hmac '" + secret_key + "' -hex | sed 's/.*= //'";
string result = exec_curl(cmd);
// Trim newline
while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) {
result.pop_back();
}
return result;
}
string get_timestamp() {
return to_string(time(nullptr));
}
string build_auth_headers(const string& method, const string& path, const string& body, const string& public_key, const string& secret_key) {
if (secret_key.empty()) {
// Legacy mode: use public_key as bearer token
return "-H 'Authorization: Bearer " + public_key + "'";
}
// HMAC mode
string timestamp = get_timestamp();
string message = timestamp + ":" + method + ":" + path + ":" + body;
string signature = compute_hmac(secret_key, message);
return "-H 'Authorization: Bearer " + public_key + "' "
"-H 'X-Timestamp: " + timestamp + "' "
"-H 'X-Signature: " + signature + "'";
}
void cmd_execute(const string& source_file, const vector<string>& envs, const vector<string>& files, bool artifacts, const string& network, int vcpu, const string& public_key, const string& secret_key) {
string lang = detect_language(source_file); string lang = detect_language(source_file);
if (lang.empty()) { if (lang.empty()) {
cerr << RED << "Error: Cannot detect language" << RESET << endl; cerr << RED << "Error: Cannot detect language" << RESET << endl;
@ -140,9 +171,10 @@ void cmd_execute(const string& source_file, const vector<string>& envs, const ve
if (vcpu > 0) json << ",\"vcpu\":" << vcpu; if (vcpu > 0) json << ",\"vcpu\":" << vcpu;
json << "}"; json << "}";
string auth_headers = build_auth_headers("POST", "/execute", json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/execute' " string cmd = "curl -s -X POST '" + API_BASE + "/execute' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' " + auth_headers + " "
"-d '" + json.str() + "'"; "-d '" + json.str() + "'";
string result = exec_curl(cmd); string result = exec_curl(cmd);
@ -188,15 +220,17 @@ void cmd_execute(const string& source_file, const vector<string>& envs, const ve
exit(exit_code); exit(exit_code);
} }
void cmd_session(bool list, const string& kill, const string& shell, const string& network, int vcpu, bool tmux, bool screen, const string& api_key) { void cmd_session(bool list, const string& kill, const string& shell, const string& network, int vcpu, bool tmux, bool screen, const string& public_key, const string& secret_key) {
if (list) { if (list) {
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("GET", "/sessions", "", public_key, secret_key);
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' " + auth_headers;
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
return; return;
} }
if (!kill.empty()) { if (!kill.empty()) {
string cmd = "curl -s -X DELETE '" + API_BASE + "/sessions/" + kill + "' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("DELETE", "/sessions/" + kill, "", public_key, secret_key);
string cmd = "curl -s -X DELETE '" + API_BASE + "/sessions/" + kill + "' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
cout << GREEN << "Session terminated: " << kill << RESET << endl; cout << GREEN << "Session terminated: " << kill << RESET << endl;
return; return;
@ -211,54 +245,62 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
json << "}"; json << "}";
cout << YELLOW << "Creating session..." << RESET << endl; cout << YELLOW << "Creating session..." << RESET << endl;
string auth_headers = build_auth_headers("POST", "/sessions", json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/sessions' " string cmd = "curl -s -X POST '" + API_BASE + "/sessions' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' " + auth_headers + " "
"-d '" + json.str() + "'"; "-d '" + json.str() + "'";
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
} }
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const string& api_key) { void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const string& public_key, const string& secret_key) {
if (list) { if (list) {
string cmd = "curl -s -X GET '" + API_BASE + "/services' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("GET", "/services", "", public_key, secret_key);
string cmd = "curl -s -X GET '" + API_BASE + "/services' " + auth_headers;
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
return; return;
} }
if (!info.empty()) { if (!info.empty()) {
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + info + "' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("GET", "/services/" + info, "", public_key, secret_key);
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + info + "' " + auth_headers;
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
return; return;
} }
if (!logs.empty()) { if (!logs.empty()) {
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + logs + "/logs' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("GET", "/services/" + logs + "/logs", "", public_key, secret_key);
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + logs + "/logs' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
return; return;
} }
if (!tail.empty()) { if (!tail.empty()) {
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + tail + "/logs?lines=9000' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("GET", "/services/" + tail + "/logs?lines=9000", "", public_key, secret_key);
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + tail + "/logs?lines=9000' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
return; return;
} }
if (!sleep.empty()) { if (!sleep.empty()) {
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + sleep + "/sleep' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("POST", "/services/" + sleep + "/sleep", "", public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + sleep + "/sleep' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
cout << GREEN << "Service sleeping: " << sleep << RESET << endl; cout << GREEN << "Service sleeping: " << sleep << RESET << endl;
return; return;
} }
if (!wake.empty()) { if (!wake.empty()) {
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + wake + "/wake' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("POST", "/services/" + wake + "/wake", "", public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + wake + "/wake' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
cout << GREEN << "Service waking: " << wake << RESET << endl; cout << GREEN << "Service waking: " << wake << RESET << endl;
return; return;
} }
if (!destroy.empty()) { if (!destroy.empty()) {
string cmd = "curl -s -X DELETE '" + API_BASE + "/services/" + destroy + "' -H 'Authorization: Bearer " + api_key + "'"; string auth_headers = build_auth_headers("DELETE", "/services/" + destroy, "", public_key, secret_key);
string cmd = "curl -s -X DELETE '" + API_BASE + "/services/" + destroy + "' " + auth_headers;
exec_curl(cmd); exec_curl(cmd);
cout << GREEN << "Service destroyed: " << destroy << RESET << endl; cout << GREEN << "Service destroyed: " << destroy << RESET << endl;
return; return;
@ -267,9 +309,10 @@ void cmd_service(const string& name, const string& ports, const string& type, co
if (!execute.empty()) { if (!execute.empty()) {
ostringstream json; ostringstream json;
json << "{\"command\":\"" << escape_json(command) << "\"}"; json << "{\"command\":\"" << escape_json(command) << "\"}";
string auth_headers = build_auth_headers("POST", "/services/" + execute + "/execute", json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + execute + "/execute' " string cmd = "curl -s -X POST '" + API_BASE + "/services/" + execute + "/execute' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' " + auth_headers + " "
"-d '" + json.str() + "'"; "-d '" + json.str() + "'";
string result = exec_curl(cmd); string result = exec_curl(cmd);
@ -308,10 +351,12 @@ void cmd_service(const string& name, const string& ports, const string& type, co
if (!dump_bootstrap.empty()) { if (!dump_bootstrap.empty()) {
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl; cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
string auth_headers = build_auth_headers("POST", "/services/" + dump_bootstrap + "/execute", json_body, public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + dump_bootstrap + "/execute' " string cmd = "curl -s -X POST '" + API_BASE + "/services/" + dump_bootstrap + "/execute' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' " + auth_headers + " "
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"; "-d '" + json_body + "'";
string result = exec_curl(cmd); string result = exec_curl(cmd);
size_t stdout_pos = result.find("\"stdout\":\""); size_t stdout_pos = result.find("\"stdout\":\"");
@ -370,9 +415,10 @@ void cmd_service(const string& name, const string& ports, const string& type, co
json << "}"; json << "}";
cout << YELLOW << "Creating service..." << RESET << endl; cout << YELLOW << "Creating service..." << RESET << endl;
string auth_headers = build_auth_headers("POST", "/services", json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/services' " string cmd = "curl -s -X POST '" + API_BASE + "/services' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' " + auth_headers + " "
"-d '" + json.str() + "'"; "-d '" + json.str() + "'";
cout << exec_curl(cmd) << endl; cout << exec_curl(cmd) << endl;
return; return;
@ -382,10 +428,11 @@ void cmd_service(const string& name, const string& ports, const string& type, co
exit(1); exit(1);
} }
void cmd_validate_key(bool extend, const string& api_key) { void cmd_validate_key(bool extend, const string& public_key, const string& secret_key) {
string auth_headers = build_auth_headers("POST", "/keys/validate", "", public_key, secret_key);
string cmd = "curl -s -X POST '" + PORTAL_BASE + "/keys/validate' " string cmd = "curl -s -X POST '" + PORTAL_BASE + "/keys/validate' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "'"; + auth_headers;
string result = exec_curl(cmd); string result = exec_curl(cmd);
@ -464,7 +511,13 @@ void cmd_validate_key(bool extend, const string& api_key) {
} }
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
string api_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : ""; string public_key = getenv("UNSANDBOX_PUBLIC_KEY") ? getenv("UNSANDBOX_PUBLIC_KEY") : "";
string secret_key = getenv("UNSANDBOX_SECRET_KEY") ? getenv("UNSANDBOX_SECRET_KEY") : "";
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (public_key.empty()) {
public_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : "";
}
if (argc < 2) { if (argc < 2) {
cerr << "Usage: " << argv[0] << " [options] <source_file>" << endl; cerr << "Usage: " << argv[0] << " [options] <source_file>" << endl;
@ -491,10 +544,10 @@ int main(int argc, char* argv[]) {
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]); else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
else if (arg == "--tmux") tmux = true; else if (arg == "--tmux") tmux = true;
else if (arg == "--screen") screen = true; else if (arg == "--screen") screen = true;
else if (arg == "-k" && i+1 < argc) api_key = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
} }
cmd_session(list, kill, shell, network, vcpu, tmux, screen, api_key); cmd_session(list, kill, shell, network, vcpu, tmux, screen, public_key, secret_key);
return 0; return 0;
} }
@ -523,10 +576,10 @@ int main(int argc, char* argv[]) {
else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i]; else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i];
else if (arg == "-n" && i+1 < argc) network = argv[++i]; else if (arg == "-n" && i+1 < argc) network = argv[++i];
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]); else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
else if (arg == "-k" && i+1 < argc) api_key = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
} }
cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, api_key); cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, public_key, secret_key);
return 0; return 0;
} }
@ -536,10 +589,10 @@ int main(int argc, char* argv[]) {
for (int i = 2; i < argc; i++) { for (int i = 2; i < argc; i++) {
string arg = argv[i]; string arg = argv[i];
if (arg == "--extend") extend = true; if (arg == "--extend") extend = true;
else if (arg == "-k" && i+1 < argc) api_key = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
} }
cmd_validate_key(extend, api_key); cmd_validate_key(extend, public_key, secret_key);
return 0; return 0;
} }
@ -556,7 +609,7 @@ int main(int argc, char* argv[]) {
else if (arg == "-a") artifacts = true; else if (arg == "-a") artifacts = true;
else if (arg == "-n" && i+1 < argc) network = argv[++i]; else if (arg == "-n" && i+1 < argc) network = argv[++i];
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]); else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
else if (arg == "-k" && i+1 < argc) api_key = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
else if (arg[0] != '-') source_file = arg; else if (arg[0] != '-') source_file = arg;
} }
@ -565,6 +618,6 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
cmd_execute(source_file, envs, files, artifacts, network, vcpu, api_key); cmd_execute(source_file, envs, files, artifacts, network, vcpu, public_key, secret_key);
return 0; return 0;
} }

98
un.cr
View file

@ -41,6 +41,7 @@ require "http/client"
require "json" require "json"
require "base64" require "base64"
require "option_parser" require "option_parser"
require "openssl/hmac"
# Extension to language mapping # Extension to language mapping
EXT_MAP = { EXT_MAP = {
@ -74,28 +75,51 @@ def detect_language(filename : String) : String
EXT_MAP.fetch(ext, "unknown") EXT_MAP.fetch(ext, "unknown")
end end
def get_api_key(args_key : String?) : String def get_api_keys(args_key : String?) : {String, String?}
key = args_key || ENV["UNSANDBOX_API_KEY"]? public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
if key.nil? || key.empty? secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
STDERR.puts "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
exit 1 # Fall back to UNSANDBOX_API_KEY for backwards compatibility
if public_key.nil? || public_key.empty? || secret_key.nil? || secret_key.empty?
legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]?
if legacy_key.nil? || legacy_key.empty?
STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
exit 1
end
return {legacy_key, nil}
end end
key
{public_key, secret_key}
end end
def api_request(endpoint : String, api_key : String, method = "GET", data : JSON::Any? = nil) def api_request(endpoint : String, public_key : String, secret_key : String?, method = "GET", data : JSON::Any? = nil)
url = URI.parse(API_BASE + endpoint) url = URI.parse(API_BASE + endpoint)
headers = HTTP::Headers{ headers = HTTP::Headers{
"Content-Type" => "application/json", "Content-Type" => "application/json"
"Authorization" => "Bearer #{api_key}"
} }
body = data ? data.to_json : ""
# Add HMAC authentication headers if secret_key is provided
if secret_key && !secret_key.empty?
timestamp = Time.utc.to_unix.to_s
message = "#{timestamp}:#{method}:#{endpoint}:#{body}"
signature = OpenSSL::HMAC.hexdigest(:sha256, secret_key, message)
headers["Authorization"] = "Bearer #{public_key}"
headers["X-Timestamp"] = timestamp
headers["X-Signature"] = signature
else
# Legacy API key authentication
headers["Authorization"] = "Bearer #{public_key}"
end
begin begin
response = case method response = case method
when "GET" when "GET"
HTTP::Client.get(url, headers: headers) HTTP::Client.get(url, headers: headers)
when "POST" when "POST"
body = data ? data.to_json : ""
HTTP::Client.post(url, headers: headers, body: body) HTTP::Client.post(url, headers: headers, body: body)
when "DELETE" when "DELETE"
HTTP::Client.delete(url, headers: headers) HTTP::Client.delete(url, headers: headers)
@ -112,7 +136,7 @@ def api_request(endpoint : String, api_key : String, method = "GET", data : JSON
end end
def cmd_execute(args) def cmd_execute(args)
api_key = get_api_key(args[:api_key]?) public_key, secret_key = get_api_keys(args[:api_key]?)
filename = args[:source_file].as(String) filename = args[:source_file].as(String)
unless File.exists?(filename) unless File.exists?(filename)
@ -173,7 +197,7 @@ def cmd_execute(args)
end end
# Execute # Execute
result = api_request("/execute", api_key, method: "POST", data: payload) result = api_request("/execute", public_key, secret_key, method: "POST", data: payload)
# Print output # Print output
if stdout = result["stdout"]?.try(&.as_s?) if stdout = result["stdout"]?.try(&.as_s?)
@ -202,10 +226,10 @@ def cmd_execute(args)
end end
def cmd_session(args) def cmd_session(args)
api_key = get_api_key(args[:api_key]?) public_key, secret_key = get_api_keys(args[:api_key]?)
if args[:list]?.as?(Bool) if args[:list]?.as?(Bool)
result = api_request("/sessions", api_key) result = api_request("/sessions", public_key, secret_key)
sessions = result["sessions"]?.try(&.as_a?) || [] of JSON::Any sessions = result["sessions"]?.try(&.as_a?) || [] of JSON::Any
if sessions.empty? if sessions.empty?
puts "No active sessions" puts "No active sessions"
@ -223,7 +247,7 @@ def cmd_session(args)
end end
if kill_id = args[:kill]?.as?(String) if kill_id = args[:kill]?.as?(String)
api_request("/sessions/#{kill_id}", api_key, method: "DELETE") api_request("/sessions/#{kill_id}", public_key, secret_key, method: "DELETE")
puts "#{GREEN}Session terminated: #{kill_id}#{RESET}" puts "#{GREEN}Session terminated: #{kill_id}#{RESET}"
return return
end end
@ -233,17 +257,33 @@ def cmd_session(args)
end end
def cmd_key(args) def cmd_key(args)
api_key = get_api_key(args[:api_key]?) public_key, secret_key = get_api_keys(args[:api_key]?)
# Validate key # Validate key
url = URI.parse(PORTAL_BASE + "/keys/validate") url = URI.parse(PORTAL_BASE + "/keys/validate")
headers = HTTP::Headers{ headers = HTTP::Headers{
"Content-Type" => "application/json", "Content-Type" => "application/json"
"Authorization" => "Bearer #{api_key}"
} }
body = "{}"
# Add HMAC authentication headers if secret_key is provided
if secret_key && !secret_key.empty?
timestamp = Time.utc.to_unix.to_s
message = "#{timestamp}:POST:/keys/validate:#{body}"
signature = OpenSSL::HMAC.hexdigest(:sha256, secret_key, message)
headers["Authorization"] = "Bearer #{public_key}"
headers["X-Timestamp"] = timestamp
headers["X-Signature"] = signature
else
# Legacy API key authentication
headers["Authorization"] = "Bearer #{public_key}"
end
begin begin
response = HTTP::Client.post(url, headers: headers, body: "{}") response = HTTP::Client.post(url, headers: headers, body: body)
result = JSON.parse(response.body) result = JSON.parse(response.body)
status = result["status"]?.try(&.as_s?) || "unknown" status = result["status"]?.try(&.as_s?) || "unknown"
@ -311,10 +351,10 @@ def cmd_key(args)
end end
def cmd_service(args) def cmd_service(args)
api_key = get_api_key(args[:api_key]?) public_key, secret_key = get_api_keys(args[:api_key]?)
if args[:list]?.as?(Bool) if args[:list]?.as?(Bool)
result = api_request("/services", api_key) result = api_request("/services", public_key, secret_key)
services = result["services"]?.try(&.as_a?) || [] of JSON::Any services = result["services"]?.try(&.as_a?) || [] of JSON::Any
if services.empty? if services.empty?
puts "No services" puts "No services"
@ -334,31 +374,31 @@ def cmd_service(args)
end end
if info_id = args[:info]?.as?(String) if info_id = args[:info]?.as?(String)
result = api_request("/services/#{info_id}", api_key) result = api_request("/services/#{info_id}", public_key, secret_key)
puts result.to_pretty_json puts result.to_pretty_json
return return
end end
if logs_id = args[:logs]?.as?(String) if logs_id = args[:logs]?.as?(String)
result = api_request("/services/#{logs_id}/logs", api_key) result = api_request("/services/#{logs_id}/logs", public_key, secret_key)
puts result["logs"]?.try(&.as_s?) || "" puts result["logs"]?.try(&.as_s?) || ""
return return
end end
if sleep_id = args[:sleep]?.as?(String) if sleep_id = args[:sleep]?.as?(String)
api_request("/services/#{sleep_id}/sleep", api_key, method: "POST") api_request("/services/#{sleep_id}/sleep", public_key, secret_key, method: "POST")
puts "#{GREEN}Service sleeping: #{sleep_id}#{RESET}" puts "#{GREEN}Service sleeping: #{sleep_id}#{RESET}"
return return
end end
if wake_id = args[:wake]?.as?(String) if wake_id = args[:wake]?.as?(String)
api_request("/services/#{wake_id}/wake", api_key, method: "POST") api_request("/services/#{wake_id}/wake", public_key, secret_key, method: "POST")
puts "#{GREEN}Service waking: #{wake_id}#{RESET}" puts "#{GREEN}Service waking: #{wake_id}#{RESET}"
return return
end end
if destroy_id = args[:destroy]?.as?(String) if destroy_id = args[:destroy]?.as?(String)
api_request("/services/#{destroy_id}", api_key, method: "DELETE") api_request("/services/#{destroy_id}", public_key, secret_key, method: "DELETE")
puts "#{GREEN}Service destroyed: #{destroy_id}#{RESET}" puts "#{GREEN}Service destroyed: #{destroy_id}#{RESET}"
return return
end end
@ -366,7 +406,7 @@ def cmd_service(args)
if execute_id = args[:execute]?.as?(String) if execute_id = args[:execute]?.as?(String)
command = args[:command]?.as?(String) || "" command = args[:command]?.as?(String) || ""
payload = JSON.parse({command: command}.to_json) payload = JSON.parse({command: command}.to_json)
result = api_request("/services/#{execute_id}/execute", api_key, method: "POST", data: payload) result = api_request("/services/#{execute_id}/execute", public_key, secret_key, method: "POST", data: payload)
if stdout = result["stdout"]?.try(&.as_s?) if stdout = result["stdout"]?.try(&.as_s?)
print BLUE, stdout, RESET print BLUE, stdout, RESET
end end
@ -379,7 +419,7 @@ def cmd_service(args)
if dump_id = args[:dump_bootstrap]?.as?(String) if dump_id = args[:dump_bootstrap]?.as?(String)
STDERR.puts "Fetching bootstrap script from #{dump_id}..." STDERR.puts "Fetching bootstrap script from #{dump_id}..."
payload = JSON.parse({command: "cat /tmp/bootstrap.sh"}.to_json) payload = JSON.parse({command: "cat /tmp/bootstrap.sh"}.to_json)
result = api_request("/services/#{dump_id}/execute", api_key, method: "POST", data: payload) result = api_request("/services/#{dump_id}/execute", public_key, secret_key, method: "POST", data: payload)
if bootstrap = result["stdout"]?.try(&.as_s?) if bootstrap = result["stdout"]?.try(&.as_s?)
if file_path = args[:dump_file]?.as?(String) if file_path = args[:dump_file]?.as?(String)
@ -433,7 +473,7 @@ def cmd_service(args)
end end
# Create service # Create service
result = api_request("/services", api_key, method: "POST", data: payload) result = api_request("/services", public_key, secret_key, method: "POST", data: payload)
puts "#{GREEN}Service created: #{result["id"]?.try(&.as_s?) || "N/A"}#{RESET}" puts "#{GREEN}Service created: #{result["id"]?.try(&.as_s?) || "N/A"}#{RESET}"
puts "Name: #{result["name"]?.try(&.as_s?) || "N/A"}" puts "Name: #{result["name"]?.try(&.as_s?) || "N/A"}"
if url = result["url"]?.try(&.as_s?) if url = result["url"]?.try(&.as_s?)

122
un.d
View file

@ -88,12 +88,43 @@ string escapeJson(string s) {
return result; return result;
} }
string computeHmac(string secretKey, string message) {
import std.process : pipeShell, Redirect, wait;
import std.stdio : File;
auto cmd = format("echo -n '%s' | openssl dgst -sha256 -hmac '%s' -hex 2>/dev/null | sed 's/.*= //'", message, secretKey);
auto pipes = pipeShell(cmd, Redirect.stdout);
string result = pipes.stdout.readln().strip();
wait(pipes.pid);
return result;
}
string getTimestamp() {
import std.datetime.systime : Clock;
return format("%d", Clock.currTime.toUnixTime());
}
string buildAuthHeaders(string method, string path, string body, string publicKey, string secretKey) {
if (secretKey.empty) {
// Legacy mode: use public_key as bearer token
return format("-H 'Authorization: Bearer %s'", publicKey);
}
// HMAC mode
string timestamp = getTimestamp();
string message = format("%s:%s:%s:%s", timestamp, method, path, body);
string signature = computeHmac(secretKey, message);
return format("-H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'",
publicKey, timestamp, signature);
}
string execCurl(string cmd) { string execCurl(string cmd) {
auto result = executeShell(cmd); auto result = executeShell(cmd);
return result.output; return result.output;
} }
void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network, int vcpu, string apiKey) { void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network, int vcpu, string publicKey, string secretKey) {
string lang = detectLanguage(sourceFile); string lang = detectLanguage(sourceFile);
if (lang.empty) { if (lang.empty) {
stderr.writefln("%sError: Cannot detect language%s", RED, RESET); stderr.writefln("%sError: Cannot detect language%s", RED, RESET);
@ -120,21 +151,25 @@ void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network
if (vcpu > 0) json ~= format(`,"vcpu":%d`, vcpu); if (vcpu > 0) json ~= format(`,"vcpu":%d`, vcpu);
json ~= "}"; json ~= "}";
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json); string authHeaders = buildAuthHeaders("POST", "/execute", json, publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
string result = execCurl(cmd); string result = execCurl(cmd);
writeln(result); writeln(result);
} }
void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string apiKey) { void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string publicKey, string secretKey) {
if (list) { if (list) {
string cmd = format(`curl -s -X GET '%s/sessions' -H 'Authorization: Bearer %s'`, API_BASE, apiKey); string authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey);
string cmd = format(`curl -s -X GET '%s/sessions' %s`, API_BASE, authHeaders);
writeln(execCurl(cmd)); writeln(execCurl(cmd));
return; return;
} }
if (!kill.empty) { if (!kill.empty) {
string cmd = format(`curl -s -X DELETE '%s/sessions/%s' -H 'Authorization: Bearer %s'`, API_BASE, kill, apiKey); string path = format("/sessions/%s", kill);
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X DELETE '%s/sessions/%s' %s`, API_BASE, kill, authHeaders);
execCurl(cmd); execCurl(cmd);
writefln("%sSession terminated: %s%s", GREEN, kill, RESET); writefln("%sSession terminated: %s%s", GREEN, kill, RESET);
return; return;
@ -148,51 +183,65 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
json ~= "}"; json ~= "}";
writefln("%sCreating session...%s", YELLOW, RESET); writefln("%sCreating session...%s", YELLOW, RESET);
string cmd = format(`curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json); string authHeaders = buildAuthHeaders("POST", "/sessions", json, publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
writeln(execCurl(cmd)); writeln(execCurl(cmd));
} }
void cmdService(string name, string ports, string bootstrap, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string apiKey) { void cmdService(string name, string ports, string bootstrap, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string publicKey, string secretKey) {
if (list) { if (list) {
string cmd = format(`curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'`, API_BASE, apiKey); string authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey);
string cmd = format(`curl -s -X GET '%s/services' %s`, API_BASE, authHeaders);
writeln(execCurl(cmd)); writeln(execCurl(cmd));
return; return;
} }
if (!info.empty) { if (!info.empty) {
string cmd = format(`curl -s -X GET '%s/services/%s' -H 'Authorization: Bearer %s'`, API_BASE, info, apiKey); string path = format("/services/%s", info);
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X GET '%s/services/%s' %s`, API_BASE, info, authHeaders);
writeln(execCurl(cmd)); writeln(execCurl(cmd));
return; return;
} }
if (!logs.empty) { if (!logs.empty) {
string cmd = format(`curl -s -X GET '%s/services/%s/logs' -H 'Authorization: Bearer %s'`, API_BASE, logs, apiKey); string path = format("/services/%s/logs", logs);
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X GET '%s/services/%s/logs' %s`, API_BASE, logs, authHeaders);
write(execCurl(cmd)); write(execCurl(cmd));
return; return;
} }
if (!tail.empty) { if (!tail.empty) {
string cmd = format(`curl -s -X GET '%s/services/%s/logs?lines=9000' -H 'Authorization: Bearer %s'`, API_BASE, tail, apiKey); string path = format("/services/%s/logs", tail);
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X GET '%s/services/%s/logs?lines=9000' %s`, API_BASE, tail, authHeaders);
write(execCurl(cmd)); write(execCurl(cmd));
return; return;
} }
if (!sleep.empty) { if (!sleep.empty) {
string cmd = format(`curl -s -X POST '%s/services/%s/sleep' -H 'Authorization: Bearer %s'`, API_BASE, sleep, apiKey); string path = format("/services/%s/sleep", sleep);
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/services/%s/sleep' %s`, API_BASE, sleep, authHeaders);
execCurl(cmd); execCurl(cmd);
writefln("%sService sleeping: %s%s", GREEN, sleep, RESET); writefln("%sService sleeping: %s%s", GREEN, sleep, RESET);
return; return;
} }
if (!wake.empty) { if (!wake.empty) {
string cmd = format(`curl -s -X POST '%s/services/%s/wake' -H 'Authorization: Bearer %s'`, API_BASE, wake, apiKey); string path = format("/services/%s/wake", wake);
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/services/%s/wake' %s`, API_BASE, wake, authHeaders);
execCurl(cmd); execCurl(cmd);
writefln("%sService waking: %s%s", GREEN, wake, RESET); writefln("%sService waking: %s%s", GREEN, wake, RESET);
return; return;
} }
if (!destroy.empty) { if (!destroy.empty) {
string cmd = format(`curl -s -X DELETE '%s/services/%s' -H 'Authorization: Bearer %s'`, API_BASE, destroy, apiKey); string path = format("/services/%s", destroy);
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
string cmd = format(`curl -s -X DELETE '%s/services/%s' %s`, API_BASE, destroy, authHeaders);
execCurl(cmd); execCurl(cmd);
writefln("%sService destroyed: %s%s", GREEN, destroy, RESET); writefln("%sService destroyed: %s%s", GREEN, destroy, RESET);
return; return;
@ -200,7 +249,9 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
if (!execute.empty) { if (!execute.empty) {
string json = format(`{"command":"%s"}`, escapeJson(command)); string json = format(`{"command":"%s"}`, escapeJson(command));
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, execute, apiKey, json); string path = format("/services/%s/execute", execute);
string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, execute, authHeaders, json);
string result = execCurl(cmd); string result = execCurl(cmd);
// Simple JSON parsing for stdout/stderr // Simple JSON parsing for stdout/stderr
@ -229,7 +280,10 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
if (!dumpBootstrap.empty) { if (!dumpBootstrap.empty) {
stderr.writefln("Fetching bootstrap script from %s...", dumpBootstrap); stderr.writefln("Fetching bootstrap script from %s...", dumpBootstrap);
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{"command":"cat /tmp/bootstrap.sh"}'`, API_BASE, dumpBootstrap, apiKey); string json = `{"command":"cat /tmp/bootstrap.sh"}`;
string path = format("/services/%s/execute", dumpBootstrap);
string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, dumpBootstrap, authHeaders, json);
string result = execCurl(cmd); string result = execCurl(cmd);
import std.algorithm : findSplitAfter; import std.algorithm : findSplitAfter;
@ -283,7 +337,8 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
json ~= "}"; json ~= "}";
writefln("%sCreating service...%s", YELLOW, RESET); writefln("%sCreating service...%s", YELLOW, RESET);
string cmd = format(`curl -s -X POST '%s/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json); string authHeaders = buildAuthHeaders("POST", "/services", json, publicKey, secretKey);
string cmd = format(`curl -s -X POST '%s/services' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
writeln(execCurl(cmd)); writeln(execCurl(cmd));
return; return;
} }
@ -318,11 +373,12 @@ string formatDuration(long totalMinutes) {
} }
} }
void validateKey(string apiKey, bool extend) { void validateKey(string publicKey, string secretKey, bool extend) {
import std.json; import std.json;
import std.datetime; 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 authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey);
string cmd = format(`curl -s -w '\n%%{http_code}' -X POST '%s/keys/validate' -H 'Content-Type: application/json' %s`, PORTAL_BASE, authHeaders);
string response = execCurl(cmd); string response = execCurl(cmd);
auto lines = response.split("\n"); auto lines = response.split("\n");
@ -413,7 +469,13 @@ void validateKey(string apiKey, bool extend) {
} }
int main(string[] args) { int main(string[] args) {
string apiKey = environment.get("UNSANDBOX_API_KEY", ""); string publicKey = environment.get("UNSANDBOX_PUBLIC_KEY", "");
string secretKey = environment.get("UNSANDBOX_SECRET_KEY", "");
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (publicKey.empty) {
publicKey = environment.get("UNSANDBOX_API_KEY", "");
}
if (args.length < 2) { if (args.length < 2) {
stderr.writefln("Usage: %s [options] <source_file>", args[0]); stderr.writefln("Usage: %s [options] <source_file>", args[0]);
@ -437,10 +499,10 @@ int main(string[] args) {
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]); else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
else if (args[i] == "--tmux") tmux = true; else if (args[i] == "--tmux") tmux = true;
else if (args[i] == "--screen") screen = true; else if (args[i] == "--screen") screen = true;
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
} }
cmdSession(list, kill, shell, network, vcpu, tmux, screen, apiKey); cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey);
return 0; return 0;
} }
@ -468,10 +530,10 @@ int main(string[] args) {
else if (args[i] == "--dump-file" && i+1 < args.length) dumpFile = args[++i]; else if (args[i] == "--dump-file" && i+1 < args.length) dumpFile = args[++i];
else if (args[i] == "-n" && i+1 < args.length) network = args[++i]; else if (args[i] == "-n" && i+1 < args.length) network = args[++i];
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]); else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
} }
cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey); cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey);
return 0; return 0;
} }
@ -480,15 +542,15 @@ int main(string[] args) {
for (size_t i = 2; i < args.length; i++) { for (size_t i = 2; i < args.length; i++) {
if (args[i] == "--extend") extend = true; if (args[i] == "--extend") extend = true;
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
} }
if (apiKey.empty) { if (publicKey.empty) {
stderr.writefln("%sError: UNSANDBOX_API_KEY not set%s", RED, RESET); stderr.writefln("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s", RED, RESET);
return 1; return 1;
} }
validateKey(apiKey, extend); validateKey(publicKey, secretKey, extend);
return 0; return 0;
} }
@ -503,7 +565,7 @@ int main(string[] args) {
else if (args[i] == "-a") artifacts = true; else if (args[i] == "-a") artifacts = true;
else if (args[i] == "-n" && i+1 < args.length) network = args[++i]; else if (args[i] == "-n" && i+1 < args.length) network = args[++i];
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]); else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
else if (!args[i].startsWith("-")) sourceFile = args[i]; else if (!args[i].startsWith("-")) sourceFile = args[i];
} }
@ -512,6 +574,6 @@ int main(string[] args) {
return 1; return 1;
} }
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey); cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey);
return 0; return 0;
} }

92
un.dart
View file

@ -42,6 +42,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart';
const String apiBase = 'https://api.unsandbox.com'; const String apiBase = 'https://api.unsandbox.com';
const String portalBase = 'https://unsandbox.com'; const String portalBase = 'https://unsandbox.com';
@ -98,13 +99,21 @@ class Args {
bool keyExtend = false; bool keyExtend = false;
} }
String getApiKey(String? argsKey) { List<String?> getApiKeys(String? argsKey) {
final key = argsKey ?? Platform.environment['UNSANDBOX_API_KEY']; final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
if (key == null || key.isEmpty) { final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY'];
stderr.writeln('${red}Error: UNSANDBOX_API_KEY not set$reset');
exit(1); // Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (publicKey == null || publicKey.isEmpty || secretKey == null || secretKey.isEmpty) {
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
if (legacyKey == null || legacyKey.isEmpty) {
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
exit(1);
}
return [legacyKey, null];
} }
return key;
return [publicKey, secretKey];
} }
String detectLanguage(String filename) { String detectLanguage(String filename) {
@ -120,18 +129,37 @@ String detectLanguage(String filename) {
return lang; return lang;
} }
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String apiKey, {String? baseUrl}) async { Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String publicKey, String? secretKey, {String? baseUrl}) async {
final base = baseUrl ?? apiBase; final base = baseUrl ?? apiBase;
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create(); final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
try { try {
final body = jsonData ?? '';
if (jsonData != null) { if (jsonData != null) {
await tempFile.writeAsString(jsonData); await tempFile.writeAsString(jsonData);
} }
final args = ['curl', '-s', '-X', method, '$base$endpoint', final args = ['curl', '-s', '-X', method, '$base$endpoint',
'-H', 'Content-Type: application/json', '-H', 'Content-Type: application/json'];
'-H', 'Authorization: Bearer $apiKey'];
// Add HMAC authentication headers if secretKey is provided
if (secretKey != null && secretKey.isNotEmpty) {
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
final message = '$timestamp:$method:$endpoint:$body';
final key = utf8.encode(secretKey);
final bytes = utf8.encode(message);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final signature = digest.toString();
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
args.addAll(['-H', 'X-Timestamp: $timestamp']);
args.addAll(['-H', 'X-Signature: $signature']);
} else {
// Legacy API key authentication
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
}
if (jsonData != null) { if (jsonData != null) {
args.addAll(['-d', '@${tempFile.path}']); args.addAll(['-d', '@${tempFile.path}']);
@ -151,7 +179,9 @@ Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, Stri
} }
Future<void> cmdExecute(Args args) async { Future<void> cmdExecute(Args args) async {
final apiKey = getApiKey(args.apiKey); final keys = getApiKeys(args.apiKey);
final publicKey = keys[0]!;
final secretKey = keys[1];
final code = await File(args.sourceFile!).readAsString(); final code = await File(args.sourceFile!).readAsString();
final language = detectLanguage(args.sourceFile!); final language = detectLanguage(args.sourceFile!);
@ -195,7 +225,7 @@ Future<void> cmdExecute(Args args) async {
payload['vcpu'] = args.vcpu; payload['vcpu'] = args.vcpu;
} }
final result = await apiRequestCurl('/execute', 'POST', jsonEncode(payload), apiKey); final result = await apiRequestCurl('/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final stdoutText = result['stdout'] as String?; final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?; final stderrText = result['stderr'] as String?;
@ -227,10 +257,12 @@ Future<void> cmdExecute(Args args) async {
} }
Future<void> cmdSession(Args args) async { Future<void> cmdSession(Args args) async {
final apiKey = getApiKey(args.apiKey); final keys = getApiKeys(args.apiKey);
final publicKey = keys[0]!;
final secretKey = keys[1];
if (args.sessionList) { if (args.sessionList) {
final result = await apiRequestCurl('/sessions', 'GET', null, apiKey); final result = await apiRequestCurl('/sessions', 'GET', null, publicKey, secretKey);
final sessions = result['sessions'] as List? ?? []; final sessions = result['sessions'] as List? ?? [];
if (sessions.isEmpty) { if (sessions.isEmpty) {
print('No active sessions'); print('No active sessions');
@ -245,7 +277,7 @@ Future<void> cmdSession(Args args) async {
} }
if (args.sessionKill != null) { if (args.sessionKill != null) {
await apiRequestCurl('/sessions/${args.sessionKill}', 'DELETE', null, apiKey); await apiRequestCurl('/sessions/${args.sessionKill}', 'DELETE', null, publicKey, secretKey);
print('${green}Session terminated: ${args.sessionKill}$reset'); print('${green}Session terminated: ${args.sessionKill}$reset');
return; return;
} }
@ -261,16 +293,18 @@ Future<void> cmdSession(Args args) async {
} }
print('${yellow}Creating session...$reset'); print('${yellow}Creating session...$reset');
final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), apiKey); final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Session created: ${result['id'] ?? 'N/A'}$reset'); print('${green}Session created: ${result['id'] ?? 'N/A'}$reset');
print('${yellow}(Interactive sessions require WebSocket - use un2 for full support)$reset'); print('${yellow}(Interactive sessions require WebSocket - use un2 for full support)$reset');
} }
Future<void> cmdService(Args args) async { Future<void> cmdService(Args args) async {
final apiKey = getApiKey(args.apiKey); final keys = getApiKeys(args.apiKey);
final publicKey = keys[0]!;
final secretKey = keys[1];
if (args.serviceList) { if (args.serviceList) {
final result = await apiRequestCurl('/services', 'GET', null, apiKey); final result = await apiRequestCurl('/services', 'GET', null, publicKey, secretKey);
final services = result['services'] as List? ?? []; final services = result['services'] as List? ?? [];
if (services.isEmpty) { if (services.isEmpty) {
print('No services'); print('No services');
@ -287,37 +321,37 @@ Future<void> cmdService(Args args) async {
} }
if (args.serviceInfo != null) { if (args.serviceInfo != null) {
final result = await apiRequestCurl('/services/${args.serviceInfo}', 'GET', null, apiKey); final result = await apiRequestCurl('/services/${args.serviceInfo}', 'GET', null, publicKey, secretKey);
print(jsonEncode(result)); print(jsonEncode(result));
return; return;
} }
if (args.serviceLogs != null) { if (args.serviceLogs != null) {
final result = await apiRequestCurl('/services/${args.serviceLogs}/logs', 'GET', null, apiKey); final result = await apiRequestCurl('/services/${args.serviceLogs}/logs', 'GET', null, publicKey, secretKey);
print(result['logs'] ?? ''); print(result['logs'] ?? '');
return; return;
} }
if (args.serviceTail != null) { if (args.serviceTail != null) {
final result = await apiRequestCurl('/services/${args.serviceTail}/logs?lines=9000', 'GET', null, apiKey); final result = await apiRequestCurl('/services/${args.serviceTail}/logs?lines=9000', 'GET', null, publicKey, secretKey);
print(result['logs'] ?? ''); print(result['logs'] ?? '');
return; return;
} }
if (args.serviceSleep != null) { if (args.serviceSleep != null) {
await apiRequestCurl('/services/${args.serviceSleep}/sleep', 'POST', null, apiKey); await apiRequestCurl('/services/${args.serviceSleep}/sleep', 'POST', null, publicKey, secretKey);
print('${green}Service sleeping: ${args.serviceSleep}$reset'); print('${green}Service sleeping: ${args.serviceSleep}$reset');
return; return;
} }
if (args.serviceWake != null) { if (args.serviceWake != null) {
await apiRequestCurl('/services/${args.serviceWake}/wake', 'POST', null, apiKey); await apiRequestCurl('/services/${args.serviceWake}/wake', 'POST', null, publicKey, secretKey);
print('${green}Service waking: ${args.serviceWake}$reset'); print('${green}Service waking: ${args.serviceWake}$reset');
return; return;
} }
if (args.serviceDestroy != null) { if (args.serviceDestroy != null) {
await apiRequestCurl('/services/${args.serviceDestroy}', 'DELETE', null, apiKey); await apiRequestCurl('/services/${args.serviceDestroy}', 'DELETE', null, publicKey, secretKey);
print('${green}Service destroyed: ${args.serviceDestroy}$reset'); print('${green}Service destroyed: ${args.serviceDestroy}$reset');
return; return;
} }
@ -326,7 +360,7 @@ Future<void> cmdService(Args args) async {
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'command': args.serviceCommand, 'command': args.serviceCommand,
}; };
final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), apiKey); final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final stdoutText = result['stdout'] as String?; final stdoutText = result['stdout'] as String?;
final stderrText = result['stderr'] as String?; final stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) { if (stdoutText != null && stdoutText.isNotEmpty) {
@ -343,7 +377,7 @@ Future<void> cmdService(Args args) async {
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'command': 'cat /tmp/bootstrap.sh', 'command': 'cat /tmp/bootstrap.sh',
}; };
final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), apiKey); final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
final bootstrap = result['stdout'] as String?; final bootstrap = result['stdout'] as String?;
if (bootstrap != null && bootstrap.isNotEmpty) { if (bootstrap != null && bootstrap.isNotEmpty) {
@ -386,7 +420,7 @@ Future<void> cmdService(Args args) async {
payload['vcpu'] = args.vcpu; payload['vcpu'] = args.vcpu;
} }
final result = await apiRequestCurl('/services', 'POST', jsonEncode(payload), apiKey); final result = await apiRequestCurl('/services', 'POST', jsonEncode(payload), publicKey, secretKey);
print('${green}Service created: ${result['id'] ?? 'N/A'}$reset'); print('${green}Service created: ${result['id'] ?? 'N/A'}$reset');
print('Name: ${result['name'] ?? 'N/A'}'); print('Name: ${result['name'] ?? 'N/A'}');
if (result.containsKey('url')) { if (result.containsKey('url')) {
@ -400,10 +434,12 @@ Future<void> cmdService(Args args) async {
} }
Future<void> cmdKey(Args args) async { Future<void> cmdKey(Args args) async {
final apiKey = getApiKey(args.apiKey); final keys = getApiKeys(args.apiKey);
final publicKey = keys[0]!;
final secretKey = keys[1];
try { try {
final result = await apiRequestCurl('/keys/validate', 'POST', null, apiKey, baseUrl: portalBase); final result = await apiRequestCurl('/keys/validate', 'POST', null, publicKey, secretKey, baseUrl: portalBase);
// Handle --extend flag // Handle --extend flag
if (args.keyExtend) { if (args.keyExtend) {

60
un.erl
View file

@ -294,12 +294,44 @@ open_extend_page(PublicKey) ->
end. end.
%% Helpers %% Helpers
get_api_keys() ->
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
if
PublicKey =/= false andalso SecretKey =/= false ->
{PublicKey, SecretKey};
ApiKey =/= false ->
{ApiKey, false};
true ->
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
halt(1)
end.
get_api_key() -> get_api_key() ->
case os:getenv("UNSANDBOX_API_KEY") of {PublicKey, _} = get_api_keys(),
false -> PublicKey.
io:format("Error: UNSANDBOX_API_KEY not set~n"),
halt(1); hmac_sha256(Secret, Message) ->
Key -> Key string:lowercase(
lists:flatten([io_lib:format("~2.16.0b", [X]) || X <- binary_to_list(crypto:mac(hmac, sha256, Secret, Message))])
).
make_signature(SecretKey, Timestamp, Method, Path, Body) ->
Message = Timestamp ++ ":" ++ Method ++ ":" ++ Path ++ ":" ++ Body,
hmac_sha256(SecretKey, Message).
build_auth_headers(PublicKey, SecretKey, Method, Path, Body) ->
if
SecretKey =/= false ->
Timestamp = integer_to_list(erlang:system_time(second)),
Signature = make_signature(SecretKey, Timestamp, Method, Path, Body),
" -H 'Authorization: Bearer " ++ PublicKey ++ "'"
++ " -H 'X-Timestamp: " ++ Timestamp ++ "'"
++ " -H 'X-Signature: " ++ Signature ++ "'";
true ->
" -H 'Authorization: Bearer " ++ PublicKey ++ "'"
end. end.
ext_to_lang(".hs") -> {ok, "haskell"}; ext_to_lang(".hs") -> {ok, "haskell"};
@ -364,30 +396,40 @@ write_temp_file(Data) ->
TmpFile. TmpFile.
curl_post(ApiKey, Endpoint, TmpFile) -> curl_post(ApiKey, Endpoint, TmpFile) ->
{ok, Body} = file:read_file(TmpFile),
BodyStr = binary_to_list(Body),
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, BodyStr),
Cmd = "curl -s -X POST https://api.unsandbox.com" ++ Endpoint ++ Cmd = "curl -s -X POST https://api.unsandbox.com" ++ Endpoint ++
" -H 'Content-Type: application/json'" ++ " -H 'Content-Type: application/json'" ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++ AuthHeaders ++
" -d @" ++ TmpFile, " -d @" ++ TmpFile,
os:cmd(Cmd). os:cmd(Cmd).
curl_post_portal(ApiKey, Endpoint, Data) -> curl_post_portal(ApiKey, Endpoint, Data) ->
TmpFile = write_temp_file(Data), TmpFile = write_temp_file(Data),
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, Data),
Cmd = "curl -s -X POST https://unsandbox.com" ++ Endpoint ++ Cmd = "curl -s -X POST https://unsandbox.com" ++ Endpoint ++
" -H 'Content-Type: application/json'" ++ " -H 'Content-Type: application/json'" ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++ AuthHeaders ++
" -d @" ++ TmpFile, " -d @" ++ TmpFile,
Result = os:cmd(Cmd), Result = os:cmd(Cmd),
file:delete(TmpFile), file:delete(TmpFile),
Result. Result.
curl_get(ApiKey, Endpoint) -> curl_get(ApiKey, Endpoint) ->
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "GET", Endpoint, ""),
Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++ Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'", AuthHeaders,
os:cmd(Cmd). os:cmd(Cmd).
curl_delete(ApiKey, Endpoint) -> curl_delete(ApiKey, Endpoint) ->
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "DELETE", Endpoint, ""),
Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++ Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'", AuthHeaders,
os:cmd(Cmd). os:cmd(Cmd).
%% Argument parsing %% Argument parsing

99
un.ex
View file

@ -401,15 +401,39 @@ defmodule Un do
end end
# Helpers # Helpers
defp get_api_key do defp get_api_keys do
case System.get_env("UNSANDBOX_API_KEY") do public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
nil -> secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
IO.puts(:stderr, "Error: UNSANDBOX_API_KEY not set")
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
api_key = System.get_env("UNSANDBOX_API_KEY")
cond do
public_key && secret_key ->
{public_key, secret_key}
api_key ->
{api_key, nil}
true ->
IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
System.halt(1) System.halt(1)
key -> key
end end
end end
defp get_api_key do
{public_key, _} = get_api_keys()
public_key
end
defp hmac_sha256(secret, message) do
:crypto.mac(:hmac, :sha256, secret, message)
|> Base.encode16(case: :lower)
end
defp make_signature(secret_key, timestamp, method, path, body) do
message = "#{timestamp}:#{method}:#{path}:#{body}"
hmac_sha256(secret_key, message)
end
defp escape_json(s) do defp escape_json(s) do
s s
|> String.replace("\\", "\\\\") |> String.replace("\\", "\\\\")
@ -427,50 +451,79 @@ defmodule Un do
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json" tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
File.write!(tmp_file, json) File.write!(tmp_file, json)
{output, _exit} = System.cmd("curl", [ {public_key, secret_key} = get_api_keys()
headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)
args = [
"-s", "-X", "POST", "-s", "-X", "POST",
"https://api.unsandbox.com#{endpoint}", "https://api.unsandbox.com#{endpoint}",
"-H", "Content-Type: application/json", "-H", "Content-Type: application/json"
"-H", "Authorization: Bearer #{api_key}", ] ++ headers ++ ["-d", "@#{tmp_file}"]
"-d", "@#{tmp_file}"
], stderr_to_stdout: true) {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file) File.rm(tmp_file)
output output
end end
defp build_auth_headers(public_key, secret_key, method, path, body) do
if secret_key do
timestamp = System.system_time(:second) |> Integer.to_string()
signature = make_signature(secret_key, timestamp, method, path, body)
[
"-H", "Authorization: Bearer #{public_key}",
"-H", "X-Timestamp: #{timestamp}",
"-H", "X-Signature: #{signature}"
]
else
# Backwards compatibility: use simple bearer token
["-H", "Authorization: Bearer #{public_key}"]
end
end
defp portal_curl_post(api_key, endpoint, json) do defp portal_curl_post(api_key, endpoint, json) do
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json" tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
File.write!(tmp_file, json) File.write!(tmp_file, json)
{output, _exit} = System.cmd("curl", [ {public_key, secret_key} = get_api_keys()
headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)
args = [
"-s", "-X", "POST", "-s", "-X", "POST",
"#{@portal_base}#{endpoint}", "#{@portal_base}#{endpoint}",
"-H", "Content-Type: application/json", "-H", "Content-Type: application/json"
"-H", "Authorization: Bearer #{api_key}", ] ++ headers ++ ["-d", "@#{tmp_file}"]
"-d", "@#{tmp_file}"
], stderr_to_stdout: true) {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file) File.rm(tmp_file)
output output
end end
defp curl_get(api_key, endpoint) do defp curl_get(api_key, endpoint) do
{output, _exit} = System.cmd("curl", [ {public_key, secret_key} = get_api_keys()
headers = build_auth_headers(public_key, secret_key, "GET", endpoint, "")
args = [
"-s", "-s",
"https://api.unsandbox.com#{endpoint}", "https://api.unsandbox.com#{endpoint}"
"-H", "Authorization: Bearer #{api_key}" ] ++ headers
], stderr_to_stdout: true)
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
output output
end end
defp curl_delete(api_key, endpoint) do defp curl_delete(api_key, endpoint) do
{output, _exit} = System.cmd("curl", [ {public_key, secret_key} = get_api_keys()
headers = build_auth_headers(public_key, secret_key, "DELETE", endpoint, "")
args = [
"-s", "-X", "DELETE", "-s", "-X", "DELETE",
"https://api.unsandbox.com#{endpoint}", "https://api.unsandbox.com#{endpoint}"
"-H", "Authorization: Bearer #{api_key}" ] ++ headers
], stderr_to_stdout: true)
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
output output
end end

85
un.f90
View file

@ -87,8 +87,8 @@ contains
subroutine handle_execute(fname) subroutine handle_execute(fname)
character(len=*), intent(in) :: fname character(len=*), intent(in) :: fname
character(len=2048) :: full_cmd character(len=4096) :: full_cmd
character(len=1024) :: env_opts, file_opts, net_opt character(len=1024) :: env_opts, file_opts, net_opt, public_key, secret_key
integer :: i, arg_idx integer :: i, arg_idx
logical :: artifacts, has_env, has_files logical :: artifacts, has_env, has_files
@ -131,11 +131,23 @@ contains
stop 1 stop 1
end if end if
! Get API key ! Get API keys (try new format first, fall back to old)
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat) call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=stat)
if (stat /= 0 .or. len_trim(api_key) == 0) then if (stat == 0 .and. len_trim(public_key) > 0) then
write(0, '(A)') 'Error: UNSANDBOX_API_KEY environment variable not set' call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=stat)
stop 1 if (stat /= 0 .or. len_trim(secret_key) == 0) then
write(0, '(A)') 'Error: UNSANDBOX_SECRET_KEY not set'
stop 1
end if
else
! Fall back to old-style single key
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
if (stat /= 0 .or. len_trim(api_key) == 0) then
write(0, '(A)') 'Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set'
stop 1
end if
public_key = api_key
secret_key = api_key
end if end if
! Parse additional arguments (simple version - only support basic flags) ! Parse additional arguments (simple version - only support basic flags)
@ -144,14 +156,17 @@ contains
net_opt = '' net_opt = ''
artifacts = .false. artifacts = .false.
! Build curl command ! Build curl command with HMAC auth (use bash to compute signature)
write(full_cmd, '(20A)') & write(full_cmd, '(30A)') &
'TS=$(date +%s); ', &
'BODY=$(jq -Rs ''{language: "', trim(language), '", code: .}'' < "', trim(fname), '"); ', &
'SIG=$(echo -n "$TS:POST:/execute:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/execute ', & 'curl -s -X POST https://api.unsandbox.com/execute ', &
'-H "Content-Type: application/json" ', & '-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(api_key), '" ', & '-H "Authorization: Bearer ', trim(public_key), '" ', &
'--data-binary @- -o /tmp/unsandbox_resp.json ', & '-H "X-Timestamp: $TS" ', &
'< <(jq -Rs ''{language: "', trim(language), '", code: .}'' ', & '-H "X-Signature: $SIG" ', &
'< "', trim(fname), '"); ', & '--data-binary "$BODY" -o /tmp/unsandbox_resp.json; ', &
'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', & 'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', &
'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', & 'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', &
'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', & 'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', &
@ -167,8 +182,9 @@ contains
end subroutine handle_execute end subroutine handle_execute
subroutine handle_session() subroutine handle_session()
character(len=2048) :: full_cmd character(len=4096) :: full_cmd
character(len=256) :: arg, session_id character(len=256) :: arg, session_id
character(len=1024) :: public_key, secret_key
integer :: i, stat integer :: i, stat
logical :: list_mode, kill_mode logical :: list_mode, kill_mode
@ -189,27 +205,46 @@ contains
end if end if
end do end do
! Get API key ! Get API keys (try new format first, fall back to old)
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat) call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=stat)
if (stat /= 0 .or. len_trim(api_key) == 0) then if (stat == 0 .and. len_trim(public_key) > 0) then
write(0, '(A)') 'Error: UNSANDBOX_API_KEY not set' call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=stat)
stop 1 if (stat /= 0 .or. len_trim(secret_key) == 0) then
write(0, '(A)') 'Error: UNSANDBOX_SECRET_KEY not set'
stop 1
end if
else
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
if (stat /= 0 .or. len_trim(api_key) == 0) then
write(0, '(A)') 'Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set'
stop 1
end if
public_key = api_key
secret_key = api_key
end if end if
if (list_mode) then if (list_mode) then
! List sessions ! List sessions - GET request with empty body
write(full_cmd, '(10A)') & write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/sessions:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/sessions ', & 'curl -s -X GET https://api.unsandbox.com/sessions ', &
'-H "Authorization: Bearer ', trim(api_key), '" | ', & '-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | ', &
'jq -r ''.sessions[] | "\(.id) \(.shell) \(.status) \(.created_at)"'' ', & 'jq -r ''.sessions[] | "\(.id) \(.shell) \(.status) \(.created_at)"'' ', &
'2>/dev/null || echo "No active sessions"' '2>/dev/null || echo "No active sessions"'
call execute_command_line(trim(full_cmd), wait=.true.) call execute_command_line(trim(full_cmd), wait=.true.)
else if (kill_mode .and. len_trim(session_id) > 0) then else if (kill_mode .and. len_trim(session_id) > 0) then
! Kill session ! Kill session - DELETE request with empty body
write(full_cmd, '(10A)') & write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/sessions/', trim(session_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE https://api.unsandbox.com/sessions/', & 'curl -s -X DELETE https://api.unsandbox.com/sessions/', &
trim(session_id), ' ', & trim(session_id), ' ', &
'-H "Authorization: Bearer ', trim(api_key), '" >/dev/null && ', & '-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mSession terminated: ', trim(session_id), '\x1b[0m"' 'echo -e "\x1b[32mSession terminated: ', trim(session_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.) call execute_command_line(trim(full_cmd), wait=.true.)
else else

211
un.forth
View file

@ -91,15 +91,30 @@
find-ext ext-lang find-ext ext-lang
; ;
\ Get API key from environment \ Get API keys from environment (HMAC or legacy)
: get-api-key ( -- addr len ) : get-public-key ( -- addr len )
s" UNSANDBOX_API_KEY" getenv s" UNSANDBOX_PUBLIC_KEY" getenv
dup 0= if dup 0= if
s" Error: UNSANDBOX_API_KEY not set" type cr 2drop s" UNSANDBOX_API_KEY" getenv
then
dup 0= if
s" Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" type cr
1 (bye) 1 (bye)
then then
; ;
: get-secret-key ( -- addr len )
s" UNSANDBOX_SECRET_KEY" getenv
dup 0= if
2drop s" UNSANDBOX_API_KEY" getenv
then
;
\ Get API key (legacy compatibility)
: get-api-key ( -- addr len )
get-public-key
;
\ Execute a file \ Execute a file
: execute-file ( addr len -- ) : execute-file ( addr len -- )
\ Check file exists \ Check file exists
@ -125,8 +140,11 @@
s" /tmp/unsandbox_script.sh" w/o create-file throw s" /tmp/unsandbox_script.sh" w/o create-file throw
>r >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" API_KEY='" r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
get-api-key r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw s" '" r@ write-line throw
s" LANG='" r@ write-file throw s" LANG='" r@ write-file throw
2swap 2drop \ drop language, keep filename on stack 2swap 2drop \ drop language, keep filename on stack
@ -135,7 +153,11 @@
s" FILE='" r@ write-file throw s" FILE='" r@ write-file throw
r@ write-file throw r@ write-file throw
s" '" r@ write-line throw s" '" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" --data-binary @- -o /tmp/unsandbox_resp.json < <(jq -Rs '{language: \"'$LANG'\", code: .}' < \"$FILE\"); jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw s" BODY=$(jq -Rs '{language: \"'$LANG'\", code: .}' < \"$FILE\")" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/execute:$BODY\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json; jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_script.sh && /tmp/unsandbox_script.sh && rm -f /tmp/unsandbox_script.sh" system s" chmod +x /tmp/unsandbox_script.sh && /tmp/unsandbox_script.sh && rm -f /tmp/unsandbox_script.sh" system
@ -147,9 +169,16 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/sessions -H 'Authorization: Bearer " r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
get-api-key r@ write-file throw get-public-key r@ write-file throw
s" ' | jq -r '.sessions[] | \"\\(.id) \\(.shell) \\(.status) \\(.created_at)\"' 2>/dev/null || echo 'No active sessions'" r@ write-line throw s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:GET:/sessions:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/sessions -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.sessions[] | \"\\(.id) \\(.shell) \\(.status) \\(.created_at)\"' 2>/dev/null || echo 'No active sessions'" r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
; ;
@ -159,11 +188,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X DELETE https://api.unsandbox.com/sessions/" r@ write-file throw s" SESSION_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mSession terminated: " r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:DELETE:/sessions/$SESSION_ID:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X DELETE https://api.unsandbox.com/sessions/$SESSION_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mSession terminated: " r@ write-file throw
r@ write-file throw r@ write-file throw
s" \\x1b[0m'" r@ write-line throw s" \\x1b[0m'" r@ write-line throw
r> close-file throw r> close-file throw
@ -175,9 +212,16 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services -H 'Authorization: Bearer " r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
get-api-key r@ write-file throw get-public-key r@ write-file throw
s" ' | jq -r '.services[] | \"\\(.id) \\(.name) \\(.status)\"' 2>/dev/null || echo 'No services'" r@ write-line throw s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:GET:/services:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.services[] | \"\\(.id) \\(.name) \\(.status)\"' 2>/dev/null || echo 'No services'" r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
; ;
@ -187,11 +231,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services/" r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' | jq ." r@ write-line throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:GET:/services/$SERVICE_ID:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services/$SERVICE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
; ;
@ -201,11 +253,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services/" r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" /logs -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' | jq -r '.logs'" r@ write-line throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:GET:/services/$SERVICE_ID/logs:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services/$SERVICE_ID/logs -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.logs'" r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
; ;
@ -215,11 +275,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" /sleep -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService sleeping: " r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/sleep:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/sleep -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService sleeping: " r@ write-file throw
r@ write-file throw r@ write-file throw
s" \\x1b[0m'" r@ write-line throw s" \\x1b[0m'" r@ write-line throw
r> close-file throw r> close-file throw
@ -231,11 +299,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" /wake -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService waking: " r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/wake:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/wake -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService waking: " r@ write-file throw
r@ write-file throw r@ write-file throw
s" \\x1b[0m'" r@ write-line throw s" \\x1b[0m'" r@ write-line throw
r> close-file throw r> close-file throw
@ -247,11 +323,19 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" curl -s -X DELETE https://api.unsandbox.com/services/" r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2dup r@ write-file throw 2dup r@ write-file throw
s" -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService destroyed: " r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:DELETE:/services/$SERVICE_ID:\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X DELETE https://api.unsandbox.com/services/$SERVICE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService destroyed: " r@ write-file throw
r@ write-file throw r@ write-file throw
s" \\x1b[0m'" r@ write-line throw s" \\x1b[0m'" r@ write-line throw
r> close-file throw r> close-file throw
@ -263,14 +347,21 @@
get-api-key get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" echo 'Fetching bootstrap script from " r@ write-file throw s" SERVICE_ID='" r@ write-file throw
2over r@ write-file throw 2over r@ write-file throw
s" ...' >&2" r@ write-line throw s" '" r@ write-line throw
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
2over r@ write-file throw get-public-key r@ write-file throw
s" /execute -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw s" '" r@ write-line throw
get-api-key r@ write-file throw s" SECRET_KEY='" r@ write-file throw
s" ' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}')" r@ write-line throw get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" echo 'Fetching bootstrap script from $SERVICE_ID...' >&2" r@ write-line throw
s" BODY='{\"command\":\"cat /tmp/bootstrap.sh\"}'" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/execute:$BODY\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\")" r@ write-line throw
s" STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty')" r@ write-line throw s" STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty')" r@ write-line throw
s" if [ -n \"$STDOUT\" ]; then" r@ write-line throw s" if [ -n \"$STDOUT\" ]; then" r@ write-line throw
2dup 0 0 d= if 2dup 0 0 d= if
@ -302,6 +393,12 @@
\ For now, just create the curl command that will be constructed by bash \ For now, just create the curl command that will be constructed by bash
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" PUBLIC_KEY='" r@ write-file throw
get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw
s" NAME=''; PORTS=''; DOMAINS=''; TYPE=''; BOOTSTRAP=''" r@ write-line throw s" NAME=''; PORTS=''; DOMAINS=''; TYPE=''; BOOTSTRAP=''" r@ write-line throw
s" for ((i=3; i<$#; i+=2)); do" r@ write-line throw s" for ((i=3; i<$#; i+=2)); do" r@ write-line throw
s" case ${!i} in" r@ write-line throw s" case ${!i} in" r@ write-line throw
@ -318,9 +415,10 @@
s" [ -n \"$DOMAINS\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg d \"$DOMAINS\" '. + {domains: ($d | split(\",\"))}')" r@ write-line throw s" [ -n \"$DOMAINS\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg d \"$DOMAINS\" '. + {domains: ($d | split(\",\"))}')" r@ write-line throw
s" [ -n \"$TYPE\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg t \"$TYPE\" '. + {service_type: $t}')" r@ write-line throw s" [ -n \"$TYPE\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg t \"$TYPE\" '. + {service_type: $t}')" r@ write-line throw
s" [ -n \"$BOOTSTRAP\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg b \"$BOOTSTRAP\" '. + {bootstrap: $b}')" r@ write-line throw s" [ -n \"$BOOTSTRAP\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg b \"$BOOTSTRAP\" '. + {bootstrap: $b}')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw s" TIMESTAMP=$(date +%s)" r@ write-line throw
get-api-key r@ write-file throw s" MESSAGE=\"$TIMESTAMP:POST:/services:$PAYLOAD\"" r@ write-line throw
s" ' -d \"$PAYLOAD\" | jq ." r@ write-line throw s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/services -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$PAYLOAD\" | jq ." r@ write-line throw
r> close-file throw r> close-file throw
s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
; ;
@ -330,17 +428,24 @@
get-api-key get-api-key
s" /tmp/unsandbox_key_cmd.sh" w/o create-file throw >r s" /tmp/unsandbox_key_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw s" #!/bin/bash" r@ write-line throw
s" API_KEY='" r@ write-file throw s" PUBLIC_KEY='" r@ write-file throw
get-api-key r@ write-file throw get-public-key r@ write-file throw
s" '" r@ write-line throw
s" SECRET_KEY='" r@ write-file throw
get-secret-key r@ write-file throw
s" '" r@ write-line throw s" '" r@ write-line throw
s" PORTAL_BASE='" r@ write-file throw s" PORTAL_BASE='" r@ write-file throw
portal-base r@ write-file throw portal-base r@ write-file throw
s" '" r@ write-line throw s" '" r@ write-line throw
s" BODY='{}'" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/keys/validate:$BODY\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
\ Check if extend flag is set \ Check if extend flag is set
0= if 0= if
\ Normal validation \ Normal validation
s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw
s" STATUS=$?" r@ write-line throw s" STATUS=$?" r@ write-line throw
s" if [ $STATUS -ne 0 ]; then" r@ write-line throw s" if [ $STATUS -ne 0 ]; then" r@ write-line throw
s" echo -e '\\x1b[31mInvalid\\x1b[0m'" r@ write-line throw s" echo -e '\\x1b[31mInvalid\\x1b[0m'" r@ write-line throw
@ -369,9 +474,9 @@
s" rm -f /tmp/unsandbox_key_resp.json" r@ write-line throw s" rm -f /tmp/unsandbox_key_resp.json" r@ write-line throw
else else
\ Extend mode \ Extend mode
s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\")" r@ write-line throw s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\")" r@ write-line throw
s" PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw s" FETCHED_PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw
s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$PUBLIC_KEY\" 2>/dev/null" r@ write-line throw s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$FETCHED_PUBLIC_KEY\" 2>/dev/null" r@ write-line throw
then then
r> close-file throw r> close-file throw

78
un.fs
View file

@ -44,6 +44,7 @@ open System
open System.IO open System.IO
open System.Net open System.Net
open System.Text open System.Text
open System.Security.Cryptography
let apiBase = "https://api.unsandbox.com" let apiBase = "https://api.unsandbox.com"
let portalBase = "https://unsandbox.com" let portalBase = "https://unsandbox.com"
@ -101,12 +102,19 @@ type Args = {
mutable KeyExtend: bool mutable KeyExtend: bool
} }
let getApiKey (argsKey: string option) = let getApiKeys (argsKey: string option) =
let key = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY") let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
if String.IsNullOrEmpty(key) then let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
eprintfn "%sError: UNSANDBOX_API_KEY not set%s" red reset
exit 1 // Fall back to UNSANDBOX_API_KEY for backwards compatibility
key if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
if String.IsNullOrEmpty(legacyKey) then
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
exit 1
(legacyKey, null)
else
(publicKey, secretKey)
let detectLanguage (filename: string) = let detectLanguage (filename: string) =
let dotIndex = filename.LastIndexOf('.') let dotIndex = filename.LastIndexOf('.')
@ -222,19 +230,35 @@ let parseJson (json: string) =
result |> Seq.map (fun (k, v) -> k, v) |> Map.ofSeq result |> Seq.map (fun (k, v) -> k, v) |> Map.ofSeq
let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (apiKey: string) = let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- method request.Method <- method
request.ContentType <- "application/json" request.ContentType <- "application/json"
request.Headers.Add("Authorization", sprintf "Bearer %s" apiKey)
request.Timeout <- 300000 request.Timeout <- 300000
let body = match data with | Some d -> toJson (box d) | None -> ""
// Add HMAC authentication headers if secretKey is provided
if not (String.IsNullOrEmpty(secretKey)) then
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let message = sprintf "%d:%s:%s:%s" timestamp method endpoint body
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
request.Headers.Add("X-Timestamp", timestamp.ToString())
request.Headers.Add("X-Signature", signature)
else
// Legacy API key authentication
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
match data with match data with
| Some d -> | Some d ->
let json = toJson (box d) let bytes = Encoding.UTF8.GetBytes(body)
let bytes = Encoding.UTF8.GetBytes(json)
request.ContentLength <- int64 bytes.Length request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream() use stream = request.GetRequestStream()
stream.Write(bytes, 0, bytes.Length) stream.Write(bytes, 0, bytes.Length)
@ -258,7 +282,7 @@ let apiRequest (endpoint: string) (method: string) (data: (string * obj) list op
failwithf "HTTP error - %s" errorMsg failwithf "HTTP error - %s" errorMsg
let cmdExecute (args: Args) = let cmdExecute (args: Args) =
let apiKey = getApiKey args.ApiKey let (publicKey, secretKey) = getApiKeys args.ApiKey
let code = File.ReadAllText(args.SourceFile.Value) let code = File.ReadAllText(args.SourceFile.Value)
let language = detectLanguage args.SourceFile.Value let language = detectLanguage args.SourceFile.Value
@ -286,7 +310,7 @@ let cmdExecute (args: Args) =
if args.Vcpu > 0 then if args.Vcpu > 0 then
payload <- payload @ [("vcpu", box args.Vcpu)] payload <- payload @ [("vcpu", box args.Vcpu)]
let result = apiRequest "/execute" "POST" (Some payload) apiKey let result = apiRequest "/execute" "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with match result.TryFind "stdout" with
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) -> | Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
@ -307,14 +331,14 @@ let cmdExecute (args: Args) =
exit exitCode exit exitCode
let cmdSession (args: Args) = let cmdSession (args: Args) =
let apiKey = getApiKey args.ApiKey let (publicKey, secretKey) = getApiKeys args.ApiKey
if args.SessionList then if args.SessionList then
let result = apiRequest "/sessions" "GET" None apiKey let result = apiRequest "/sessions" "GET" None publicKey secretKey
printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created" printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created"
printfn "No sessions (list parsing not implemented)" printfn "No sessions (list parsing not implemented)"
elif args.SessionKill.IsSome then elif args.SessionKill.IsSome then
let result = apiRequest (sprintf "/sessions/%s" args.SessionKill.Value) "DELETE" None apiKey let result = apiRequest (sprintf "/sessions/%s" args.SessionKill.Value) "DELETE" None publicKey secretKey
printfn "%sSession terminated: %s%s" green args.SessionKill.Value reset printfn "%sSession terminated: %s%s" green args.SessionKill.Value reset
else else
let mutable payload = [("shell", box (match args.SessionShell with | Some s -> s | None -> "bash"))] let mutable payload = [("shell", box (match args.SessionShell with | Some s -> s | None -> "bash"))]
@ -324,7 +348,7 @@ let cmdSession (args: Args) =
payload <- payload @ [("vcpu", box args.Vcpu)] payload <- payload @ [("vcpu", box args.Vcpu)]
printfn "%sCreating session...%s" yellow reset printfn "%sCreating session...%s" yellow reset
let result = apiRequest "/sessions" "POST" (Some payload) apiKey let result = apiRequest "/sessions" "POST" (Some payload) publicKey secretKey
match result.TryFind "id" with match result.TryFind "id" with
| Some id -> printfn "%sSession created: %s%s" green (id.ToString()) reset | Some id -> printfn "%sSession created: %s%s" green (id.ToString()) reset
| None -> printfn "%sSession created%s" green reset | None -> printfn "%sSession created%s" green reset
@ -412,37 +436,37 @@ let cmdKey (args: Args) =
exit 1 exit 1
let cmdService (args: Args) = let cmdService (args: Args) =
let apiKey = getApiKey args.ApiKey let (publicKey, secretKey) = getApiKeys args.ApiKey
if args.ServiceList then if args.ServiceList then
let result = apiRequest "/services" "GET" None apiKey let result = apiRequest "/services" "GET" None publicKey secretKey
printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains" printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains"
printfn "No services (list parsing not implemented)" printfn "No services (list parsing not implemented)"
elif args.ServiceInfo.IsSome then elif args.ServiceInfo.IsSome then
let result = apiRequest (sprintf "/services/%s" args.ServiceInfo.Value) "GET" None apiKey let result = apiRequest (sprintf "/services/%s" args.ServiceInfo.Value) "GET" None publicKey secretKey
printfn "%s" (toJson (box result)) printfn "%s" (toJson (box result))
elif args.ServiceLogs.IsSome then elif args.ServiceLogs.IsSome then
let result = apiRequest (sprintf "/services/%s/logs" args.ServiceLogs.Value) "GET" None apiKey let result = apiRequest (sprintf "/services/%s/logs" args.ServiceLogs.Value) "GET" None publicKey secretKey
match result.TryFind "logs" with match result.TryFind "logs" with
| Some logs -> printfn "%s" (logs.ToString()) | Some logs -> printfn "%s" (logs.ToString())
| None -> () | None -> ()
elif args.ServiceTail.IsSome then elif args.ServiceTail.IsSome then
let result = apiRequest (sprintf "/services/%s/logs?lines=9000" args.ServiceTail.Value) "GET" None apiKey let result = apiRequest (sprintf "/services/%s/logs?lines=9000" args.ServiceTail.Value) "GET" None publicKey secretKey
match result.TryFind "logs" with match result.TryFind "logs" with
| Some logs -> printfn "%s" (logs.ToString()) | Some logs -> printfn "%s" (logs.ToString())
| None -> () | None -> ()
elif args.ServiceSleep.IsSome then elif args.ServiceSleep.IsSome then
let result = apiRequest (sprintf "/services/%s/sleep" args.ServiceSleep.Value) "POST" None apiKey let result = apiRequest (sprintf "/services/%s/sleep" args.ServiceSleep.Value) "POST" None publicKey secretKey
printfn "%sService sleeping: %s%s" green args.ServiceSleep.Value reset printfn "%sService sleeping: %s%s" green args.ServiceSleep.Value reset
elif args.ServiceWake.IsSome then elif args.ServiceWake.IsSome then
let result = apiRequest (sprintf "/services/%s/wake" args.ServiceWake.Value) "POST" None apiKey let result = apiRequest (sprintf "/services/%s/wake" args.ServiceWake.Value) "POST" None publicKey secretKey
printfn "%sService waking: %s%s" green args.ServiceWake.Value reset printfn "%sService waking: %s%s" green args.ServiceWake.Value reset
elif args.ServiceDestroy.IsSome then elif args.ServiceDestroy.IsSome then
let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None apiKey let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None publicKey secretKey
printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset
elif args.ServiceExecute.IsSome then elif args.ServiceExecute.IsSome then
let payload = [("command", box args.ServiceCommand.Value)] let payload = [("command", box args.ServiceCommand.Value)]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) apiKey let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with match result.TryFind "stdout" with
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) -> | Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
printf "%s%s%s" blue (stdout.ToString()) reset printf "%s%s%s" blue (stdout.ToString()) reset
@ -454,7 +478,7 @@ let cmdService (args: Args) =
elif args.ServiceDumpBootstrap.IsSome then elif args.ServiceDumpBootstrap.IsSome then
eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value
let payload = [("command", box "cat /tmp/bootstrap.sh")] let payload = [("command", box "cat /tmp/bootstrap.sh")]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) apiKey let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with match result.TryFind "stdout" with
| Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) -> | Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) ->
@ -485,7 +509,7 @@ let cmdService (args: Args) =
if args.Vcpu > 0 then if args.Vcpu > 0 then
payload <- payload @ [("vcpu", box args.Vcpu)] payload <- payload @ [("vcpu", box args.Vcpu)]
let result = apiRequest "/services" "POST" (Some payload) apiKey let result = apiRequest "/services" "POST" (Some payload) publicKey secretKey
match result.TryFind "id" with match result.TryFind "id" with
| Some id -> printfn "%sService created: %s%s" green (id.ToString()) reset | Some id -> printfn "%sService created: %s%s" green (id.ToString()) reset
| None -> printfn "%sService created%s" green reset | None -> printfn "%sService created%s" green reset

109
un.go
View file

@ -47,7 +47,10 @@ package main
import ( import (
"bytes" "bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -133,21 +136,38 @@ func detectLanguage(filename string) (string, error) {
return "", fmt.Errorf("cannot detect language from extension") return "", fmt.Errorf("cannot detect language from extension")
} }
func getAPIKey(keyArg string) string { func getAPIKeys(keyArg string) (string, string) {
if keyArg != "" { publicKey := os.Getenv("UNSANDBOX_PUBLIC_KEY")
return keyArg secretKey := os.Getenv("UNSANDBOX_SECRET_KEY")
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if publicKey == "" || secretKey == "" {
fallbackKey := keyArg
if fallbackKey == "" {
fallbackKey = os.Getenv("UNSANDBOX_API_KEY")
}
if fallbackKey == "" {
fmt.Fprintf(os.Stderr, "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)%s\n", Red, Reset)
os.Exit(1)
}
// Use fallback key as both public and secret for backwards compatibility
return fallbackKey, fallbackKey
} }
key := os.Getenv("UNSANDBOX_API_KEY")
if key == "" { return publicKey, secretKey
fmt.Fprintf(os.Stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", Red, Reset)
os.Exit(1)
}
return key
} }
func apiRequest(endpoint, method string, data map[string]interface{}, apiKey string) map[string]interface{} { func computeHMAC(secretKey, timestamp, method, path, body string) string {
message := fmt.Sprintf("%s:%s:%s:%s", timestamp, method, path, body)
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
func apiRequest(endpoint, method string, data map[string]interface{}, publicKey, secretKey string) map[string]interface{} {
url := APIBase + endpoint url := APIBase + endpoint
var reqBody io.Reader var reqBody io.Reader
bodyStr := ""
if data != nil { if data != nil {
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
@ -155,6 +175,7 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
fmt.Fprintf(os.Stderr, "%sError marshaling JSON: %v%s\n", Red, err, Reset) fmt.Fprintf(os.Stderr, "%sError marshaling JSON: %v%s\n", Red, err, Reset)
os.Exit(1) os.Exit(1)
} }
bodyStr = string(jsonData)
reqBody = bytes.NewBuffer(jsonData) reqBody = bytes.NewBuffer(jsonData)
} }
@ -164,7 +185,13 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
os.Exit(1) os.Exit(1)
} }
req.Header.Set("Authorization", "Bearer "+apiKey) // HMAC authentication
timestamp := fmt.Sprintf("%d", time.Now().Unix())
signature := computeHMAC(secretKey, timestamp, method, endpoint, bodyStr)
req.Header.Set("Authorization", "Bearer "+publicKey)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Signature", signature)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client := &http.Client{} client := &http.Client{}
@ -195,7 +222,7 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
return result return result
} }
func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts bool, outputDir, network string, vcpu int, apiKey string) { func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts bool, outputDir, network string, vcpu int, publicKey, secretKey string) {
code, err := os.ReadFile(sourceFile) code, err := os.ReadFile(sourceFile)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%sError reading file: %v%s\n", Red, err, Reset) fmt.Fprintf(os.Stderr, "%sError reading file: %v%s\n", Red, err, Reset)
@ -254,7 +281,7 @@ func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts boo
payload["vcpu"] = vcpu payload["vcpu"] = vcpu
} }
result := apiRequest("/execute", "POST", payload, apiKey) result := apiRequest("/execute", "POST", payload, publicKey, secretKey)
// Print output // Print output
if stdout, ok := result["stdout"].(string); ok && stdout != "" { if stdout, ok := result["stdout"].(string); ok && stdout != "" {
@ -294,9 +321,9 @@ func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts boo
os.Exit(exitCode) os.Exit(exitCode)
} }
func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, apiKey string) { func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, publicKey, secretKey string) {
if sessionList != "" { if sessionList != "" {
result := apiRequest("/sessions", "GET", nil, apiKey) result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
sessions := result["sessions"].([]interface{}) sessions := result["sessions"].([]interface{})
if len(sessions) == 0 { if len(sessions) == 0 {
fmt.Println("No active sessions") fmt.Println("No active sessions")
@ -312,7 +339,7 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
} }
if sessionKill != "" { if sessionKill != "" {
apiRequest("/sessions/"+sessionKill, "DELETE", nil, apiKey) apiRequest("/sessions/"+sessionKill, "DELETE", nil, publicKey, secretKey)
fmt.Printf("%sSession terminated: %s%s\n", Green, sessionKill, Reset) fmt.Printf("%sSession terminated: %s%s\n", Green, sessionKill, Reset)
return return
} }
@ -338,13 +365,13 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
} }
fmt.Printf("%sCreating session...%s\n", Yellow, Reset) fmt.Printf("%sCreating session...%s\n", Yellow, Reset)
result := apiRequest("/sessions", "POST", payload, apiKey) result := apiRequest("/sessions", "POST", payload, publicKey, secretKey)
fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset) fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset)
} }
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, network string, vcpu int, apiKey string) { func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, network string, vcpu int, publicKey, secretKey string) {
if serviceList != "" { if serviceList != "" {
result := apiRequest("/services", "GET", nil, apiKey) result := apiRequest("/services", "GET", nil, publicKey, secretKey)
services := result["services"].([]interface{}) services := result["services"].([]interface{})
if len(services) == 0 { if len(services) == 0 {
fmt.Println("No services") fmt.Println("No services")
@ -376,45 +403,45 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
} }
if serviceInfo != "" { if serviceInfo != "" {
result := apiRequest("/services/"+serviceInfo, "GET", nil, apiKey) result := apiRequest("/services/"+serviceInfo, "GET", nil, publicKey, secretKey)
jsonData, _ := json.MarshalIndent(result, "", " ") jsonData, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(jsonData)) fmt.Println(string(jsonData))
return return
} }
if serviceLogs != "" { if serviceLogs != "" {
result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, apiKey) result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, publicKey, secretKey)
fmt.Print(result["logs"]) fmt.Print(result["logs"])
return return
} }
if serviceTail != "" { if serviceTail != "" {
result := apiRequest("/services/"+serviceTail+"/logs?lines=9000", "GET", nil, apiKey) result := apiRequest("/services/"+serviceTail+"/logs?lines=9000", "GET", nil, publicKey, secretKey)
fmt.Print(result["logs"]) fmt.Print(result["logs"])
return return
} }
if serviceSleep != "" { if serviceSleep != "" {
apiRequest("/services/"+serviceSleep+"/sleep", "POST", nil, apiKey) apiRequest("/services/"+serviceSleep+"/sleep", "POST", nil, publicKey, secretKey)
fmt.Printf("%sService sleeping: %s%s\n", Green, serviceSleep, Reset) fmt.Printf("%sService sleeping: %s%s\n", Green, serviceSleep, Reset)
return return
} }
if serviceWake != "" { if serviceWake != "" {
apiRequest("/services/"+serviceWake+"/wake", "POST", nil, apiKey) apiRequest("/services/"+serviceWake+"/wake", "POST", nil, publicKey, secretKey)
fmt.Printf("%sService waking: %s%s\n", Green, serviceWake, Reset) fmt.Printf("%sService waking: %s%s\n", Green, serviceWake, Reset)
return return
} }
if serviceDestroy != "" { if serviceDestroy != "" {
apiRequest("/services/"+serviceDestroy, "DELETE", nil, apiKey) apiRequest("/services/"+serviceDestroy, "DELETE", nil, publicKey, secretKey)
fmt.Printf("%sService destroyed: %s%s\n", Green, serviceDestroy, Reset) fmt.Printf("%sService destroyed: %s%s\n", Green, serviceDestroy, Reset)
return return
} }
if serviceExecute != "" { if serviceExecute != "" {
payload := map[string]interface{}{"command": serviceCommand} payload := map[string]interface{}{"command": serviceCommand}
result := apiRequest("/services/"+serviceExecute+"/execute", "POST", payload, apiKey) result := apiRequest("/services/"+serviceExecute+"/execute", "POST", payload, publicKey, secretKey)
if stdout, ok := result["stdout"].(string); ok { if stdout, ok := result["stdout"].(string); ok {
fmt.Printf("%s%s%s", Blue, stdout, Reset) fmt.Printf("%s%s%s", Blue, stdout, Reset)
} }
@ -427,7 +454,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
if serviceDumpBootstrap != "" { if serviceDumpBootstrap != "" {
fmt.Fprintf(os.Stderr, "Fetching bootstrap script from %s...\n", serviceDumpBootstrap) fmt.Fprintf(os.Stderr, "Fetching bootstrap script from %s...\n", serviceDumpBootstrap)
payload := map[string]interface{}{"command": "cat /tmp/bootstrap.sh"} payload := map[string]interface{}{"command": "cat /tmp/bootstrap.sh"}
result := apiRequest("/services/"+serviceDumpBootstrap+"/execute", "POST", payload, apiKey) result := apiRequest("/services/"+serviceDumpBootstrap+"/execute", "POST", payload, publicKey, secretKey)
if bootstrap, ok := result["stdout"].(string); ok && bootstrap != "" { if bootstrap, ok := result["stdout"].(string); ok && bootstrap != "" {
if serviceDumpFile != "" { if serviceDumpFile != "" {
@ -482,7 +509,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
payload["vcpu"] = vcpu payload["vcpu"] = vcpu
} }
result := apiRequest("/services", "POST", payload, apiKey) result := apiRequest("/services", "POST", payload, publicKey, secretKey)
fmt.Printf("%sService created: %s%s\n", Green, result["id"], Reset) fmt.Printf("%sService created: %s%s\n", Green, result["id"], Reset)
fmt.Printf("Name: %s\n", result["name"]) fmt.Printf("Name: %s\n", result["name"])
if url, ok := result["url"]; ok { if url, ok := result["url"]; ok {
@ -524,7 +551,7 @@ func formatDuration(d time.Duration) string {
} }
} }
func validateKey(apiKey string, extend bool) { func validateKey(publicKey, secretKey string, extend bool) {
url := PortalBase + "/keys/validate" url := PortalBase + "/keys/validate"
reqBody := bytes.NewBuffer(nil) reqBody := bytes.NewBuffer(nil)
@ -534,7 +561,13 @@ func validateKey(apiKey string, extend bool) {
os.Exit(1) os.Exit(1)
} }
req.Header.Set("Authorization", "Bearer "+apiKey) // HMAC authentication
timestamp := fmt.Sprintf("%d", time.Now().Unix())
signature := computeHMAC(secretKey, timestamp, "POST", "/keys/validate", "")
req.Header.Set("Authorization", "Bearer "+publicKey)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Signature", signature)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client := &http.Client{} client := &http.Client{}
@ -702,7 +735,7 @@ func main() {
switch os.Args[1] { switch os.Args[1] {
case "session": case "session":
sessionCmd.Parse(os.Args[2:]) sessionCmd.Parse(os.Args[2:])
key := getAPIKey(*sessionKey) publicKey, secretKey := getAPIKeys(*sessionKey)
net := *sessionNetwork net := *sessionNetwork
if net == "" { if net == "" {
net = *network net = *network
@ -711,12 +744,12 @@ func main() {
if vc == 0 { if vc == 0 {
vc = *vcpu vc = *vcpu
} }
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, key) cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, publicKey, secretKey)
return return
case "service": case "service":
serviceCmd.Parse(os.Args[2:]) serviceCmd.Parse(os.Args[2:])
key := getAPIKey(*serviceKey) publicKey, secretKey := getAPIKeys(*serviceKey)
net := *serviceNetwork net := *serviceNetwork
if net == "" { if net == "" {
net = *network net = *network
@ -725,13 +758,13 @@ func main() {
if vc == 0 { if vc == 0 {
vc = *vcpu vc = *vcpu
} }
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, key) cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, publicKey, secretKey)
return return
case "key": case "key":
keyCmd.Parse(os.Args[2:]) keyCmd.Parse(os.Args[2:])
key := getAPIKey(*keyKey) publicKey, secretKey := getAPIKeys(*keyKey)
validateKey(key, *keyExtend) validateKey(publicKey, secretKey, *keyExtend)
return return
} }
} }
@ -746,6 +779,6 @@ func main() {
} }
sourceFile := flag.Arg(0) sourceFile := flag.Arg(0)
key := getAPIKey(*apiKey) publicKey, secretKey := getAPIKeys(*apiKey)
cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, key) cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, publicKey, secretKey)
} }

104
un.groovy
View file

@ -92,13 +92,24 @@ class Args {
Boolean keyExtend = false Boolean keyExtend = false
} }
def getApiKey(argsKey) { import javax.crypto.Mac
def key = argsKey ?: System.getenv('UNSANDBOX_API_KEY') import javax.crypto.spec.SecretKeySpec
if (!key) {
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}") def getApiKeys(argsKey) {
System.exit(1) def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY')
def secretKey = System.getenv('UNSANDBOX_SECRET_KEY')
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (!publicKey || !secretKey) {
def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
if (!legacyKey) {
System.err.println("${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}")
System.exit(1)
}
return [legacyKey, null]
} }
return key
return [publicKey, secretKey]
} }
def detectLanguage(filename) { def detectLanguage(filename) {
@ -116,16 +127,33 @@ def detectLanguage(filename) {
return language return language
} }
def apiRequest(endpoint, method, data, apiKey) { def apiRequest(endpoint, method, data, publicKey, secretKey) {
def tempFile = File.createTempFile('un_request_', '.json') def tempFile = File.createTempFile('un_request_', '.json')
try { try {
def body = data ?: ""
if (data) { if (data) {
tempFile.text = data tempFile.text = data
} }
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}", def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', 'Content-Type: application/json', '-H', 'Content-Type: application/json']
'-H', "Authorization: Bearer ${apiKey}"]
// Add HMAC authentication headers if secretKey is provided
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:${method}:${endpoint}:${body}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
// Legacy API key authentication
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
if (data) { if (data) {
curlCmd += ['-d', "@${tempFile.absolutePath}"] curlCmd += ['-d', "@${tempFile.absolutePath}"]
@ -147,7 +175,7 @@ def apiRequest(endpoint, method, data, apiKey) {
} }
def cmdExecute(args) { def cmdExecute(args) {
def apiKey = getApiKey(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey)
def file = new File(args.sourceFile) def file = new File(args.sourceFile)
if (!file.exists()) { if (!file.exists()) {
System.err.println("${RED}Error: File not found: ${args.sourceFile}${RESET}") System.err.println("${RED}Error: File not found: ${args.sourceFile}${RESET}")
@ -203,7 +231,7 @@ def cmdExecute(args) {
json += '}' json += '}'
def output = apiRequest('/execute', 'POST', json, apiKey) def output = apiRequest('/execute', 'POST', json, publicKey, secretKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\]|\\.)*)"/ def stdoutMatch = output =~ /"stdout":"((?:[^"\\]|\\.)*)"/
def stderrMatch = output =~ /"stderr":"((?:[^"\\]|\\.)*)"/ def stderrMatch = output =~ /"stderr":"((?:[^"\\]|\\.)*)"/
@ -245,17 +273,17 @@ def cmdExecute(args) {
} }
def cmdSession(args) { def cmdSession(args) {
def apiKey = getApiKey(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.sessionList) { if (args.sessionList) {
def output = apiRequest('/sessions', 'GET', null, apiKey) def output = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created")) println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created"))
println("No sessions (list parsing not implemented)") println("No sessions (list parsing not implemented)")
return return
} }
if (args.sessionKill) { if (args.sessionKill) {
apiRequest("/sessions/${args.sessionKill}", 'DELETE', null, apiKey) apiRequest("/sessions/${args.sessionKill}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}") println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
return return
} }
@ -270,7 +298,7 @@ def cmdSession(args) {
json += '}' json += '}'
println("${YELLOW}Creating session...${RESET}") println("${YELLOW}Creating session...${RESET}")
def output = apiRequest('/sessions', 'POST', json, apiKey) def output = apiRequest('/sessions', 'POST', json, publicKey, secretKey)
def idMatch = output =~ /"id":"([^"]+)"/ def idMatch = output =~ /"id":"([^"]+)"/
if (idMatch.find()) { if (idMatch.find()) {
println("${GREEN}Session created: ${idMatch.group(1)}${RESET}") println("${GREEN}Session created: ${idMatch.group(1)}${RESET}")
@ -296,12 +324,28 @@ def openBrowser(url) {
} }
def cmdKey(args) { def cmdKey(args) {
def apiKey = getApiKey(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey)
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate", def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
'-H', 'Content-Type: application/json', '-H', 'Content-Type: application/json']
'-H', "Authorization: Bearer ${apiKey}",
'-d', '{}'] // Add HMAC authentication headers if secretKey is provided
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:POST:/keys/validate:{}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
curlCmd += ['-d', '{}']
def proc = curlCmd.execute() def proc = curlCmd.execute()
def output = proc.text def output = proc.text
@ -361,23 +405,23 @@ def cmdKey(args) {
} }
def cmdService(args) { def cmdService(args) {
def apiKey = getApiKey(args.apiKey) def (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.serviceList) { if (args.serviceList) {
def output = apiRequest('/services', 'GET', null, apiKey) def output = apiRequest('/services', 'GET', null, publicKey, secretKey)
println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains")) println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains"))
println("No services (list parsing not implemented)") println("No services (list parsing not implemented)")
return return
} }
if (args.serviceInfo) { if (args.serviceInfo) {
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, apiKey) def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, publicKey, secretKey)
println(output) println(output)
return return
} }
if (args.serviceLogs) { if (args.serviceLogs) {
def output = apiRequest("/services/${args.serviceLogs}/logs", 'GET', null, apiKey) def output = apiRequest("/services/${args.serviceLogs}/logs", 'GET', null, publicKey, secretKey)
def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/ def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/
if (logsMatch.find()) { if (logsMatch.find()) {
println(logsMatch.group(1).replace('\\n', '\n')) println(logsMatch.group(1).replace('\\n', '\n'))
@ -386,7 +430,7 @@ def cmdService(args) {
} }
if (args.serviceTail) { if (args.serviceTail) {
def output = apiRequest("/services/${args.serviceTail}/logs?lines=9000", 'GET', null, apiKey) def output = apiRequest("/services/${args.serviceTail}/logs?lines=9000", 'GET', null, publicKey, secretKey)
def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/ def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/
if (logsMatch.find()) { if (logsMatch.find()) {
println(logsMatch.group(1).replace('\\n', '\n')) println(logsMatch.group(1).replace('\\n', '\n'))
@ -395,26 +439,26 @@ def cmdService(args) {
} }
if (args.serviceSleep) { if (args.serviceSleep) {
apiRequest("/services/${args.serviceSleep}/sleep", 'POST', null, apiKey) apiRequest("/services/${args.serviceSleep}/sleep", 'POST', null, publicKey, secretKey)
println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}") println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}")
return return
} }
if (args.serviceWake) { if (args.serviceWake) {
apiRequest("/services/${args.serviceWake}/wake", 'POST', null, apiKey) apiRequest("/services/${args.serviceWake}/wake", 'POST', null, publicKey, secretKey)
println("${GREEN}Service waking: ${args.serviceWake}${RESET}") println("${GREEN}Service waking: ${args.serviceWake}${RESET}")
return return
} }
if (args.serviceDestroy) { if (args.serviceDestroy) {
apiRequest("/services/${args.serviceDestroy}", 'DELETE', null, apiKey) apiRequest("/services/${args.serviceDestroy}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}") println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
return return
} }
if (args.serviceExecute) { if (args.serviceExecute) {
def json = """{"command":"${args.serviceCommand}"}""" def json = """{"command":"${args.serviceCommand}"}"""
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, apiKey) def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, publicKey, secretKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/ def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
def stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/ def stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/
@ -441,7 +485,7 @@ def cmdService(args) {
if (args.serviceDumpBootstrap) { if (args.serviceDumpBootstrap) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...") System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
def json = """{"command":"cat /tmp/bootstrap.sh"}""" def json = """{"command":"cat /tmp/bootstrap.sh"}"""
def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, apiKey) def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, publicKey, secretKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/ def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
if (stdoutMatch.find()) { if (stdoutMatch.find()) {
@ -491,7 +535,7 @@ def cmdService(args) {
} }
json += '}' json += '}'
def output = apiRequest('/services', 'POST', json, apiKey) def output = apiRequest('/services', 'POST', json, publicKey, secretKey)
def idMatch = output =~ /"id":"([^"]+)"/ def idMatch = output =~ /"id":"([^"]+)"/
if (idMatch.find()) { if (idMatch.find()) {
println("${GREEN}Service created: ${idMatch.group(1)}${RESET}") println("${GREEN}Service created: ${idMatch.group(1)}${RESET}")

101
un.hs
View file

@ -62,11 +62,15 @@ import System.Process (readProcessWithExitCode)
import System.IO (hPutStrLn, stderr) import System.IO (hPutStrLn, stderr)
import System.Directory (createDirectoryIfMissing, setPermissions, getPermissions, setOwnerExecutable) import System.Directory (createDirectoryIfMissing, setPermissions, getPermissions, setOwnerExecutable)
import Data.List (isPrefixOf, intercalate) import Data.List (isPrefixOf, intercalate)
import Data.Char (isDigit) import Data.Char (isDigit, ord)
import Text.Printf (printf) import Text.Printf (printf)
import Control.Monad (when, unless, forM_) import Control.Monad (when, unless, forM_)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import Crypto.Hash.SHA256 (hmac)
import Numeric (showHex)
import Data.Time.Clock.POSIX (getPOSIXTime)
-- API constants -- API constants
apiBase :: String apiBase :: String
@ -382,39 +386,77 @@ serviceCommand opts = do
-- HTTP helpers using curl -- HTTP helpers using curl
curlPost :: String -> String -> String -> IO (ExitCode, String, String) curlPost :: String -> String -> String -> IO (ExitCode, String, String)
curlPost apiKey url body = do curlPost apiKey url body = do
(publicKey, secretKey) <- getApiKeys
-- Extract path from URL
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "POST" path body
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl" (exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
[ "-s", "-X", "POST" ([ "-s", "-X", "POST"
, url , url
, "-H", "Content-Type: application/json" , "-H", "Content-Type: application/json"
, "-H", "Authorization: Bearer " ++ apiKey ] ++ authHeaders ++ ["-d", body]) ""
, "-d", body
] ""
return (exitCode, stdout, stderr) return (exitCode, stdout, stderr)
curlGet :: String -> String -> IO (ExitCode, String, String) curlGet :: String -> String -> IO (ExitCode, String, String)
curlGet apiKey url = curlGet apiKey url = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "GET" path ""
readProcessWithExitCode "curl" readProcessWithExitCode "curl"
[ "-s", url ([ "-s", url ] ++ authHeaders) ""
, "-H", "Authorization: Bearer " ++ apiKey
] ""
curlDelete :: String -> String -> IO (ExitCode, String, String) curlDelete :: String -> String -> IO (ExitCode, String, String)
curlDelete apiKey url = curlDelete apiKey url = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "DELETE" path ""
readProcessWithExitCode "curl" readProcessWithExitCode "curl"
[ "-s", "-X", "DELETE" ([ "-s", "-X", "DELETE", url ] ++ authHeaders) ""
, url
, "-H", "Authorization: Bearer " ++ apiKey -- Get API keys from environment
] "" getApiKeys :: IO (String, Maybe String)
getApiKeys = do
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
apiKey <- lookupEnv "UNSANDBOX_API_KEY"
case (publicKey, secretKey, apiKey) of
(Just pk, Just sk, _) -> return (pk, Just sk)
(_, _, Just ak) -> return (ak, Nothing)
_ -> do
hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"
exitFailure
-- Get API key from environment
getApiKey :: IO String getApiKey :: IO String
getApiKey = do getApiKey = do
maybeKey <- lookupEnv "UNSANDBOX_API_KEY" (publicKey, _) <- getApiKeys
case maybeKey of return publicKey
Nothing -> do
hPutStrLn stderr "Error: UNSANDBOX_API_KEY not set" -- HMAC-SHA256
exitFailure hmacSha256 :: String -> String -> String
Just key -> return key hmacSha256 secret message =
let secretBS = BSC.pack secret
messageBS = BSC.pack message
mac = hmac secretBS messageBS
in concatMap (printf "%02x") (BS.unpack mac)
makeSignature :: String -> String -> String -> String -> String -> String
makeSignature secretKey timestamp method path body =
let message = timestamp ++ ":" ++ method ++ ":" ++ path ++ ":" ++ body
in hmacSha256 secretKey message
buildAuthHeaders :: String -> Maybe String -> String -> String -> String -> IO [String]
buildAuthHeaders publicKey maybeSecretKey method path body =
case maybeSecretKey of
Just secretKey -> do
now <- getPOSIXTime
let timestamp = show (floor now :: Integer)
let signature = makeSignature secretKey timestamp method path body
return [ "-H", "Authorization: Bearer " ++ publicKey
, "-H", "X-Timestamp: " ++ timestamp
, "-H", "X-Signature: " ++ signature
]
Nothing ->
return ["-H", "Authorization: Bearer " ++ publicKey]
-- Parse exit code from JSON response -- Parse exit code from JSON response
parseExitCode :: String -> Int parseExitCode :: String -> Int
@ -565,11 +607,12 @@ extendKey apiKey = do
-- HTTP helper for portal API -- HTTP helper for portal API
curlPostPortal :: String -> String -> String -> IO (ExitCode, String, String) curlPostPortal :: String -> String -> String -> IO (ExitCode, String, String)
curlPostPortal apiKey url body = do curlPostPortal apiKey url body = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length portalBase) url
authHeaders <- buildAuthHeaders publicKey secretKey "POST" path body
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl" (exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
[ "-s", "-X", "POST" ([ "-s", "-X", "POST"
, url , url
, "-H", "Content-Type: application/json" , "-H", "Content-Type: application/json"
, "-H", "Authorization: Bearer " ++ apiKey ] ++ authHeaders ++ ["-d", body]) ""
, "-d", body
] ""
return (exitCode, stdout, stderr) return (exitCode, stdout, stderr)

83
un.jl
View file

@ -42,6 +42,7 @@ using JSON
using Base64 using Base64
using ArgParse using ArgParse
using Printf using Printf
using SHA
# Extension to language mapping # Extension to language mapping
const EXT_MAP = Dict( const EXT_MAP = Dict(
@ -75,19 +76,54 @@ function detect_language(filename::String)::String
return get(EXT_MAP, ext, "unknown") return get(EXT_MAP, ext, "unknown")
end end
function get_api_key(args_key=nothing)::String function get_api_keys(args_key=nothing)::Tuple{String,String}
key = something(args_key, get(ENV, "UNSANDBOX_API_KEY", "")) # Try new-style keys first
if isempty(key) public_key = something(args_key, get(ENV, "UNSANDBOX_PUBLIC_KEY", ""))
println(stderr, "$(RED)Error: UNSANDBOX_API_KEY not set$(RESET)") secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
# Fall back to old-style single key for backwards compatibility
if isempty(public_key)
old_key = get(ENV, "UNSANDBOX_API_KEY", "")
if isempty(old_key)
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)")
exit(1)
end
# Old-style: use same key for both public and secret
return (old_key, old_key)
end
if isempty(secret_key)
println(stderr, "$(RED)Error: UNSANDBOX_SECRET_KEY not set$(RESET)")
exit(1) exit(1)
end end
return key
return (public_key, secret_key)
end end
function api_request(endpoint::String, api_key::String; method="GET", data=nothing) function hmac_sha256_hex(key::String, message::String)::String
h = hmac_sha256(Vector{UInt8}(key), Vector{UInt8}(message))
return bytes2hex(h)
end
function compute_signature(secret_key::String, timestamp::Int64, method::String, path::String, body::String)::String
message = "$(timestamp):$(method):$(path):$(body)"
return hmac_sha256_hex(secret_key, message)
end
function api_request(endpoint::String, public_key::String, secret_key::String; method="GET", data=nothing)
url = API_BASE * endpoint url = API_BASE * endpoint
# Prepare body
body = data !== nothing ? JSON.json(data) : ""
# Generate timestamp and signature
timestamp = Int64(floor(time()))
signature = compute_signature(secret_key, timestamp, method, endpoint, body)
headers = [ headers = [
"Authorization" => "Bearer $api_key", "Authorization" => "Bearer $public_key",
"X-Timestamp" => string(timestamp),
"X-Signature" => signature,
"Content-Type" => "application/json" "Content-Type" => "application/json"
] ]
@ -95,7 +131,6 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
if method == "GET" if method == "GET"
response = HTTP.get(url, headers, readtimeout=300) response = HTTP.get(url, headers, readtimeout=300)
elseif method == "POST" elseif method == "POST"
body = data !== nothing ? JSON.json(data) : ""
response = HTTP.post(url, headers, body, readtimeout=300) response = HTTP.post(url, headers, body, readtimeout=300)
elseif method == "DELETE" elseif method == "DELETE"
response = HTTP.delete(url, headers, readtimeout=300) response = HTTP.delete(url, headers, readtimeout=300)
@ -115,7 +150,7 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
end end
function cmd_execute(args) function cmd_execute(args)
api_key = get_api_key(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"])
filename = args["source_file"] filename = args["source_file"]
if !isfile(filename) if !isfile(filename)
@ -176,7 +211,7 @@ function cmd_execute(args)
end end
# Execute # Execute
result = api_request("/execute", api_key, method="POST", data=payload) result = api_request("/execute", public_key, secret_key, method="POST", data=payload)
# Print output # Print output
if haskey(result, "stdout") && !isempty(result["stdout"]) if haskey(result, "stdout") && !isempty(result["stdout"])
@ -205,10 +240,10 @@ function cmd_execute(args)
end end
function cmd_session(args) function cmd_session(args)
api_key = get_api_key(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"])
if args["list"] if args["list"]
result = api_request("/sessions", api_key) result = api_request("/sessions", public_key, secret_key)
sessions = get(result, "sessions", []) sessions = get(result, "sessions", [])
if isempty(sessions) if isempty(sessions)
println("No active sessions") println("No active sessions")
@ -226,7 +261,7 @@ function cmd_session(args)
end end
if args["kill"] !== nothing if args["kill"] !== nothing
api_request("/sessions/$(args["kill"])", api_key, method="DELETE") api_request("/sessions/$(args["kill"])", public_key, secret_key, method="DELETE")
println("$(GREEN)Session terminated: $(args["kill"])$(RESET)") println("$(GREEN)Session terminated: $(args["kill"])$(RESET)")
return return
end end
@ -236,10 +271,10 @@ function cmd_session(args)
end end
function cmd_service(args) function cmd_service(args)
api_key = get_api_key(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"])
if args["list"] if args["list"]
result = api_request("/services", api_key) result = api_request("/services", public_key, secret_key)
services = get(result, "services", []) services = get(result, "services", [])
if isempty(services) if isempty(services)
println("No services") println("No services")
@ -259,31 +294,31 @@ function cmd_service(args)
end end
if args["info"] !== nothing if args["info"] !== nothing
result = api_request("/services/$(args["info"])", api_key) result = api_request("/services/$(args["info"])", public_key, secret_key)
println(JSON.json(result, 2)) println(JSON.json(result, 2))
return return
end end
if args["logs"] !== nothing if args["logs"] !== nothing
result = api_request("/services/$(args["logs"])/logs", api_key) result = api_request("/services/$(args["logs"])/logs", public_key, secret_key)
println(get(result, "logs", "")) println(get(result, "logs", ""))
return return
end end
if args["sleep"] !== nothing if args["sleep"] !== nothing
api_request("/services/$(args["sleep"])/sleep", api_key, method="POST") api_request("/services/$(args["sleep"])/sleep", public_key, secret_key, method="POST")
println("$(GREEN)Service sleeping: $(args["sleep"])$(RESET)") println("$(GREEN)Service sleeping: $(args["sleep"])$(RESET)")
return return
end end
if args["wake"] !== nothing if args["wake"] !== nothing
api_request("/services/$(args["wake"])/wake", api_key, method="POST") api_request("/services/$(args["wake"])/wake", public_key, secret_key, method="POST")
println("$(GREEN)Service waking: $(args["wake"])$(RESET)") println("$(GREEN)Service waking: $(args["wake"])$(RESET)")
return return
end end
if args["destroy"] !== nothing if args["destroy"] !== nothing
api_request("/services/$(args["destroy"])", api_key, method="DELETE") api_request("/services/$(args["destroy"])", public_key, secret_key, method="DELETE")
println("$(GREEN)Service destroyed: $(args["destroy"])$(RESET)") println("$(GREEN)Service destroyed: $(args["destroy"])$(RESET)")
return return
end end
@ -291,7 +326,7 @@ function cmd_service(args)
if args["dump-bootstrap"] !== nothing if args["dump-bootstrap"] !== nothing
println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...") println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...")
payload = Dict("command" => "cat /tmp/bootstrap.sh") payload = Dict("command" => "cat /tmp/bootstrap.sh")
result = api_request("/services/$(args["dump-bootstrap"])/execute", api_key, method="POST", data=payload) result = api_request("/services/$(args["dump-bootstrap"])/execute", public_key, secret_key, method="POST", data=payload)
if haskey(result, "stdout") && !isempty(result["stdout"]) if haskey(result, "stdout") && !isempty(result["stdout"])
bootstrap = result["stdout"] bootstrap = result["stdout"]
@ -351,7 +386,7 @@ function cmd_service(args)
payload["vcpu"] = args["vcpu"] payload["vcpu"] = args["vcpu"]
end end
result = api_request("/services", api_key, method="POST", data=payload) result = api_request("/services", public_key, secret_key, method="POST", data=payload)
println("$(GREEN)Service created: $(get(result, "id", "N/A"))$(RESET)") println("$(GREEN)Service created: $(get(result, "id", "N/A"))$(RESET)")
println("Name: $(get(result, "name", "N/A"))") println("Name: $(get(result, "name", "N/A"))")
if haskey(result, "url") if haskey(result, "url")
@ -452,7 +487,9 @@ function validate_key(api_key::String)
end end
function cmd_key(args) function cmd_key(args)
api_key = get_api_key(args["api-key"]) (public_key, secret_key) = get_api_keys(args["api-key"])
# For portal validation, we still use public_key as bearer token
api_key = public_key
# Handle --extend flag # Handle --extend flag
if args["extend"] if args["extend"]

129
un.js
View file

@ -55,6 +55,7 @@ const fs = require('fs');
const https = require('https'); const https = require('https');
const path = require('path'); const path = require('path');
const { exec } = require('child_process'); const { exec } = require('child_process');
const crypto = require('crypto');
const API_BASE = "https://api.unsandbox.com"; const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com"; const PORTAL_BASE = "https://unsandbox.com";
@ -80,13 +81,25 @@ const EXT_MAP = {
".tcl": "tcl", ".raku": "raku", ".m": "objc", ".tcl": "tcl", ".raku": "raku", ".m": "objc",
}; };
function getApiKey(argsKey) { function getApiKeys(argsKey) {
const key = argsKey || process.env.UNSANDBOX_API_KEY; // Try new split key format first
if (!key) { let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`); let secretKey = process.env.UNSANDBOX_SECRET_KEY;
process.exit(1);
// Fall back to old single key format for backwards compatibility
if (!publicKey || !secretKey) {
const oldKey = argsKey || process.env.UNSANDBOX_API_KEY;
if (oldKey) {
publicKey = oldKey;
secretKey = oldKey;
} else {
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
process.exit(1);
}
} }
return key;
return { publicKey, secretKey };
} }
function detectLanguage(filename) { function detectLanguage(filename) {
@ -111,32 +124,45 @@ function detectLanguage(filename) {
return lang; return lang;
} }
function apiRequest(endpoint, method = "GET", data = null, apiKey = null) { function apiRequest(endpoint, method = "GET", data = null, publicKey = null, secretKey = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(API_BASE + endpoint); const url = new URL(API_BASE + endpoint);
// Prepare body
const body = data ? JSON.stringify(data) : "";
// Generate HMAC signature
const timestamp = Math.floor(Date.now() / 1000).toString();
const signatureInput = `${timestamp}:${method}:${endpoint}:${body}`;
const signature = crypto.createHmac('sha256', secretKey)
.update(signatureInput)
.digest('hex');
const options = { const options = {
hostname: url.hostname, hostname: url.hostname,
path: url.pathname + url.search, path: url.pathname + url.search,
method: method, method: method,
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 300000 timeout: 300000
}; };
const req = https.request(options, (res) => { const req = https.request(options, (res) => {
let body = ''; let responseBody = '';
res.on('data', chunk => body += chunk); res.on('data', chunk => responseBody += chunk);
res.on('end', () => { res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 200 && res.statusCode < 300) {
try { try {
resolve(JSON.parse(body)); resolve(JSON.parse(responseBody));
} catch (e) { } catch (e) {
resolve(body); resolve(responseBody);
} }
} else { } else {
console.error(`${RED}Error: HTTP ${res.statusCode} - ${body}${RESET}`); console.error(`${RED}Error: HTTP ${res.statusCode} - ${responseBody}${RESET}`);
process.exit(1); process.exit(1);
} }
}); });
@ -148,42 +174,55 @@ function apiRequest(endpoint, method = "GET", data = null, apiKey = null) {
}); });
if (data) { if (data) {
req.write(JSON.stringify(data)); req.write(body);
} }
req.end(); req.end();
}); });
} }
function portalRequest(endpoint, method = "GET", data = null, apiKey = null) { function portalRequest(endpoint, method = "GET", data = null, publicKey = null, secretKey = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(PORTAL_BASE + endpoint); const url = new URL(PORTAL_BASE + endpoint);
// Prepare body
const body = data ? JSON.stringify(data) : "";
// Generate HMAC signature
const timestamp = Math.floor(Date.now() / 1000).toString();
const signatureInput = `${timestamp}:${method}:${endpoint}:${body}`;
const signature = crypto.createHmac('sha256', secretKey)
.update(signatureInput)
.digest('hex');
const options = { const options = {
hostname: url.hostname, hostname: url.hostname,
path: url.pathname + url.search, path: url.pathname + url.search,
method: method, method: method,
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 30000 timeout: 30000
}; };
const req = https.request(options, (res) => { const req = https.request(options, (res) => {
let body = ''; let responseBody = '';
res.on('data', chunk => body += chunk); res.on('data', chunk => responseBody += chunk);
res.on('end', () => { res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 200 && res.statusCode < 300) {
try { try {
resolve(JSON.parse(body)); resolve(JSON.parse(responseBody));
} catch (e) { } catch (e) {
resolve(body); resolve(responseBody);
} }
} else { } else {
try { try {
const errorBody = JSON.parse(body); const errorBody = JSON.parse(responseBody);
resolve({ error: errorBody.error || body, status: res.statusCode }); resolve({ error: errorBody.error || responseBody, status: res.statusCode });
} catch (e) { } catch (e) {
resolve({ error: body, status: res.statusCode }); resolve({ error: responseBody, status: res.statusCode });
} }
} }
}); });
@ -194,7 +233,7 @@ function portalRequest(endpoint, method = "GET", data = null, apiKey = null) {
}); });
if (data) { if (data) {
req.write(JSON.stringify(data)); req.write(body);
} }
req.end(); req.end();
}); });
@ -220,9 +259,9 @@ function openBrowser(url) {
}); });
} }
async function validateKey(apiKey, shouldExtend = false) { async function validateKey(publicKey, secretKey, shouldExtend = false) {
try { try {
const result = await portalRequest("/keys/validate", "POST", {}, apiKey); const result = await portalRequest("/keys/validate", "POST", {}, publicKey, secretKey);
// Handle --extend flag first // Handle --extend flag first
if (shouldExtend) { if (shouldExtend) {
@ -265,12 +304,12 @@ async function validateKey(apiKey, shouldExtend = false) {
} }
async function cmdKey(args) { async function cmdKey(args) {
const apiKey = getApiKey(args.apiKey); const { publicKey, secretKey } = getApiKeys(args.apiKey);
await validateKey(apiKey, args.extend); await validateKey(publicKey, secretKey, args.extend);
} }
async function cmdExecute(args) { async function cmdExecute(args) {
const apiKey = getApiKey(args.apiKey); const { publicKey, secretKey } = getApiKeys(args.apiKey);
let code; let code;
try { try {
@ -312,7 +351,7 @@ async function cmdExecute(args) {
if (args.network) payload.network = args.network; if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu; if (args.vcpu) payload.vcpu = args.vcpu;
const result = await apiRequest("/execute", "POST", payload, apiKey); const result = await apiRequest("/execute", "POST", payload, publicKey, secretKey);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`); if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`); if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
@ -334,10 +373,10 @@ async function cmdExecute(args) {
} }
async function cmdSession(args) { async function cmdSession(args) {
const apiKey = getApiKey(args.apiKey); const { publicKey, secretKey } = getApiKeys(args.apiKey);
if (args.list) { if (args.list) {
const result = await apiRequest("/sessions", "GET", null, apiKey); const result = await apiRequest("/sessions", "GET", null, publicKey, secretKey);
const sessions = result.sessions || []; const sessions = result.sessions || [];
if (sessions.length === 0) { if (sessions.length === 0) {
console.log("No active sessions"); console.log("No active sessions");
@ -351,7 +390,7 @@ async function cmdSession(args) {
} }
if (args.kill) { if (args.kill) {
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, apiKey); await apiRequest(`/sessions/${args.kill}`, "DELETE", null, publicKey, secretKey);
console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`); console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`);
return; return;
} }
@ -370,16 +409,16 @@ async function cmdSession(args) {
if (args.audit) payload.audit = true; if (args.audit) payload.audit = true;
console.log(`${YELLOW}Creating session...${RESET}`); console.log(`${YELLOW}Creating session...${RESET}`);
const result = await apiRequest("/sessions", "POST", payload, apiKey); const result = await apiRequest("/sessions", "POST", payload, publicKey, secretKey);
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`); console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`); console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
} }
async function cmdService(args) { async function cmdService(args) {
const apiKey = getApiKey(args.apiKey); const { publicKey, secretKey } = getApiKeys(args.apiKey);
if (args.list) { if (args.list) {
const result = await apiRequest("/services", "GET", null, apiKey); const result = await apiRequest("/services", "GET", null, publicKey, secretKey);
const services = result.services || []; const services = result.services || [];
if (services.length === 0) { if (services.length === 0) {
console.log("No services"); console.log("No services");
@ -395,44 +434,44 @@ async function cmdService(args) {
} }
if (args.info) { if (args.info) {
const result = await apiRequest(`/services/${args.info}`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.info}`, "GET", null, publicKey, secretKey);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
return; return;
} }
if (args.logs) { if (args.logs) {
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, publicKey, secretKey);
console.log(result.logs || ""); console.log(result.logs || "");
return; return;
} }
if (args.tail) { if (args.tail) {
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, publicKey, secretKey);
console.log(result.logs || ""); console.log(result.logs || "");
return; return;
} }
if (args.sleep) { if (args.sleep) {
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, apiKey); await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, publicKey, secretKey);
console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`); console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`);
return; return;
} }
if (args.wake) { if (args.wake) {
await apiRequest(`/services/${args.wake}/wake`, "POST", null, apiKey); await apiRequest(`/services/${args.wake}/wake`, "POST", null, publicKey, secretKey);
console.log(`${GREEN}Service waking: ${args.wake}${RESET}`); console.log(`${GREEN}Service waking: ${args.wake}${RESET}`);
return; return;
} }
if (args.destroy) { if (args.destroy) {
await apiRequest(`/services/${args.destroy}`, "DELETE", null, apiKey); await apiRequest(`/services/${args.destroy}`, "DELETE", null, publicKey, secretKey);
console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`); console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`);
return; return;
} }
if (args.execute) { if (args.execute) {
const payload = { command: args.command }; const payload = { command: args.command };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, apiKey); const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, publicKey, secretKey);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`); if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`); if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
return; return;
@ -441,7 +480,7 @@ async function cmdService(args) {
if (args.dumpBootstrap) { if (args.dumpBootstrap) {
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`); console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
const payload = { command: "cat /tmp/bootstrap.sh" }; const payload = { command: "cat /tmp/bootstrap.sh" };
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, apiKey); const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, publicKey, secretKey);
if (result.stdout) { if (result.stdout) {
const bootstrap = result.stdout; const bootstrap = result.stdout;
@ -481,7 +520,7 @@ async function cmdService(args) {
if (args.network) payload.network = args.network; if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu; if (args.vcpu) payload.vcpu = args.vcpu;
const result = await apiRequest("/services", "POST", payload, apiKey); const result = await apiRequest("/services", "POST", payload, publicKey, secretKey);
console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`); console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`);
console.log(`Name: ${result.name || 'N/A'}`); console.log(`Name: ${result.name || 'N/A'}`);
if (result.url) console.log(`URL: ${result.url}`); if (result.url) console.log(`URL: ${result.url}`);

117
un.kt
View file

@ -45,6 +45,8 @@ import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.Base64 import java.util.Base64
import kotlin.system.exitProcess import kotlin.system.exitProcess
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
val API_BASE = "https://api.unsandbox.com" val API_BASE = "https://api.unsandbox.com"
val PORTAL_BASE = "https://unsandbox.com" val PORTAL_BASE = "https://unsandbox.com"
@ -123,7 +125,7 @@ fun main(args: Array<String>) {
} }
fun cmdExecute(args: Args) { fun cmdExecute(args: Args) {
val apiKey = getApiKey(args.apiKey) val (publicKey, secretKey) = getApiKeys(args.apiKey)
val code = File(args.sourceFile!!).readText() val code = File(args.sourceFile!!).readText()
val language = detectLanguage(args.sourceFile!!) val language = detectLanguage(args.sourceFile!!)
@ -167,7 +169,7 @@ fun cmdExecute(args: Args) {
payload["vcpu"] = args.vcpu payload["vcpu"] = args.vcpu
} }
val result = apiRequest("/execute", "POST", payload, apiKey) val result = apiRequest("/execute", "POST", payload, publicKey, secretKey)
val stdout = result["stdout"] as? String val stdout = result["stdout"] as? String
val stderr = result["stderr"] as? String val stderr = result["stderr"] as? String
@ -198,10 +200,10 @@ fun cmdExecute(args: Args) {
} }
fun cmdSession(args: Args) { fun cmdSession(args: Args) {
val apiKey = getApiKey(args.apiKey) val (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.sessionList) { if (args.sessionList) {
val result = apiRequest("/sessions", "GET", null, apiKey) val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val sessions = result["sessions"] as? List<Map<String, Any>> val sessions = result["sessions"] as? List<Map<String, Any>>
if (sessions.isNullOrEmpty()) { if (sessions.isNullOrEmpty()) {
@ -221,7 +223,7 @@ fun cmdSession(args: Args) {
} }
if (args.sessionKill != null) { if (args.sessionKill != null) {
apiRequest("/sessions/${args.sessionKill}", "DELETE", null, apiKey) apiRequest("/sessions/${args.sessionKill}", "DELETE", null, publicKey, secretKey)
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}") println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
return return
} }
@ -237,16 +239,16 @@ fun cmdSession(args: Args) {
} }
println("${YELLOW}Creating session...${RESET}") println("${YELLOW}Creating session...${RESET}")
val result = apiRequest("/sessions", "POST", payload, apiKey) val result = apiRequest("/sessions", "POST", payload, publicKey, secretKey)
println("${GREEN}Session created: ${result["id"] ?: "N/A"}${RESET}") println("${GREEN}Session created: ${result["id"] ?: "N/A"}${RESET}")
println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}") println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}")
} }
fun cmdService(args: Args) { fun cmdService(args: Args) {
val apiKey = getApiKey(args.apiKey) val (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.serviceList) { if (args.serviceList) {
val result = apiRequest("/services", "GET", null, apiKey) val result = apiRequest("/services", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val services = result["services"] as? List<Map<String, Any>> val services = result["services"] as? List<Map<String, Any>>
if (services.isNullOrEmpty()) { if (services.isNullOrEmpty()) {
@ -270,44 +272,44 @@ fun cmdService(args: Args) {
} }
if (args.serviceInfo != null) { if (args.serviceInfo != null) {
val result = apiRequest("/services/${args.serviceInfo}", "GET", null, apiKey) val result = apiRequest("/services/${args.serviceInfo}", "GET", null, publicKey, secretKey)
println(toJson(result)) println(toJson(result))
return return
} }
if (args.serviceLogs != null) { if (args.serviceLogs != null) {
val result = apiRequest("/services/${args.serviceLogs}/logs", "GET", null, apiKey) val result = apiRequest("/services/${args.serviceLogs}/logs", "GET", null, publicKey, secretKey)
println(result["logs"] ?: "") println(result["logs"] ?: "")
return return
} }
if (args.serviceTail != null) { if (args.serviceTail != null) {
val result = apiRequest("/services/${args.serviceTail}/logs?lines=9000", "GET", null, apiKey) val result = apiRequest("/services/${args.serviceTail}/logs?lines=9000", "GET", null, publicKey, secretKey)
println(result["logs"] ?: "") println(result["logs"] ?: "")
return return
} }
if (args.serviceSleep != null) { if (args.serviceSleep != null) {
apiRequest("/services/${args.serviceSleep}/sleep", "POST", null, apiKey) apiRequest("/services/${args.serviceSleep}/sleep", "POST", null, publicKey, secretKey)
println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}") println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}")
return return
} }
if (args.serviceWake != null) { if (args.serviceWake != null) {
apiRequest("/services/${args.serviceWake}/wake", "POST", null, apiKey) apiRequest("/services/${args.serviceWake}/wake", "POST", null, publicKey, secretKey)
println("${GREEN}Service waking: ${args.serviceWake}${RESET}") println("${GREEN}Service waking: ${args.serviceWake}${RESET}")
return return
} }
if (args.serviceDestroy != null) { if (args.serviceDestroy != null) {
apiRequest("/services/${args.serviceDestroy}", "DELETE", null, apiKey) apiRequest("/services/${args.serviceDestroy}", "DELETE", null, publicKey, secretKey)
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}") println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
return return
} }
if (args.serviceExecute != null) { if (args.serviceExecute != null) {
val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!) val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!)
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, apiKey) val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, publicKey, secretKey)
if (result.containsKey("stdout")) { if (result.containsKey("stdout")) {
val stdout = result["stdout"] as? String val stdout = result["stdout"] as? String
if (stdout != null && stdout.isNotEmpty()) { if (stdout != null && stdout.isNotEmpty()) {
@ -326,7 +328,7 @@ fun cmdService(args: Args) {
if (args.serviceDumpBootstrap != null) { if (args.serviceDumpBootstrap != null) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...") System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
val payload = mutableMapOf<String, Any>("command" to "cat /tmp/bootstrap.sh") val payload = mutableMapOf<String, Any>("command" to "cat /tmp/bootstrap.sh")
val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, apiKey) val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, publicKey, secretKey)
val bootstrap = result["stdout"] as? String val bootstrap = result["stdout"] as? String
if (bootstrap != null && bootstrap.isNotEmpty()) { if (bootstrap != null && bootstrap.isNotEmpty()) {
@ -368,7 +370,7 @@ fun cmdService(args: Args) {
payload["vcpu"] = args.vcpu payload["vcpu"] = args.vcpu
} }
val result = apiRequest("/services", "POST", payload, apiKey) val result = apiRequest("/services", "POST", payload, publicKey, secretKey)
println("${GREEN}Service created: ${result["id"] ?: "N/A"}${RESET}") println("${GREEN}Service created: ${result["id"] ?: "N/A"}${RESET}")
println("Name: ${result["name"] ?: "N/A"}") println("Name: ${result["name"] ?: "N/A"}")
if (result.containsKey("url")) { if (result.containsKey("url")) {
@ -382,21 +384,21 @@ fun cmdService(args: Args) {
} }
fun cmdKey(args: Args) { fun cmdKey(args: Args) {
val apiKey = getApiKey(args.apiKey) val (publicKey, secretKey) = getApiKeys(args.apiKey)
val result = validateKey(apiKey) val result = validateKey(publicKey, secretKey)
val valid = result["valid"] as? Boolean ?: false val valid = result["valid"] as? Boolean ?: false
val expired = result["expired"] as? Boolean ?: false val expired = result["expired"] as? Boolean ?: false
val publicKey = result["public_key"] as? String ?: "" val pubKey = result["public_key"] as? String ?: ""
val tier = result["tier"] as? String ?: "" val tier = result["tier"] as? String ?: ""
val expiresAt = result["expires_at"] as? String ?: "" val expiresAt = result["expires_at"] as? String ?: ""
if (args.keyExtend) { if (args.keyExtend) {
if (publicKey.isEmpty()) { if (pubKey.isEmpty()) {
System.err.println("${RED}Error: Could not retrieve public key${RESET}") System.err.println("${RED}Error: Could not retrieve public key${RESET}")
exitProcess(1) exitProcess(1)
} }
val extendUrl = "$PORTAL_BASE/keys/extend?pk=$publicKey" val extendUrl = "$PORTAL_BASE/keys/extend?pk=$pubKey"
println("${YELLOW}Opening browser to extend key...${RESET}") println("${YELLOW}Opening browser to extend key...${RESET}")
println(extendUrl) println(extendUrl)
@ -418,13 +420,13 @@ fun cmdKey(args: Args) {
if (expired) { if (expired) {
println("${RED}Status: Expired${RESET}") println("${RED}Status: Expired${RESET}")
println("Public Key: $publicKey") println("Public Key: $pubKey")
println("Tier: $tier") println("Tier: $tier")
println("Expired: $expiresAt") println("Expired: $expiresAt")
println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}") println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}")
} else if (valid) { } else if (valid) {
println("${GREEN}Status: Valid${RESET}") println("${GREEN}Status: Valid${RESET}")
println("Public Key: $publicKey") println("Public Key: $pubKey")
println("Tier: $tier") println("Tier: $tier")
println("Expires: $expiresAt") println("Expires: $expiresAt")
} else { } else {
@ -433,12 +435,21 @@ fun cmdKey(args: Args) {
} }
} }
fun validateKey(apiKey: String): Map<String, Any> { fun validateKey(publicKey: String?, secretKey: String): Map<String, Any> {
val url = URL("$PORTAL_BASE/keys/validate") val timestamp = System.currentTimeMillis() / 1000
val method = "POST"
val path = "/keys/validate"
val body = ""
val signatureData = "$timestamp:$method:$path:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL("$PORTAL_BASE$path")
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST" connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer $apiKey") connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json") connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000 connection.connectTimeout = 30000
connection.readTimeout = 30000 connection.readTimeout = 30000
@ -452,13 +463,39 @@ fun validateKey(apiKey: String): Map<String, Any> {
return parseJson(response) return parseJson(response)
} }
fun getApiKey(argsKey: String?): String { fun getApiKeys(argsKey: String?): Pair<String?, String> {
val key = argsKey ?: System.getenv("UNSANDBOX_API_KEY") var publicKey: String? = null
if (key.isNullOrEmpty()) { var secretKey: String? = null
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}")
if (argsKey != null) {
secretKey = argsKey
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
} else {
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
if (publicKey == null || secretKey == null) {
val apiKey = System.getenv("UNSANDBOX_API_KEY")
if (apiKey != null && apiKey.isNotEmpty()) {
secretKey = apiKey
}
}
}
if (secretKey.isNullOrEmpty()) {
System.err.println("${RED}Error: UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set${RESET}")
exitProcess(1) exitProcess(1)
} }
return key
return Pair(publicKey, secretKey)
}
fun hmacSha256(secretKey: String, data: String): String {
val mac = Mac.getInstance("HmacSHA256")
val keySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(keySpec)
val hash = mac.doFinal(data.toByteArray(Charsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
} }
fun detectLanguage(filename: String): String { fun detectLanguage(filename: String): String {
@ -469,20 +506,26 @@ fun detectLanguage(filename: String): String {
return EXT_MAP[".$ext"] ?: throw RuntimeException("Unsupported file extension: .$ext") return EXT_MAP[".$ext"] ?: throw RuntimeException("Unsupported file extension: .$ext")
} }
fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, apiKey: String): Map<String, Any> { fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, publicKey: String?, secretKey: String): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val body = if (data != null) toJson(data) else ""
val signatureData = "$timestamp:$method:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint) val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = method connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer $apiKey") connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json") connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000 connection.connectTimeout = 30000
connection.readTimeout = 300000 connection.readTimeout = 300000
if (data != null) { if (data != null) {
connection.doOutput = true connection.doOutput = true
val json = toJson(data) connection.outputStream.use { it.write(body.toByteArray()) }
connection.outputStream.use { it.write(json.toByteArray()) }
} }
if (connection.responseCode !in 200..299) { if (connection.responseCode !in 200..299) {

77
un.lisp
View file

@ -104,38 +104,75 @@
(defun curl-post (api-key endpoint json-data) (defun curl-post (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data))) (let ((tmp-file (write-temp-file json-data)))
(unwind-protect (unwind-protect
(run-curl (list "curl" "-s" "-X" "POST" (destructuring-bind (public-key secret-key) (get-api-keys)
(format nil "https://api.unsandbox.com~a" endpoint) (let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
"-H" "Content-Type: application/json" (base-args (list "curl" "-s" "-X" "POST"
"-H" (format nil "Authorization: Bearer ~a" api-key) (format nil "https://api.unsandbox.com~a" endpoint)
"-d" (format nil "@~a" tmp-file))) "-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(delete-file tmp-file)))) (delete-file tmp-file))))
(defun curl-get (api-key endpoint) (defun curl-get (api-key endpoint)
(run-curl (list "curl" "-s" (destructuring-bind (public-key secret-key) (get-api-keys)
(format nil "https://api.unsandbox.com~a" endpoint) (let ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
"-H" (format nil "Authorization: Bearer ~a" api-key)))) (base-args (list "curl" "-s"
(format nil "https://api.unsandbox.com~a" endpoint))))
(run-curl (append base-args auth-headers)))))
(defun curl-delete (api-key endpoint) (defun curl-delete (api-key endpoint)
(run-curl (list "curl" "-s" "-X" "DELETE" (destructuring-bind (public-key secret-key) (get-api-keys)
(format nil "https://api.unsandbox.com~a" endpoint) (let ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
"-H" (format nil "Authorization: Bearer ~a" api-key)))) (base-args (list "curl" "-s" "-X" "DELETE"
(format nil "https://api.unsandbox.com~a" endpoint))))
(run-curl (append base-args auth-headers)))))
(defun curl-post-portal (api-key endpoint json-data) (defun curl-post-portal (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data))) (let ((tmp-file (write-temp-file json-data)))
(unwind-protect (unwind-protect
(run-curl (list "curl" "-s" "-X" "POST" (destructuring-bind (public-key secret-key) (get-api-keys)
(format nil "~a~a" *portal-base* endpoint) (let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
"-H" "Content-Type: application/json" (base-args (list "curl" "-s" "-X" "POST"
"-H" (format nil "Authorization: Bearer ~a" api-key) (format nil "~a~a" *portal-base* endpoint)
"-d" (format nil "@~a" tmp-file))) "-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(delete-file tmp-file)))) (delete-file tmp-file))))
(defun get-api-keys ()
(let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY"))
(secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY"))
(api-key (uiop:getenv "UNSANDBOX_API_KEY")))
(cond
((and public-key secret-key) (list public-key secret-key))
(api-key (list api-key nil))
(t (progn
(format t "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
(uiop:quit 1))))))
(defun get-api-key () (defun get-api-key ()
(or (uiop:getenv "UNSANDBOX_API_KEY") (first (get-api-keys)))
(progn
(format t "Error: UNSANDBOX_API_KEY not set~%") (defun hmac-sha256 (secret message)
(uiop:quit 1)))) "Compute HMAC-SHA256 using openssl command"
(let* ((secret-escaped (uiop:escape-sh-token secret))
(message-escaped (uiop:escape-sh-token message))
(cmd (format nil "echo -n ~a | openssl dgst -sha256 -hmac ~a | awk '{print $2}'"
message-escaped secret-escaped))
(result (string-trim '(#\Space #\Tab #\Newline #\Return)
(uiop:run-program cmd :output :string))))
result))
(defun make-signature (secret-key timestamp method path body)
(let ((message (format nil "~a:~a:~a:~a" timestamp method path body)))
(hmac-sha256 secret-key message)))
(defun build-auth-headers (public-key secret-key method path body)
(if secret-key
(let* ((timestamp (write-to-string (floor (get-universal-time))))
(signature (make-signature secret-key timestamp method path body)))
(list "-H" (format nil "Authorization: Bearer ~a" public-key)
"-H" (format nil "X-Timestamp: ~a" timestamp)
"-H" (format nil "X-Signature: ~a" signature)))
(list "-H" (format nil "Authorization: Bearer ~a" public-key))))
(defun execute-cmd (file) (defun execute-cmd (file)
(let* ((api-key (get-api-key)) (let* ((api-key (get-api-key))

136
un.lua
View file

@ -76,13 +76,23 @@ local EXT_MAP = {
[".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc" [".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc"
} }
local function get_api_key(args_key) local function get_api_keys(args_key)
local key = args_key or os.getenv("UNSANDBOX_API_KEY") local public_key = os.getenv("UNSANDBOX_PUBLIC_KEY")
if not key then local secret_key = os.getenv("UNSANDBOX_SECRET_KEY")
io.stderr:write(RED .. "Error: UNSANDBOX_API_KEY not set" .. RESET .. "\n")
os.exit(1) if not public_key or not secret_key then
local old_key = args_key or os.getenv("UNSANDBOX_API_KEY")
if old_key then
public_key = old_key
secret_key = old_key
else
io.stderr:write(RED .. "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" .. RESET .. "\n")
io.stderr:write(RED .. " (or legacy UNSANDBOX_API_KEY for backwards compatibility)" .. RESET .. "\n")
os.exit(1)
end
end end
return key
return {public_key = public_key, secret_key = secret_key}
end end
local function detect_language(filename) local function detect_language(filename)
@ -115,20 +125,46 @@ local function shell_escape(str)
return "'" .. str:gsub("'", "'\\''") .. "'" return "'" .. str:gsub("'", "'\\''") .. "'"
end end
local function api_request(endpoint, method, data, api_key) local function api_request(endpoint, method, data, keys)
method = method or "GET" method = method or "GET"
local url = API_BASE .. endpoint local url = API_BASE .. endpoint
local tmpfile = os.tmpname() local tmpfile = os.tmpname()
-- Generate timestamp and signature
local timestamp = tostring(os.time())
local body = data and json.encode(data) or ""
-- Parse URL to get path
local path = endpoint
-- Create HMAC signature using openssl command
local message = timestamp .. ":" .. method .. ":" .. path .. ":" .. body
local sig_tmpfile = os.tmpname()
local msg_tmpfile = os.tmpname()
-- Write message to temp file
local f = io.open(msg_tmpfile, "w")
f:write(message)
f:close()
-- Generate HMAC using openssl
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
local sig_handle = io.popen(hmac_cmd)
local signature = sig_handle:read("*a"):gsub("%s+$", "")
sig_handle:close()
os.remove(msg_tmpfile)
local cmd = "curl -s -X " .. method .. " " .. shell_escape(url) .. local cmd = "curl -s -X " .. method .. " " .. shell_escape(url) ..
" -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'" " -H 'Content-Type: application/json'"
local data_file
if data then if data then
local payload = json.encode(data) data_file = os.tmpname()
local data_file = os.tmpname()
local f = io.open(data_file, "w") local f = io.open(data_file, "w")
f:write(payload) f:write(body)
f:close() f:close()
cmd = cmd .. " -d @" .. shell_escape(data_file) cmd = cmd .. " -d @" .. shell_escape(data_file)
end end
@ -144,7 +180,7 @@ local function api_request(endpoint, method, data, api_key)
file:close() file:close()
os.remove(tmpfile) os.remove(tmpfile)
if data then if data_file then
os.remove(data_file) os.remove(data_file)
end end
@ -198,7 +234,7 @@ local function base64_decode(data)
end end
local function cmd_execute(options) local function cmd_execute(options)
local api_key = get_api_key(options.api_key) local keys = get_api_keys(options.api_key)
local code = read_file(options.source_file) local code = read_file(options.source_file)
local language = detect_language(options.source_file) local language = detect_language(options.source_file)
@ -233,7 +269,7 @@ local function cmd_execute(options)
if options.network then payload.network = options.network end if options.network then payload.network = options.network end
if options.vcpu then payload.vcpu = options.vcpu end if options.vcpu then payload.vcpu = options.vcpu end
local result = api_request("/execute", "POST", payload, api_key) local result = api_request("/execute", "POST", payload, keys)
if result.stdout then if result.stdout then
io.write(BLUE .. result.stdout .. RESET) io.write(BLUE .. result.stdout .. RESET)
@ -261,10 +297,10 @@ local function cmd_execute(options)
end end
local function cmd_session(options) local function cmd_session(options)
local api_key = get_api_key(options.api_key) local keys = get_api_keys(options.api_key)
if options.list then if options.list then
local result = api_request("/sessions", "GET", nil, api_key) local result = api_request("/sessions", "GET", nil, keys)
local sessions = result.sessions or {} local sessions = result.sessions or {}
if #sessions == 0 then if #sessions == 0 then
print("No active sessions") print("No active sessions")
@ -280,7 +316,7 @@ local function cmd_session(options)
end end
if options.kill then if options.kill then
api_request("/sessions/" .. options.kill, "DELETE", nil, api_key) api_request("/sessions/" .. options.kill, "DELETE", nil, keys)
print(GREEN .. "Session terminated: " .. options.kill .. RESET) print(GREEN .. "Session terminated: " .. options.kill .. RESET)
return return
end end
@ -299,21 +335,39 @@ local function cmd_session(options)
if options.audit then payload.audit = true end if options.audit then payload.audit = true end
print(YELLOW .. "Creating session..." .. RESET) print(YELLOW .. "Creating session..." .. RESET)
local result = api_request("/sessions", "POST", payload, api_key) local result = api_request("/sessions", "POST", payload, keys)
print(GREEN .. "Session created: " .. (result.id or "N/A") .. RESET) print(GREEN .. "Session created: " .. (result.id or "N/A") .. RESET)
print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET) print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET)
end end
local function cmd_key(options) local function cmd_key(options)
local api_key = get_api_key(options.api_key) local keys = get_api_keys(options.api_key)
if options.extend then if options.extend then
-- Get public_key from validation response -- Get public_key from validation response
local url = PORTAL_BASE .. "/keys/validate" local url = PORTAL_BASE .. "/keys/validate"
local tmpfile = os.tmpname() local tmpfile = os.tmpname()
local timestamp = tostring(os.time())
local body = ""
local path = "/keys/validate"
local message = timestamp .. ":POST:" .. path .. ":" .. body
local msg_tmpfile = os.tmpname()
local f = io.open(msg_tmpfile, "w")
f:write(message)
f:close()
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
local sig_handle = io.popen(hmac_cmd)
local signature = sig_handle:read("*a"):gsub("%s+$", "")
sig_handle:close()
os.remove(msg_tmpfile)
local cmd = "curl -s -X POST " .. shell_escape(url) .. local cmd = "curl -s -X POST " .. shell_escape(url) ..
" -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'" .. " -H 'Content-Type: application/json'" ..
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile) " -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
@ -351,8 +405,26 @@ local function cmd_key(options)
local url = PORTAL_BASE .. "/keys/validate" local url = PORTAL_BASE .. "/keys/validate"
local tmpfile = os.tmpname() local tmpfile = os.tmpname()
local timestamp = tostring(os.time())
local body = ""
local path = "/keys/validate"
local message = timestamp .. ":POST:" .. path .. ":" .. body
local msg_tmpfile = os.tmpname()
local f = io.open(msg_tmpfile, "w")
f:write(message)
f:close()
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
local sig_handle = io.popen(hmac_cmd)
local signature = sig_handle:read("*a"):gsub("%s+$", "")
sig_handle:close()
os.remove(msg_tmpfile)
local cmd = "curl -s -X POST " .. shell_escape(url) .. local cmd = "curl -s -X POST " .. shell_escape(url) ..
" -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'" .. " -H 'Content-Type: application/json'" ..
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile) " -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
@ -390,10 +462,10 @@ local function cmd_key(options)
end end
local function cmd_service(options) local function cmd_service(options)
local api_key = get_api_key(options.api_key) local keys = get_api_keys(options.api_key)
if options.list then if options.list then
local result = api_request("/services", "GET", nil, api_key) local result = api_request("/services", "GET", nil, keys)
local services = result.services or {} local services = result.services or {}
if #services == 0 then if #services == 0 then
print("No services") print("No services")
@ -411,44 +483,44 @@ local function cmd_service(options)
end end
if options.info then if options.info then
local result = api_request("/services/" .. options.info, "GET", nil, api_key) local result = api_request("/services/" .. options.info, "GET", nil, keys)
print(json.encode(result)) print(json.encode(result))
return return
end end
if options.logs then if options.logs then
local result = api_request("/services/" .. options.logs .. "/logs", "GET", nil, api_key) local result = api_request("/services/" .. options.logs .. "/logs", "GET", nil, keys)
print(result.logs or "") print(result.logs or "")
return return
end end
if options.tail then if options.tail then
local result = api_request("/services/" .. options.tail .. "/logs?lines=9000", "GET", nil, api_key) local result = api_request("/services/" .. options.tail .. "/logs?lines=9000", "GET", nil, keys)
print(result.logs or "") print(result.logs or "")
return return
end end
if options.sleep then if options.sleep then
api_request("/services/" .. options.sleep .. "/sleep", "POST", nil, api_key) api_request("/services/" .. options.sleep .. "/sleep", "POST", nil, keys)
print(GREEN .. "Service sleeping: " .. options.sleep .. RESET) print(GREEN .. "Service sleeping: " .. options.sleep .. RESET)
return return
end end
if options.wake then if options.wake then
api_request("/services/" .. options.wake .. "/wake", "POST", nil, api_key) api_request("/services/" .. options.wake .. "/wake", "POST", nil, keys)
print(GREEN .. "Service waking: " .. options.wake .. RESET) print(GREEN .. "Service waking: " .. options.wake .. RESET)
return return
end end
if options.destroy then if options.destroy then
api_request("/services/" .. options.destroy, "DELETE", nil, api_key) api_request("/services/" .. options.destroy, "DELETE", nil, keys)
print(GREEN .. "Service destroyed: " .. options.destroy .. RESET) print(GREEN .. "Service destroyed: " .. options.destroy .. RESET)
return return
end end
if options.execute then if options.execute then
local payload = { command = options.command } local payload = { command = options.command }
local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, api_key) local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, keys)
if result.stdout then io.write(BLUE .. result.stdout .. RESET) end if result.stdout then io.write(BLUE .. result.stdout .. RESET) end
if result.stderr then io.stderr:write(RED .. result.stderr .. RESET) end if result.stderr then io.stderr:write(RED .. result.stderr .. RESET) end
return return
@ -457,7 +529,7 @@ local function cmd_service(options)
if options.dump_bootstrap then if options.dump_bootstrap then
io.stderr:write("Fetching bootstrap script from " .. options.dump_bootstrap .. "...\n") io.stderr:write("Fetching bootstrap script from " .. options.dump_bootstrap .. "...\n")
local payload = { command = "cat /tmp/bootstrap.sh" } local payload = { command = "cat /tmp/bootstrap.sh" }
local result = api_request("/services/" .. options.dump_bootstrap .. "/execute", "POST", payload, api_key) local result = api_request("/services/" .. options.dump_bootstrap .. "/execute", "POST", payload, keys)
if result.stdout then if result.stdout then
local bootstrap = result.stdout local bootstrap = result.stdout
@ -514,7 +586,7 @@ local function cmd_service(options)
if options.network then payload.network = options.network end if options.network then payload.network = options.network end
if options.vcpu then payload.vcpu = options.vcpu end if options.vcpu then payload.vcpu = options.vcpu end
local result = api_request("/services", "POST", payload, api_key) local result = api_request("/services", "POST", payload, keys)
print(GREEN .. "Service created: " .. (result.id or "N/A") .. RESET) print(GREEN .. "Service created: " .. (result.id or "N/A") .. RESET)
print("Name: " .. (result.name or "N/A")) print("Name: " .. (result.name or "N/A"))
if result.url then print("URL: " .. result.url) end if result.url then print("URL: " .. result.url) end

101
un.m
View file

@ -41,6 +41,7 @@
// Full-featured CLI matching un.c/un.py capabilities // Full-featured CLI matching un.c/un.py capabilities
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <CommonCrypto/CommonHMAC.h>
static NSString* API_BASE = @"https://api.unsandbox.com"; static NSString* API_BASE = @"https://api.unsandbox.com";
static NSString* PORTAL_BASE = @"https://unsandbox.com"; static NSString* PORTAL_BASE = @"https://unsandbox.com";
@ -69,13 +70,47 @@ NSDictionary* getExtMap() {
}; };
} }
NSString* getApiKey() { void getApiKeys(NSString** publicKey, NSString** secretKey) {
NSString* key = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"]; // Try new-style keys first
if (!key) { *publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", [RED UTF8String], [RESET UTF8String]); *secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
// Fall back to old-style single key
if (!*publicKey || [*publicKey length] == 0) {
NSString* oldKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
if (!oldKey) {
fprintf(stderr, "%sError: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
*publicKey = oldKey;
*secretKey = oldKey;
return;
}
if (!*secretKey || [*secretKey length] == 0) {
fprintf(stderr, "%sError: UNSANDBOX_SECRET_KEY not set%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1); exit(1);
} }
return key; }
NSString* hmacSha256Hex(NSString* key, NSString* message) {
const char* cKey = [key UTF8String];
const char* cMessage = [message UTF8String];
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cMessage, strlen(cMessage), digest);
NSMutableString* hex = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[hex appendFormat:@"%02x", digest[i]];
}
return hex;
}
NSString* computeSignature(NSString* secretKey, long timestamp, NSString* method, NSString* path, NSString* body) {
NSString* message = [NSString stringWithFormat:@"%ld:%@:%@:%@", timestamp, method, path, body];
return hmacSha256Hex(secretKey, message);
} }
NSString* detectLanguage(NSString* filename) { NSString* detectLanguage(NSString* filename) {
@ -92,15 +127,15 @@ NSString* detectLanguage(NSString* filename) {
return language; return language;
} }
NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* data, NSString* apiKey) { NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey) {
NSString* urlString = [API_BASE stringByAppendingString:endpoint]; NSString* urlString = [API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString]; NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method]; [request setHTTPMethod:method];
[request setValue:[@"Bearer " stringByAppendingString:apiKey] forHTTPHeaderField:@"Authorization"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setTimeoutInterval:300]; [request setTimeoutInterval:300];
// Prepare body
NSString* bodyString = @"";
if (data) { if (data) {
NSError* error = nil; NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error]; NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
@ -109,9 +144,20 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]); [RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1); exit(1);
} }
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[request setHTTPBody:jsonData]; [request setHTTPBody:jsonData];
} }
// Generate timestamp and signature
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = computeSignature(secretKey, timestamp, method, endpoint, bodyString);
// Set headers
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil; NSHTTPURLResponse* response = nil;
NSError* error = nil; NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request NSData* responseData = [NSURLConnection sendSynchronousRequest:request
@ -139,7 +185,8 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
} }
void cmdExecute(NSArray* args) { void cmdExecute(NSArray* args) {
NSString* apiKey = getApiKey(); NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
NSString* sourceFile = nil; NSString* sourceFile = nil;
NSMutableDictionary* envVars = [NSMutableDictionary dictionary]; NSMutableDictionary* envVars = [NSMutableDictionary dictionary];
NSMutableArray* inputFiles = [NSMutableArray array]; NSMutableArray* inputFiles = [NSMutableArray array];
@ -233,7 +280,7 @@ void cmdExecute(NSArray* args) {
} }
// Execute // Execute
NSDictionary* result = apiRequest(@"/execute", @"POST", payload, apiKey); NSDictionary* result = apiRequest(@"/execute", @"POST", payload, publicKey, secretKey);
// Print output // Print output
NSString* stdoutText = result[@"stdout"] ?: @""; NSString* stdoutText = result[@"stdout"] ?: @"";
@ -361,7 +408,8 @@ void validateKey(NSString* apiKey, BOOL shouldExtend) {
} }
void cmdKey(NSArray* args) { void cmdKey(NSArray* args) {
NSString* apiKey = getApiKey(); NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL shouldExtend = NO; BOOL shouldExtend = NO;
for (NSString* arg in args) { for (NSString* arg in args) {
@ -370,11 +418,13 @@ void cmdKey(NSArray* args) {
} }
} }
validateKey(apiKey, shouldExtend); // For portal validation, we use public key as bearer token
validateKey(publicKey, shouldExtend);
} }
void cmdSession(NSArray* args) { void cmdSession(NSArray* args) {
NSString* apiKey = getApiKey(); NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL listMode = NO; BOOL listMode = NO;
NSString* killId = nil; NSString* killId = nil;
NSString* shell = nil; NSString* shell = nil;
@ -398,7 +448,7 @@ void cmdSession(NSArray* args) {
} }
if (listMode) { if (listMode) {
NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, apiKey); NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, publicKey, secretKey);
NSArray* sessions = result[@"sessions"]; NSArray* sessions = result[@"sessions"];
if ([sessions count] == 0) { if ([sessions count] == 0) {
printf("No active sessions\n"); printf("No active sessions\n");
@ -417,7 +467,7 @@ void cmdSession(NSArray* args) {
if (killId) { if (killId) {
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@", killId]; NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@", killId];
apiRequest(endpoint, @"DELETE", nil, apiKey); apiRequest(endpoint, @"DELETE", nil, publicKey, secretKey);
printf("%sSession terminated: %s%s\n", [GREEN UTF8String], [killId UTF8String], [RESET UTF8String]); printf("%sSession terminated: %s%s\n", [GREEN UTF8String], [killId UTF8String], [RESET UTF8String]);
return; return;
} }
@ -430,14 +480,15 @@ void cmdSession(NSArray* args) {
if (vcpu > 0) payload[@"vcpu"] = @(vcpu); if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]); printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
NSDictionary* result = apiRequest(@"/sessions", @"POST", payload, apiKey); NSDictionary* result = apiRequest(@"/sessions", @"POST", payload, publicKey, secretKey);
printf("%sSession created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]); printf("%sSession created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
printf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n", printf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n",
[YELLOW UTF8String], [RESET UTF8String]); [YELLOW UTF8String], [RESET UTF8String]);
} }
void cmdService(NSArray* args) { void cmdService(NSArray* args) {
NSString* apiKey = getApiKey(); NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL listMode = NO; BOOL listMode = NO;
NSString* infoId = nil; NSString* infoId = nil;
NSString* logsId = nil; NSString* logsId = nil;
@ -488,7 +539,7 @@ void cmdService(NSArray* args) {
} }
if (listMode) { if (listMode) {
NSDictionary* result = apiRequest(@"/services", @"GET", nil, apiKey); NSDictionary* result = apiRequest(@"/services", @"GET", nil, publicKey, secretKey);
NSArray* services = result[@"services"]; NSArray* services = result[@"services"];
if ([services count] == 0) { if ([services count] == 0) {
printf("No services\n"); printf("No services\n");
@ -512,7 +563,7 @@ void cmdService(NSArray* args) {
if (infoId) { if (infoId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", infoId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@", infoId];
NSDictionary* result = apiRequest(endpoint, @"GET", nil, apiKey); NSDictionary* result = apiRequest(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]); printf("%s\n", [jsonString UTF8String]);
@ -521,28 +572,28 @@ void cmdService(NSArray* args) {
if (logsId) { if (logsId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/logs", logsId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@/logs", logsId];
NSDictionary* result = apiRequest(endpoint, @"GET", nil, apiKey); NSDictionary* result = apiRequest(endpoint, @"GET", nil, publicKey, secretKey);
printf("%s", [result[@"logs"] UTF8String]); printf("%s", [result[@"logs"] UTF8String]);
return; return;
} }
if (sleepId) { if (sleepId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/sleep", sleepId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@/sleep", sleepId];
apiRequest(endpoint, @"POST", nil, apiKey); apiRequest(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sService sleeping: %s%s\n", [GREEN UTF8String], [sleepId UTF8String], [RESET UTF8String]); printf("%sService sleeping: %s%s\n", [GREEN UTF8String], [sleepId UTF8String], [RESET UTF8String]);
return; return;
} }
if (wakeId) { if (wakeId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/wake", wakeId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@/wake", wakeId];
apiRequest(endpoint, @"POST", nil, apiKey); apiRequest(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sService waking: %s%s\n", [GREEN UTF8String], [wakeId UTF8String], [RESET UTF8String]); printf("%sService waking: %s%s\n", [GREEN UTF8String], [wakeId UTF8String], [RESET UTF8String]);
return; return;
} }
if (destroyId) { if (destroyId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId];
apiRequest(endpoint, @"DELETE", nil, apiKey); apiRequest(endpoint, @"DELETE", nil, publicKey, secretKey);
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]); printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
return; return;
} }
@ -551,7 +602,7 @@ void cmdService(NSArray* args) {
fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]); fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]);
NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"}; NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"};
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/execute", dumpBootstrapId]; NSString* endpoint = [NSString stringWithFormat:@"/services/%@/execute", dumpBootstrapId];
NSDictionary* result = apiRequest(endpoint, @"POST", payload, apiKey); NSDictionary* result = apiRequest(endpoint, @"POST", payload, publicKey, secretKey);
if (result[@"stdout"] && [result[@"stdout"] length] > 0) { if (result[@"stdout"] && [result[@"stdout"] length] > 0) {
NSString* bootstrap = result[@"stdout"]; NSString* bootstrap = result[@"stdout"];
@ -610,7 +661,7 @@ void cmdService(NSArray* args) {
if (network) payload[@"network"] = network; if (network) payload[@"network"] = network;
if (vcpu > 0) payload[@"vcpu"] = @(vcpu); if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
NSDictionary* result = apiRequest(@"/services", @"POST", payload, apiKey); NSDictionary* result = apiRequest(@"/services", @"POST", payload, publicKey, secretKey);
printf("%sService created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]); printf("%sService created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
printf("Name: %s\n", [result[@"name"] UTF8String]); printf("Name: %s\n", [result[@"name"] UTF8String]);
if (result[@"url"]) { if (result[@"url"]) {

91
un.ml
View file

@ -114,11 +114,22 @@ let escape_json s =
(* Execute curl command *) (* Execute curl command *)
let curl_post api_key endpoint json = let curl_post api_key endpoint json =
let cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'" let (public_key, secret_key) = get_api_keys () in
endpoint api_key json in let auth_headers = build_auth_headers public_key secret_key "POST" endpoint json in
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 https://api.unsandbox.com%s -H 'Content-Type: application/json'%s -d @%s"
endpoint auth_headers tmp_file in
let ic = Unix.open_process_in cmd in let ic = Unix.open_process_in cmd in
let output = read_file "/dev/stdin" 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 let _ = Unix.close_process_in ic in
Sys.remove tmp_file;
output output
let portal_curl_post api_key endpoint json = let portal_curl_post api_key endpoint json =
@ -126,8 +137,10 @@ let portal_curl_post api_key endpoint json =
let oc = open_out tmp_file in let oc = open_out tmp_file in
output_string oc json; output_string oc json;
close_out oc; close_out oc;
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s" let (public_key, secret_key) = get_api_keys () in
portal_base endpoint api_key tmp_file in let auth_headers = build_auth_headers public_key secret_key "POST" endpoint json in
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json'%s -d @%s"
portal_base endpoint auth_headers tmp_file in
let ic = Unix.open_process_in cmd in let ic = Unix.open_process_in cmd in
let rec read_all acc = let rec read_all acc =
try let line = input_line ic in read_all (acc ^ line ^ "\n") try let line = input_line ic in read_all (acc ^ line ^ "\n")
@ -139,8 +152,10 @@ let portal_curl_post api_key endpoint json =
output output
let curl_get api_key endpoint = let curl_get api_key endpoint =
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s -H 'Authorization: Bearer %s'" let (public_key, secret_key) = get_api_keys () in
endpoint api_key in let auth_headers = build_auth_headers public_key secret_key "GET" endpoint "" in
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s%s"
endpoint auth_headers in
let ic = Unix.open_process_in cmd in let ic = Unix.open_process_in cmd in
let rec read_all acc = let rec read_all acc =
try try
@ -153,17 +168,18 @@ let curl_get api_key endpoint =
output output
let curl_delete api_key endpoint = let curl_delete api_key endpoint =
let cmd = Printf.sprintf "curl -s -X DELETE https://api.unsandbox.com%s -H 'Authorization: Bearer %s'" let (public_key, secret_key) = get_api_keys () in
endpoint api_key in let auth_headers = build_auth_headers public_key secret_key "DELETE" endpoint "" in
let cmd = Printf.sprintf "curl -s -X DELETE https://api.unsandbox.com%s%s"
endpoint auth_headers in
let ic = Unix.open_process_in cmd in let ic = Unix.open_process_in cmd in
let output = read_all "" where let rec read_all acc =
let rec read_all acc = try
try let line = input_line ic in
let line = input_line ic in read_all (acc ^ line ^ "\n")
read_all (acc ^ line ^ "\n") with End_of_file -> acc
with End_of_file -> acc
in read_all ""
in in
let output = read_all "" in
let _ = Unix.close_process_in ic in let _ = Unix.close_process_in ic in
output output
@ -214,13 +230,46 @@ let unescape_json s =
let s = Str.global_replace (Str.regexp "\\\\\\\\") "\\" s in let s = Str.global_replace (Str.regexp "\\\\\\\\") "\\" s in
s s
(* Get API key *) (* Get API keys *)
let get_api_key () = let get_api_keys () =
try Sys.getenv "UNSANDBOX_API_KEY" let public_key = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
with Not_found -> let secret_key = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
Printf.fprintf stderr "Error: UNSANDBOX_API_KEY not set\n"; let api_key = try Some (Sys.getenv "UNSANDBOX_API_KEY") with Not_found -> None in
match (public_key, secret_key, api_key) with
| (Some pk, Some sk, _) -> (pk, Some sk)
| (_, _, Some ak) -> (ak, None)
| _ ->
Printf.fprintf stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n";
exit 1 exit 1
let get_api_key () =
let (public_key, _) = get_api_keys () in
public_key
(* HMAC-SHA256 using openssl command *)
let hmac_sha256 secret message =
let cmd = Printf.sprintf "echo -n '%s' | openssl dgst -sha256 -hmac '%s' | awk '{print $2}'"
(Str.global_replace (Str.regexp "'") "'\\''" message)
(Str.global_replace (Str.regexp "'") "'\\''" secret) in
let ic = Unix.open_process_in cmd in
let result = input_line ic in
let _ = Unix.close_process_in ic in
String.trim result
let make_signature secret_key timestamp method_ path body =
let message = Printf.sprintf "%s:%s:%s:%s" timestamp method_ path body in
hmac_sha256 secret_key message
let build_auth_headers public_key secret_key method_ path body =
match secret_key with
| Some sk ->
let timestamp = string_of_int (int_of_float (Unix.time ())) in
let signature = make_signature sk timestamp method_ path body in
Printf.sprintf " -H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'"
public_key timestamp signature
| None ->
Printf.sprintf " -H 'Authorization: Bearer %s'" public_key
(* Execute command *) (* Execute command *)
let execute_command file env_vars artifacts out_dir network vcpu = let execute_command file env_vars artifacts out_dir network vcpu =
let api_key = get_api_key () in let api_key = get_api_key () in

124
un.nim
View file

@ -43,7 +43,7 @@
# un.nim session --list # un.nim session --list
# un.nim service --name web --ports 8080 # un.nim service --name web --ports 8080
import os, strutils, osproc, strformat import os, strutils, osproc, strformat, times
const const
API_BASE = "https://api.unsandbox.com" API_BASE = "https://api.unsandbox.com"
@ -76,10 +76,29 @@ proc escapeJson(s: string): string =
of '\t': result.add("\\t") of '\t': result.add("\\t")
else: result.add(c) else: result.add(c)
proc computeHmac(secretKey: string, message: string): string =
let cmd = fmt"echo -n '{message}' | openssl dgst -sha256 -hmac '{secretKey}' -hex 2>/dev/null | sed 's/.*= //'"
result = execProcess(cmd).strip()
proc getTimestamp(): string =
result = $toUnix(getTime())
proc buildAuthHeaders(meth: string, path: string, body: string, publicKey: string, secretKey: string): string =
if secretKey == "":
# Legacy mode: use public_key as bearer token
return fmt"-H 'Authorization: Bearer {publicKey}'"
# HMAC mode
let timestamp = getTimestamp()
let message = fmt"{timestamp}:{meth}:{path}:{body}"
let signature = computeHmac(secretKey, message)
return fmt"-H 'Authorization: Bearer {publicKey}' -H 'X-Timestamp: {timestamp}' -H 'X-Signature: {signature}'"
proc execCurl(cmd: string): string = proc execCurl(cmd: string): string =
result = execProcess(cmd) result = execProcess(cmd)
proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network: string, vcpu: int, apiKey: string) = proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network: string, vcpu: int, publicKey: string, secretKey: string) =
let lang = detectLanguage(sourceFile) let lang = detectLanguage(sourceFile)
if lang == "": if lang == "":
stderr.writeLine(RED & "Error: Cannot detect language" & RESET) stderr.writeLine(RED & "Error: Cannot detect language" & RESET)
@ -102,17 +121,21 @@ proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network:
if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""") if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""")
json.add("}") json.add("}")
let cmd = fmt"""curl -s -X POST '{API_BASE}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" let authHeaders = buildAuthHeaders("POST", "/execute", json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
echo execCurl(cmd) echo execCurl(cmd)
proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, apiKey: string) = proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, publicKey: string, secretKey: string) =
if list: if list:
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' -H 'Authorization: Bearer {apiKey}'""" let authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' {authHeaders}"""
echo execCurl(cmd) echo execCurl(cmd)
return return
if kill != "": if kill != "":
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/sessions/{kill}' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/sessions/{kill}"
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/sessions/{kill}' {authHeaders}"""
discard execCurl(cmd) discard execCurl(cmd)
echo GREEN & "Session terminated: " & kill & RESET echo GREEN & "Session terminated: " & kill & RESET
return return
@ -125,51 +148,67 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
json.add("}") json.add("}")
echo YELLOW & "Creating session..." & RESET echo YELLOW & "Creating session..." & RESET
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" let authHeaders = buildAuthHeaders("POST", "/sessions", json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
echo execCurl(cmd) echo execCurl(cmd)
proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, apiKey: string) = proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, publicKey: string, secretKey: string) =
if list: if list:
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' -H 'Authorization: Bearer {apiKey}'""" let authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' {authHeaders}"""
echo execCurl(cmd) echo execCurl(cmd)
return return
if info != "": if info != "":
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{info}' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{info}"
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{info}' {authHeaders}"""
echo execCurl(cmd) echo execCurl(cmd)
return return
if logs != "": if logs != "":
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{logs}/logs' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{logs}/logs"
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{logs}/logs' {authHeaders}"""
stdout.write(execCurl(cmd)) stdout.write(execCurl(cmd))
return return
if tail != "": if tail != "":
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{tail}/logs?lines=9000' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{tail}/logs"
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{tail}/logs?lines=9000' {authHeaders}"""
stdout.write(execCurl(cmd)) stdout.write(execCurl(cmd))
return return
if sleep != "": if sleep != "":
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{sleep}/sleep' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{sleep}/sleep"
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{sleep}/sleep' {authHeaders}"""
discard execCurl(cmd) discard execCurl(cmd)
echo GREEN & "Service sleeping: " & sleep & RESET echo GREEN & "Service sleeping: " & sleep & RESET
return return
if wake != "": if wake != "":
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{wake}/wake' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{wake}/wake"
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{wake}/wake' {authHeaders}"""
discard execCurl(cmd) discard execCurl(cmd)
echo GREEN & "Service waking: " & wake & RESET echo GREEN & "Service waking: " & wake & RESET
return return
if destroy != "": if destroy != "":
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/services/{destroy}' -H 'Authorization: Bearer {apiKey}'""" let path = fmt"/services/{destroy}"
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/services/{destroy}' {authHeaders}"""
discard execCurl(cmd) discard execCurl(cmd)
echo GREEN & "Service destroyed: " & destroy & RESET echo GREEN & "Service destroyed: " & destroy & RESET
return return
if execute != "": if execute != "":
let json = fmt"""{"command":"{escapeJson(command)}"}""" let json = fmt"""{"command":"{escapeJson(command)}"}"""
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" let path = fmt"/services/{execute}/execute"
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
let result = execCurl(cmd) let result = execCurl(cmd)
# Simple parsing for stdout/stderr # Simple parsing for stdout/stderr
@ -202,7 +241,10 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
if dumpBootstrap != "": if dumpBootstrap != "":
stderr.writeLine("Fetching bootstrap script from " & dumpBootstrap & "...") stderr.writeLine("Fetching bootstrap script from " & dumpBootstrap & "...")
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{dumpBootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{{"command":"cat /tmp/bootstrap.sh"}}'""" let json = """{"command":"cat /tmp/bootstrap.sh"}"""
let path = fmt"/services/{dumpBootstrap}/execute"
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{dumpBootstrap}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
let result = execCurl(cmd) let result = execCurl(cmd)
let stdoutStart = result.find("\"stdout\":\"") let stdoutStart = result.find("\"stdout\":\"")
@ -252,15 +294,17 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
json.add("}") json.add("}")
echo YELLOW & "Creating service..." & RESET echo YELLOW & "Creating service..." & RESET
let cmd = fmt"""curl -s -X POST '{API_BASE}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" let authHeaders = buildAuthHeaders("POST", "/services", json, publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{API_BASE}/services' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
echo execCurl(cmd) echo execCurl(cmd)
return return
stderr.writeLine(RED & "Error: Specify --name to create a service" & RESET) stderr.writeLine(RED & "Error: Specify --name to create a service" & RESET)
quit(1) quit(1)
proc cmdKey(extend: bool, apiKey: string) = proc cmdKey(extend: bool, publicKey: string, secretKey: string) =
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' -H 'Authorization: Bearer {apiKey}'""" let authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' {authHeaders}"""
let response = execCurl(cmd) let response = execCurl(cmd)
# Parse JSON response manually (simple approach) # Parse JSON response manually (simple approach)
@ -271,10 +315,10 @@ proc cmdKey(extend: bool, apiKey: string) =
let pkStart = response.find("\"public_key\":\"") + 14 let pkStart = response.find("\"public_key\":\"") + 14
let pkEnd = response.find("\"", pkStart) let pkEnd = response.find("\"", pkStart)
if pkEnd > pkStart: if pkEnd > pkStart:
let publicKey = response[pkStart..<pkEnd] let pubKey = response[pkStart..<pkEnd]
echo "Public Key: " & publicKey echo "Public Key: " & pubKey
if extend: if extend:
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}" let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={pubKey}"
echo BLUE & "Opening browser to extend key..." & RESET echo BLUE & "Opening browser to extend key..." & RESET
discard execProcess(fmt"xdg-open '{extendUrl}'") discard execProcess(fmt"xdg-open '{extendUrl}'")
@ -296,13 +340,13 @@ proc cmdKey(extend: bool, apiKey: string) =
echo RED & "Expired" & RESET echo RED & "Expired" & RESET
# Extract and display key info # Extract and display key info
var publicKey = "" var pubKey = ""
if response.contains("\"public_key\":"): if response.contains("\"public_key\":"):
let pkStart = response.find("\"public_key\":\"") + 14 let pkStart = response.find("\"public_key\":\"") + 14
let pkEnd = response.find("\"", pkStart) let pkEnd = response.find("\"", pkStart)
if pkEnd > pkStart: if pkEnd > pkStart:
publicKey = response[pkStart..<pkEnd] pubKey = response[pkStart..<pkEnd]
echo "Public Key: " & publicKey echo "Public Key: " & pubKey
# Extract tier # Extract tier
if response.contains("\"tier\":"): if response.contains("\"tier\":"):
@ -321,8 +365,8 @@ proc cmdKey(extend: bool, apiKey: string) =
echo "" echo ""
echo YELLOW & "To renew: Visit " & PORTAL_BASE & "/keys/extend" & RESET echo YELLOW & "To renew: Visit " & PORTAL_BASE & "/keys/extend" & RESET
if extend and publicKey != "": if extend and pubKey != "":
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}" let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={pubKey}"
echo BLUE & "Opening browser to extend key..." & RESET echo BLUE & "Opening browser to extend key..." & RESET
discard execProcess(fmt"xdg-open '{extendUrl}'") discard execProcess(fmt"xdg-open '{extendUrl}'")
@ -335,7 +379,13 @@ proc cmdKey(extend: bool, apiKey: string) =
echo "Error: " & response[errStart..<errEnd] echo "Error: " & response[errStart..<errEnd]
proc main() = proc main() =
var apiKey = getEnv("UNSANDBOX_API_KEY", "") var publicKey = getEnv("UNSANDBOX_PUBLIC_KEY", "")
var secretKey = getEnv("UNSANDBOX_SECRET_KEY", "")
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
if publicKey == "":
publicKey = getEnv("UNSANDBOX_API_KEY", "")
let args = commandLineParams() let args = commandLineParams()
if args.len < 1: if args.len < 1:
@ -351,9 +401,9 @@ proc main() =
while i < args.len: while i < args.len:
case args[i] case args[i]
of "--extend": extend = true of "--extend": extend = true
of "-k": apiKey = args[i+1]; inc i of "-k": publicKey = args[i+1]; inc i
inc i inc i
cmdKey(extend, apiKey) cmdKey(extend, publicKey, secretKey)
return return
if args[0] == "session": if args[0] == "session":
@ -371,9 +421,9 @@ proc main() =
of "-v": vcpu = parseInt(args[i+1]); inc i of "-v": vcpu = parseInt(args[i+1]); inc i
of "--tmux": tmux = true of "--tmux": tmux = true
of "--screen": screen = true of "--screen": screen = true
of "-k": apiKey = args[i+1]; inc i of "-k": publicKey = args[i+1]; inc i
inc i inc i
cmdSession(list, kill, shell, network, vcpu, tmux, screen, apiKey) cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey)
return return
if args[0] == "service": if args[0] == "service":
@ -401,9 +451,9 @@ proc main() =
of "--dump-file": dumpFile = args[i+1]; inc i of "--dump-file": dumpFile = args[i+1]; inc i
of "-n": network = args[i+1]; inc i of "-n": network = args[i+1]; inc i
of "-v": vcpu = parseInt(args[i+1]); inc i of "-v": vcpu = parseInt(args[i+1]); inc i
of "-k": apiKey = args[i+1]; inc i of "-k": publicKey = args[i+1]; inc i
inc i inc i
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey) cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey)
return return
# Execute mode # Execute mode
@ -418,7 +468,7 @@ proc main() =
of "-a": artifacts = true of "-a": artifacts = true
of "-n": network = args[i+1]; inc i of "-n": network = args[i+1]; inc i
of "-v": vcpu = parseInt(args[i+1]); inc i of "-v": vcpu = parseInt(args[i+1]); inc i
of "-k": apiKey = args[i+1]; inc i of "-k": publicKey = args[i+1]; inc i
else: else:
if not args[i].startsWith("-"): if not args[i].startsWith("-"):
sourceFile = args[i] sourceFile = args[i]
@ -428,7 +478,7 @@ proc main() =
stderr.writeLine(RED & "Error: No source file specified" & RESET) stderr.writeLine(RED & "Error: No source file specified" & RESET)
quit(1) quit(1)
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey) cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey)
when isMainModule: when isMainModule:
main() main()

103
un.php
View file

@ -76,13 +76,23 @@ const EXT_MAP = [
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc' '.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
]; ];
function get_api_key($args_key = null) { function get_api_keys($args_key = null) {
$key = $args_key ?: getenv('UNSANDBOX_API_KEY'); $public_key = getenv('UNSANDBOX_PUBLIC_KEY');
if (!$key) { $secret_key = getenv('UNSANDBOX_SECRET_KEY');
fwrite(STDERR, RED . "Error: UNSANDBOX_API_KEY not set" . RESET . "\n");
exit(1); if (!$public_key || !$secret_key) {
$old_key = $args_key ?: getenv('UNSANDBOX_API_KEY');
if ($old_key) {
$public_key = $old_key;
$secret_key = $old_key;
} else {
fwrite(STDERR, RED . "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" . RESET . "\n");
fwrite(STDERR, RED . " (or legacy UNSANDBOX_API_KEY for backwards compatibility)" . RESET . "\n");
exit(1);
}
} }
return $key;
return ['public_key' => $public_key, 'secret_key' => $secret_key];
} }
function detect_language($filename) { function detect_language($filename) {
@ -109,12 +119,23 @@ function detect_language($filename) {
return $lang; return $lang;
} }
function api_request($endpoint, $method = 'GET', $data = null, $api_key = null) { function api_request($endpoint, $method = 'GET', $data = null, $keys = null) {
$url = API_BASE . $endpoint; $url = API_BASE . $endpoint;
$ch = curl_init($url); $ch = curl_init($url);
$timestamp = (string)time();
$body = $data ? json_encode($data) : '';
// Parse URL to get path and query
$parsed_url = parse_url($url);
$path = $parsed_url['path'] . (isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '');
$message = "$timestamp:$method:$path:$body";
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
$headers = [ $headers = [
'Authorization: Bearer ' . $api_key, 'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json' 'Content-Type: application/json'
]; ];
@ -126,7 +147,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
]); ]);
if ($data) { if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} }
$response = curl_exec($ch); $response = curl_exec($ch);
@ -149,7 +170,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
} }
function cmd_execute($options) { function cmd_execute($options) {
$api_key = get_api_key($options['api_key']); $keys = get_api_keys($options['api_key']);
if (!file_exists($options['source_file'])) { if (!file_exists($options['source_file'])) {
fwrite(STDERR, RED . "Error: File not found: {$options['source_file']}" . RESET . "\n"); fwrite(STDERR, RED . "Error: File not found: {$options['source_file']}" . RESET . "\n");
@ -193,7 +214,7 @@ function cmd_execute($options) {
if ($options['network']) $payload['network'] = $options['network']; if ($options['network']) $payload['network'] = $options['network'];
if ($options['vcpu']) $payload['vcpu'] = $options['vcpu']; if ($options['vcpu']) $payload['vcpu'] = $options['vcpu'];
$result = api_request('/execute', 'POST', $payload, $api_key); $result = api_request('/execute', 'POST', $payload, $keys);
if (!empty($result['stdout'])) { if (!empty($result['stdout'])) {
echo BLUE . $result['stdout'] . RESET; echo BLUE . $result['stdout'] . RESET;
@ -221,10 +242,10 @@ function cmd_execute($options) {
} }
function cmd_session($options) { function cmd_session($options) {
$api_key = get_api_key($options['api_key']); $keys = get_api_keys($options['api_key']);
if ($options['list']) { if ($options['list']) {
$result = api_request('/sessions', 'GET', null, $api_key); $result = api_request('/sessions', 'GET', null, $keys);
$sessions = $result['sessions'] ?? []; $sessions = $result['sessions'] ?? [];
if (empty($sessions)) { if (empty($sessions)) {
echo "No active sessions\n"; echo "No active sessions\n";
@ -240,7 +261,7 @@ function cmd_session($options) {
} }
if ($options['kill']) { if ($options['kill']) {
api_request("/sessions/{$options['kill']}", 'DELETE', null, $api_key); api_request("/sessions/{$options['kill']}", 'DELETE', null, $keys);
echo GREEN . "Session terminated: {$options['kill']}" . RESET . "\n"; echo GREEN . "Session terminated: {$options['kill']}" . RESET . "\n";
return; return;
} }
@ -259,17 +280,27 @@ function cmd_session($options) {
if ($options['audit']) $payload['audit'] = true; if ($options['audit']) $payload['audit'] = true;
echo YELLOW . "Creating session..." . RESET . "\n"; echo YELLOW . "Creating session..." . RESET . "\n";
$result = api_request('/sessions', 'POST', $payload, $api_key); $result = api_request('/sessions', 'POST', $payload, $keys);
echo GREEN . "Session created: " . ($result['id'] ?? 'N/A') . RESET . "\n"; echo GREEN . "Session created: " . ($result['id'] ?? 'N/A') . RESET . "\n";
echo YELLOW . "(Interactive sessions require WebSocket - use un2 for full support)" . RESET . "\n"; echo YELLOW . "(Interactive sessions require WebSocket - use un2 for full support)" . RESET . "\n";
} }
function validate_key($api_key) { function validate_key($keys) {
$url = PORTAL_BASE . '/keys/validate'; $url = PORTAL_BASE . '/keys/validate';
$ch = curl_init($url); $ch = curl_init($url);
$timestamp = (string)time();
$body = '';
$parsed_url = parse_url($url);
$path = $parsed_url['path'];
$message = "$timestamp:POST:$path:$body";
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
$headers = [ $headers = [
'Authorization: Bearer ' . $api_key, 'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json' 'Content-Type: application/json'
]; ];
@ -322,15 +353,25 @@ function validate_key($api_key) {
} }
function cmd_key($options) { function cmd_key($options) {
$api_key = get_api_key($options['api_key']); $keys = get_api_keys($options['api_key']);
if ($options['extend']) { if ($options['extend']) {
// First validate to get public_key // First validate to get public_key
$url = PORTAL_BASE . '/keys/validate'; $url = PORTAL_BASE . '/keys/validate';
$ch = curl_init($url); $ch = curl_init($url);
$timestamp = (string)time();
$body = '';
$parsed_url = parse_url($url);
$path = $parsed_url['path'];
$message = "$timestamp:POST:$path:$body";
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
$headers = [ $headers = [
'Authorization: Bearer ' . $api_key, 'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json' 'Content-Type: application/json'
]; ];
@ -367,15 +408,15 @@ function cmd_key($options) {
echo "$extend_url\n"; echo "$extend_url\n";
} }
} else { } else {
validate_key($api_key); validate_key($keys);
} }
} }
function cmd_service($options) { function cmd_service($options) {
$api_key = get_api_key($options['api_key']); $keys = get_api_keys($options['api_key']);
if ($options['list']) { if ($options['list']) {
$result = api_request('/services', 'GET', null, $api_key); $result = api_request('/services', 'GET', null, $keys);
$services = $result['services'] ?? []; $services = $result['services'] ?? [];
if (empty($services)) { if (empty($services)) {
echo "No services\n"; echo "No services\n";
@ -393,44 +434,44 @@ function cmd_service($options) {
} }
if ($options['info']) { if ($options['info']) {
$result = api_request("/services/{$options['info']}", 'GET', null, $api_key); $result = api_request("/services/{$options['info']}", 'GET', null, $keys);
echo json_encode($result, JSON_PRETTY_PRINT) . "\n"; echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
return; return;
} }
if ($options['logs']) { if ($options['logs']) {
$result = api_request("/services/{$options['logs']}/logs", 'GET', null, $api_key); $result = api_request("/services/{$options['logs']}/logs", 'GET', null, $keys);
echo $result['logs'] ?? ''; echo $result['logs'] ?? '';
return; return;
} }
if ($options['tail']) { if ($options['tail']) {
$result = api_request("/services/{$options['tail']}/logs?lines=9000", 'GET', null, $api_key); $result = api_request("/services/{$options['tail']}/logs?lines=9000", 'GET', null, $keys);
echo $result['logs'] ?? ''; echo $result['logs'] ?? '';
return; return;
} }
if ($options['sleep']) { if ($options['sleep']) {
api_request("/services/{$options['sleep']}/sleep", 'POST', null, $api_key); api_request("/services/{$options['sleep']}/sleep", 'POST', null, $keys);
echo GREEN . "Service sleeping: {$options['sleep']}" . RESET . "\n"; echo GREEN . "Service sleeping: {$options['sleep']}" . RESET . "\n";
return; return;
} }
if ($options['wake']) { if ($options['wake']) {
api_request("/services/{$options['wake']}/wake", 'POST', null, $api_key); api_request("/services/{$options['wake']}/wake", 'POST', null, $keys);
echo GREEN . "Service waking: {$options['wake']}" . RESET . "\n"; echo GREEN . "Service waking: {$options['wake']}" . RESET . "\n";
return; return;
} }
if ($options['destroy']) { if ($options['destroy']) {
api_request("/services/{$options['destroy']}", 'DELETE', null, $api_key); api_request("/services/{$options['destroy']}", 'DELETE', null, $keys);
echo GREEN . "Service destroyed: {$options['destroy']}" . RESET . "\n"; echo GREEN . "Service destroyed: {$options['destroy']}" . RESET . "\n";
return; return;
} }
if ($options['execute']) { if ($options['execute']) {
$payload = ['command' => $options['command']]; $payload = ['command' => $options['command']];
$result = api_request("/services/{$options['execute']}/execute", 'POST', $payload, $api_key); $result = api_request("/services/{$options['execute']}/execute", 'POST', $payload, $keys);
if (!empty($result['stdout'])) echo BLUE . $result['stdout'] . RESET; if (!empty($result['stdout'])) echo BLUE . $result['stdout'] . RESET;
if (!empty($result['stderr'])) fwrite(STDERR, RED . $result['stderr'] . RESET); if (!empty($result['stderr'])) fwrite(STDERR, RED . $result['stderr'] . RESET);
return; return;
@ -439,7 +480,7 @@ function cmd_service($options) {
if ($options['dump_bootstrap']) { if ($options['dump_bootstrap']) {
fwrite(STDERR, "Fetching bootstrap script from {$options['dump_bootstrap']}...\n"); fwrite(STDERR, "Fetching bootstrap script from {$options['dump_bootstrap']}...\n");
$payload = ['command' => 'cat /tmp/bootstrap.sh']; $payload = ['command' => 'cat /tmp/bootstrap.sh'];
$result = api_request("/services/{$options['dump_bootstrap']}/execute", 'POST', $payload, $api_key); $result = api_request("/services/{$options['dump_bootstrap']}/execute", 'POST', $payload, $keys);
if (!empty($result['stdout'])) { if (!empty($result['stdout'])) {
$bootstrap = $result['stdout']; $bootstrap = $result['stdout'];
@ -483,7 +524,7 @@ function cmd_service($options) {
if ($options['network']) $payload['network'] = $options['network']; if ($options['network']) $payload['network'] = $options['network'];
if ($options['vcpu']) $payload['vcpu'] = $options['vcpu']; if ($options['vcpu']) $payload['vcpu'] = $options['vcpu'];
$result = api_request('/services', 'POST', $payload, $api_key); $result = api_request('/services', 'POST', $payload, $keys);
echo GREEN . "Service created: " . ($result['id'] ?? 'N/A') . RESET . "\n"; echo GREEN . "Service created: " . ($result['id'] ?? 'N/A') . RESET . "\n";
echo "Name: " . ($result['name'] ?? 'N/A') . "\n"; echo "Name: " . ($result['name'] ?? 'N/A') . "\n";
if (!empty($result['url'])) echo "URL: {$result['url']}\n"; if (!empty($result['url'])) echo "URL: {$result['url']}\n";

85
un.pl
View file

@ -57,6 +57,7 @@ use LWP::UserAgent;
use HTTP::Request; use HTTP::Request;
use MIME::Base64; use MIME::Base64;
use File::Path qw(make_path); use File::Path qw(make_path);
use Digest::SHA qw(hmac_sha256_hex);
my $API_BASE = 'https://api.unsandbox.com'; my $API_BASE = 'https://api.unsandbox.com';
my $PORTAL_BASE = 'https://unsandbox.com'; my $PORTAL_BASE = 'https://unsandbox.com';
@ -84,12 +85,20 @@ my %EXT_MAP = (
sub get_api_key { sub get_api_key {
my ($args_key) = @_; my ($args_key) = @_;
my $key = $args_key || $ENV{'UNSANDBOX_API_KEY'}; my $public_key = $ENV{'UNSANDBOX_PUBLIC_KEY'} || '';
unless ($key) { my $secret_key = $ENV{'UNSANDBOX_SECRET_KEY'} || '';
print STDERR "${RED}Error: UNSANDBOX_API_KEY not set${RESET}\n";
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if (!$public_key && $ENV{'UNSANDBOX_API_KEY'}) {
$public_key = $ENV{'UNSANDBOX_API_KEY'};
$secret_key = '';
}
unless ($public_key) {
print STDERR "${RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${RESET}\n";
exit 1; exit 1;
} }
return $key; return ($public_key, $secret_key);
} }
sub detect_language { sub detect_language {
@ -117,17 +126,28 @@ sub detect_language {
} }
sub api_request { sub api_request {
my ($endpoint, $method, $data, $api_key) = @_; my ($endpoint, $method, $data, $public_key, $secret_key) = @_;
$method //= 'GET'; $method //= 'GET';
my $url = "$API_BASE$endpoint"; my $url = "$API_BASE$endpoint";
my $ua = LWP::UserAgent->new(timeout => 300); my $ua = LWP::UserAgent->new(timeout => 300);
my $request = HTTP::Request->new($method => $url); my $request = HTTP::Request->new($method => $url);
$request->header('Authorization' => "Bearer $api_key"); $request->header('Authorization' => "Bearer $public_key");
$request->header('Content-Type' => 'application/json'); $request->header('Content-Type' => 'application/json');
my $body = '';
if ($data) { if ($data) {
$request->content(encode_json($data)); $body = encode_json($data);
$request->content($body);
}
# Add HMAC signature if secret_key is present
if ($secret_key) {
my $timestamp = time();
my $sig_input = "${timestamp}:${method}:${endpoint}:${body}";
my $signature = hmac_sha256_hex($sig_input, $secret_key);
$request->header('X-Timestamp' => $timestamp);
$request->header('X-Signature' => $signature);
} }
my $response = $ua->request($request); my $response = $ua->request($request);
@ -142,7 +162,7 @@ sub api_request {
sub cmd_execute { sub cmd_execute {
my ($options) = @_; my ($options) = @_;
my $api_key = get_api_key($options->{api_key}); my ($public_key, $secret_key) = get_api_key($options->{api_key});
unless (-e $options->{source_file}) { unless (-e $options->{source_file}) {
print STDERR "${RED}Error: File not found: $options->{source_file}${RESET}\n"; print STDERR "${RED}Error: File not found: $options->{source_file}${RESET}\n";
@ -190,7 +210,7 @@ sub cmd_execute {
$payload->{network} = $options->{network} if $options->{network}; $payload->{network} = $options->{network} if $options->{network};
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu}; $payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
my $result = api_request('/execute', 'POST', $payload, $api_key); my $result = api_request('/execute', 'POST', $payload, $public_key, $secret_key);
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout}; print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr}; print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
@ -215,10 +235,10 @@ sub cmd_execute {
sub cmd_session { sub cmd_session {
my ($options) = @_; my ($options) = @_;
my $api_key = get_api_key($options->{api_key}); my ($public_key, $secret_key) = get_api_key($options->{api_key});
if ($options->{list}) { if ($options->{list}) {
my $result = api_request('/sessions', 'GET', undef, $api_key); my $result = api_request('/sessions', 'GET', undef, $public_key, $secret_key);
my $sessions = $result->{sessions} || []; my $sessions = $result->{sessions} || [];
if (@$sessions == 0) { if (@$sessions == 0) {
print "No active sessions\n"; print "No active sessions\n";
@ -234,7 +254,7 @@ sub cmd_session {
} }
if ($options->{kill}) { if ($options->{kill}) {
api_request("/sessions/$options->{kill}", 'DELETE', undef, $api_key); api_request("/sessions/$options->{kill}", 'DELETE', undef, $public_key, $secret_key);
print "${GREEN}Session terminated: $options->{kill}${RESET}\n"; print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
return; return;
} }
@ -253,17 +273,17 @@ sub cmd_session {
$payload->{audit} = JSON::PP::true if $options->{audit}; $payload->{audit} = JSON::PP::true if $options->{audit};
print "${YELLOW}Creating session...${RESET}\n"; print "${YELLOW}Creating session...${RESET}\n";
my $result = api_request('/sessions', 'POST', $payload, $api_key); my $result = api_request('/sessions', 'POST', $payload, $public_key, $secret_key);
print "${GREEN}Session created: ", ($result->{id} // 'N/A'), "${RESET}\n"; print "${GREEN}Session created: ", ($result->{id} // 'N/A'), "${RESET}\n";
print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n"; print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
} }
sub cmd_service { sub cmd_service {
my ($options) = @_; my ($options) = @_;
my $api_key = get_api_key($options->{api_key}); my ($public_key, $secret_key) = get_api_key($options->{api_key});
if ($options->{list}) { if ($options->{list}) {
my $result = api_request('/services', 'GET', undef, $api_key); my $result = api_request('/services', 'GET', undef, $public_key, $secret_key);
my $services = $result->{services} || []; my $services = $result->{services} || [];
if (@$services == 0) { if (@$services == 0) {
print "No services\n"; print "No services\n";
@ -281,45 +301,45 @@ sub cmd_service {
} }
if ($options->{info}) { if ($options->{info}) {
my $result = api_request("/services/$options->{info}", 'GET', undef, $api_key); my $result = api_request("/services/$options->{info}", 'GET', undef, $public_key, $secret_key);
print encode_json($result); print encode_json($result);
print "\n"; print "\n";
return; return;
} }
if ($options->{logs}) { if ($options->{logs}) {
my $result = api_request("/services/$options->{logs}/logs", 'GET', undef, $api_key); my $result = api_request("/services/$options->{logs}/logs", 'GET', undef, $public_key, $secret_key);
print $result->{logs} // ''; print $result->{logs} // '';
return; return;
} }
if ($options->{tail}) { if ($options->{tail}) {
my $result = api_request("/services/$options->{tail}/logs?lines=9000", 'GET', undef, $api_key); my $result = api_request("/services/$options->{tail}/logs?lines=9000", 'GET', undef, $public_key, $secret_key);
print $result->{logs} // ''; print $result->{logs} // '';
return; return;
} }
if ($options->{sleep}) { if ($options->{sleep}) {
api_request("/services/$options->{sleep}/sleep", 'POST', undef, $api_key); api_request("/services/$options->{sleep}/sleep", 'POST', undef, $public_key, $secret_key);
print "${GREEN}Service sleeping: $options->{sleep}${RESET}\n"; print "${GREEN}Service sleeping: $options->{sleep}${RESET}\n";
return; return;
} }
if ($options->{wake}) { if ($options->{wake}) {
api_request("/services/$options->{wake}/wake", 'POST', undef, $api_key); api_request("/services/$options->{wake}/wake", 'POST', undef, $public_key, $secret_key);
print "${GREEN}Service waking: $options->{wake}${RESET}\n"; print "${GREEN}Service waking: $options->{wake}${RESET}\n";
return; return;
} }
if ($options->{destroy}) { if ($options->{destroy}) {
api_request("/services/$options->{destroy}", 'DELETE', undef, $api_key); api_request("/services/$options->{destroy}", 'DELETE', undef, $public_key, $secret_key);
print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n"; print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
return; return;
} }
if ($options->{execute}) { if ($options->{execute}) {
my $payload = { command => $options->{command} }; my $payload = { command => $options->{command} };
my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $api_key); my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $public_key, $secret_key);
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout}; print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr}; print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
return; return;
@ -328,7 +348,7 @@ sub cmd_service {
if ($options->{dump_bootstrap}) { if ($options->{dump_bootstrap}) {
print STDERR "Fetching bootstrap script from $options->{dump_bootstrap}...\n"; print STDERR "Fetching bootstrap script from $options->{dump_bootstrap}...\n";
my $payload = { command => 'cat /tmp/bootstrap.sh' }; my $payload = { command => 'cat /tmp/bootstrap.sh' };
my $result = api_request("/services/$options->{dump_bootstrap}/execute", 'POST', $payload, $api_key); my $result = api_request("/services/$options->{dump_bootstrap}/execute", 'POST', $payload, $public_key, $secret_key);
if ($result->{stdout}) { if ($result->{stdout}) {
my $bootstrap = $result->{stdout}; my $bootstrap = $result->{stdout};
@ -379,7 +399,7 @@ sub cmd_service {
$payload->{network} = $options->{network} if $options->{network}; $payload->{network} = $options->{network} if $options->{network};
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu}; $payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
my $result = api_request('/services', 'POST', $payload, $api_key); my $result = api_request('/services', 'POST', $payload, $public_key, $secret_key);
print "${GREEN}Service created: ", ($result->{id} // 'N/A'), "${RESET}\n"; print "${GREEN}Service created: ", ($result->{id} // 'N/A'), "${RESET}\n";
print "Name: ", ($result->{name} // 'N/A'), "\n"; print "Name: ", ($result->{name} // 'N/A'), "\n";
print "URL: $result->{url}\n" if $result->{url}; print "URL: $result->{url}\n" if $result->{url};
@ -405,15 +425,24 @@ sub open_browser {
} }
sub validate_key { sub validate_key {
my ($api_key, $should_extend) = @_; my ($public_key, $secret_key, $should_extend) = @_;
# Call /keys/validate endpoint # Call /keys/validate endpoint
my $url = "$PORTAL_BASE/keys/validate"; my $url = "$PORTAL_BASE/keys/validate";
my $ua = LWP::UserAgent->new(timeout => 30); my $ua = LWP::UserAgent->new(timeout => 30);
my $request = HTTP::Request->new('POST' => $url); my $request = HTTP::Request->new('POST' => $url);
$request->header('Authorization' => "Bearer $api_key"); $request->header('Authorization' => "Bearer $public_key");
$request->header('Content-Type' => 'application/json'); $request->header('Content-Type' => 'application/json');
# Add HMAC signature if secret_key is present
if ($secret_key) {
my $timestamp = time();
my $sig_input = "${timestamp}:POST:/keys/validate:";
my $signature = hmac_sha256_hex($sig_input, $secret_key);
$request->header('X-Timestamp' => $timestamp);
$request->header('X-Signature' => $signature);
}
my $response = $ua->request($request); my $response = $ua->request($request);
my $result = decode_json($response->content); my $result = decode_json($response->content);
@ -455,8 +484,8 @@ sub validate_key {
sub cmd_key { sub cmd_key {
my ($options) = @_; my ($options) = @_;
my $api_key = get_api_key($options->{api_key}); my ($public_key, $secret_key) = get_api_key($options->{api_key});
validate_key($api_key, $options->{extend}); validate_key($public_key, $secret_key, $options->{extend});
} }
sub main { sub main {

125
un.pro
View file

@ -79,15 +79,32 @@ read_file_content(Filename, Content) :-
read_string(Stream, _, Content), read_string(Stream, _, Content),
close(Stream). close(Stream).
% Get API key from environment % Get API keys from environment (HMAC or legacy)
get_api_key(ApiKey) :- get_public_key(PublicKey) :-
( getenv('UNSANDBOX_API_KEY', ApiKey), ( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
ApiKey \= '' PublicKey \= ''
-> true -> true
; write(user_error, 'Error: UNSANDBOX_API_KEY environment variable not set\n'), ; getenv('UNSANDBOX_API_KEY', PublicKey),
PublicKey \= ''
-> true
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'),
halt(1) halt(1)
). ).
get_secret_key(SecretKey) :-
( getenv('UNSANDBOX_SECRET_KEY', SecretKey),
SecretKey \= ''
-> true
; getenv('UNSANDBOX_API_KEY', SecretKey),
SecretKey \= ''
-> true
; SecretKey = ''
).
% Get API key (legacy compatibility)
get_api_key(ApiKey) :-
get_public_key(ApiKey).
% Execute command using curl % Execute command using curl
execute_file(Filename) :- execute_file(Filename) :-
% Check file exists % Check file exists
@ -105,97 +122,108 @@ execute_file(Filename) :-
halt(1) halt(1)
), ),
% Get API key % Get API keys
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
% Build and execute curl command % Build and execute curl command with HMAC
format(atom(Cmd), format(atom(Cmd),
'curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" --data-binary @- -o /tmp/unsandbox_resp.json < <(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json', 'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json',
[ApiKey, Language, Filename]), [Language, Filename, SecretKey, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Session list % Session list
session_list :- session_list :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"',
[ApiKey]), [SecretKey, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Session kill % Session kill
session_kill(SessionId) :- session_kill(SessionId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X DELETE https://api.unsandbox.com/sessions/~w -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mSession terminated: ~w\\x1b[0m"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:DELETE:/sessions/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X DELETE https://api.unsandbox.com/sessions/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mSession terminated: ~w\\x1b[0m"',
[SessionId, ApiKey, SessionId]), [SessionId, SecretKey, SessionId, PublicKey, SessionId]),
shell(Cmd, 0). shell(Cmd, 0).
% Service list % Service list
service_list :- service_list :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"',
[ApiKey]), [SecretKey, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Service info % Service info
service_info(ServiceId) :- service_info(ServiceId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" | jq .', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq .',
[ServiceId, ApiKey]), [ServiceId, SecretKey, ServiceId, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Service logs % Service logs
service_logs(ServiceId) :- service_logs(ServiceId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/services/~w/logs -H "Authorization: Bearer ~w" | jq -r ".logs"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services/~w/logs:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services/~w/logs -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r ".logs"',
[ServiceId, ApiKey]), [ServiceId, SecretKey, ServiceId, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Service sleep % Service sleep
service_sleep(ServiceId) :- service_sleep(ServiceId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X POST https://api.unsandbox.com/services/~w/sleep -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService sleeping: ~w\\x1b[0m"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/sleep:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services/~w/sleep -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService sleeping: ~w\\x1b[0m"',
[ServiceId, ApiKey, ServiceId]), [ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0). shell(Cmd, 0).
% Service wake % Service wake
service_wake(ServiceId) :- service_wake(ServiceId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X POST https://api.unsandbox.com/services/~w/wake -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService waking: ~w\\x1b[0m"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/wake:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services/~w/wake -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService waking: ~w\\x1b[0m"',
[ServiceId, ApiKey, ServiceId]), [ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0). shell(Cmd, 0).
% Service destroy % Service destroy
service_destroy(ServiceId) :- service_destroy(ServiceId) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd), format(atom(Cmd),
'curl -s -X DELETE https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService destroyed: ~w\\x1b[0m"', 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:DELETE:/services/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X DELETE https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService destroyed: ~w\\x1b[0m"',
[ServiceId, ApiKey, ServiceId]), [ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0). shell(Cmd, 0).
% Service dump bootstrap % Service dump bootstrap
service_dump_bootstrap(ServiceId, DumpFile) :- service_dump_bootstrap(ServiceId, DumpFile) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
( DumpFile = '' ( DumpFile = ''
-> % No file specified, print to stdout -> % No file specified, print to stdout
format(atom(Cmd), format(atom(Cmd),
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi', 'echo "Fetching bootstrap script from ~w..." >&2; BODY=\'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
[ServiceId, ServiceId, ApiKey]) [ServiceId, ServiceId, SecretKey, ServiceId, PublicKey])
; % File specified, save to file ; % File specified, save to file
format(atom(Cmd), format(atom(Cmd),
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi', 'echo "Fetching bootstrap script from ~w..." >&2; BODY=\'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
[ServiceId, ServiceId, ApiKey, DumpFile, DumpFile, DumpFile]) [ServiceId, ServiceId, SecretKey, ServiceId, PublicKey, DumpFile, DumpFile, DumpFile])
), ),
shell(Cmd, 0). shell(Cmd, 0).
% Service create % Service create
service_create(Name, Ports, Bootstrap, ServiceType) :- service_create(Name, Ports, Bootstrap, ServiceType) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
% Build JSON payload % Build JSON payload
( Ports \= '' ( Ports \= ''
-> format(atom(PortsJson), ',"ports":[~w]', [Ports]) -> format(atom(PortsJson), ',"ports":[~w]', [Ports])
@ -210,25 +238,26 @@ service_create(Name, Ports, Bootstrap, ServiceType) :-
; ServiceTypeJson = '' ; ServiceTypeJson = ''
), ),
format(atom(Json), '{"name":"~w"~w~w~w}', [Name, PortsJson, BootstrapJson, ServiceTypeJson]), format(atom(Json), '{"name":"~w"~w~w~w}', [Name, PortsJson, BootstrapJson, ServiceTypeJson]),
% Execute curl command % Execute curl command with HMAC
format(atom(Cmd), format(atom(Cmd),
'curl -s -X POST https://api.unsandbox.com/services -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'~w\' && echo -e "\\x1b[32mService created\\x1b[0m"', 'BODY=\'~w\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" && echo -e "\\x1b[32mService created\\x1b[0m"',
[ApiKey, Json]), [Json, SecretKey, PublicKey]),
shell(Cmd, 0). shell(Cmd, 0).
% Key validate % Key validate
validate_key(Extend) :- validate_key(Extend) :-
get_api_key(ApiKey), get_public_key(PublicKey),
get_secret_key(SecretKey),
portal_base(PortalBase), portal_base(PortalBase),
( Extend = true ( Extend = true
-> % Build command for --extend mode -> % Build command for --extend mode
format(atom(Cmd), format(atom(Cmd),
'RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null', 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
[PortalBase, ApiKey, PortalBase]) [SecretKey, PortalBase, PublicKey, PortalBase])
; % Build command for normal validation ; % Build command for normal validation
format(atom(Cmd), format(atom(Cmd),
'curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json', 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
[PortalBase, ApiKey]) [SecretKey, PortalBase, PublicKey])
), ),
shell(Cmd, 0). shell(Cmd, 0).

37
un.ps1
View file

@ -59,24 +59,47 @@ $EXT_MAP = @{
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk" ".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
} }
function Get-ApiKey { function Get-ApiKeys {
$key = $env:UNSANDBOX_API_KEY $publicKey = $env:UNSANDBOX_PUBLIC_KEY
if (-not $key) { $secretKey = $env:UNSANDBOX_SECRET_KEY
Write-Error "Error: UNSANDBOX_API_KEY not set"
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if (-not $publicKey -and $env:UNSANDBOX_API_KEY) {
$publicKey = $env:UNSANDBOX_API_KEY
$secretKey = ""
}
if (-not $publicKey) {
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
exit 1 exit 1
} }
return $key return @($publicKey, $secretKey)
} }
function Invoke-Api { function Invoke-Api {
param($Endpoint, $Method = "GET", $Body = $null, $BaseUrl = $null) param($Endpoint, $Method = "GET", $Body = $null, $BaseUrl = $null)
$apiKey = Get-ApiKey $publicKey, $secretKey = Get-ApiKeys
$headers = @{ $headers = @{
"Authorization" = "Bearer $apiKey" "Authorization" = "Bearer $publicKey"
"Content-Type" = "application/json" "Content-Type" = "application/json"
} }
# Add HMAC signature if secret key exists
if ($secretKey) {
$timestamp = [int][double]::Parse((Get-Date -UFormat %s))
$bodyContent = if ($Body) { $Body } else { "" }
$sigInput = "${timestamp}:${Method}:${Endpoint}:${bodyContent}"
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($secretKey)
$hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sigInput))
$signature = [System.BitConverter]::ToString($hash).Replace("-", "").ToLower()
$headers["X-Timestamp"] = $timestamp.ToString()
$headers["X-Signature"] = $signature
}
$base = if ($BaseUrl) { $BaseUrl } else { $API_BASE } $base = if ($BaseUrl) { $BaseUrl } else { $API_BASE }
$uri = "$base$Endpoint" $uri = "$base$Endpoint"

129
un.py
View file

@ -59,6 +59,9 @@ import argparse
import urllib.request import urllib.request
import urllib.error import urllib.error
import webbrowser import webbrowser
import hmac
import hashlib
import time
API_BASE = "https://api.unsandbox.com" API_BASE = "https://api.unsandbox.com"
PORTAL_BASE = "https://unsandbox.com" PORTAL_BASE = "https://unsandbox.com"
@ -86,16 +89,28 @@ EXT_MAP = {
} }
def get_api_key(args_key=None): def get_api_keys(args_key=None):
"""Get API key from args or environment""" """Get API keys from args or environment. Returns (public_key, secret_key)."""
key = args_key or os.environ.get("UNSANDBOX_API_KEY") # Try new split key format first
if not key: public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
print(f"{RED}Error: UNSANDBOX_API_KEY not set{RESET}", file=sys.stderr) secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
sys.exit(1)
return key # Fall back to old single key format for backwards compatibility
if not public_key or not secret_key:
old_key = args_key or os.environ.get("UNSANDBOX_API_KEY")
if old_key:
# Old format: use the key as secret, derive public from it or use as-is
public_key = old_key
secret_key = old_key
else:
print(f"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}", file=sys.stderr)
print(f"{RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility){RESET}", file=sys.stderr)
sys.exit(1)
return public_key, secret_key
def detect_language(filename): def detect_language(filename, exit_on_error=True):
"""Detect language from file extension""" """Detect language from file extension"""
ext = os.path.splitext(filename)[1].lower() ext = os.path.splitext(filename)[1].lower()
lang = EXT_MAP.get(ext) lang = EXT_MAP.get(ext)
@ -114,22 +129,52 @@ def detect_language(filename):
if 'php' in first_line: return 'php' if 'php' in first_line: return 'php'
except: except:
pass pass
print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr) if exit_on_error:
sys.exit(1) print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr)
sys.exit(1)
return None
return lang return lang
def api_request(endpoint, method="GET", data=None, api_key=None): def read_file(filepath):
"""Make API request and return response""" """Read file contents - helper for tests"""
with open(filepath, 'r') as f:
return f.read()
def execute_code(language, code, public_key=None, secret_key=None):
"""Execute code and return result - helper for tests"""
if not public_key:
public_key, secret_key = get_api_keys()
return api_request("/execute", method="POST", data={"language": language, "code": code}, public_key=public_key, secret_key=secret_key)
def api_request(endpoint, method="GET", data=None, public_key=None, secret_key=None):
"""Make API request with HMAC authentication"""
url = f"{API_BASE}{endpoint}" url = f"{API_BASE}{endpoint}"
# Prepare body
body = json.dumps(data) if data else ""
# Generate HMAC signature
timestamp = str(int(time.time()))
signature_input = f"{timestamp}:{method}:{endpoint}:{body}"
signature = hmac.new(
secret_key.encode('utf-8'),
signature_input.encode('utf-8'),
hashlib.sha256
).hexdigest()
headers = { headers = {
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {public_key}",
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
req = urllib.request.Request(url, method=method, headers=headers) req = urllib.request.Request(url, method=method, headers=headers)
if data: if data:
req.data = json.dumps(data).encode('utf-8') req.data = body.encode('utf-8')
try: try:
with urllib.request.urlopen(req, timeout=300) as resp: with urllib.request.urlopen(req, timeout=300) as resp:
@ -145,7 +190,7 @@ def api_request(endpoint, method="GET", data=None, api_key=None):
def cmd_execute(args): def cmd_execute(args):
"""Execute source code""" """Execute source code"""
api_key = get_api_key(args.api_key) public_key, secret_key = get_api_keys(args.api_key)
# Read source file # Read source file
try: try:
@ -199,7 +244,7 @@ def cmd_execute(args):
payload["vcpu"] = args.vcpu payload["vcpu"] = args.vcpu
# Execute # Execute
result = api_request("/execute", method="POST", data=payload, api_key=api_key) result = api_request("/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
# Print output # Print output
if result.get("stdout"): if result.get("stdout"):
@ -225,10 +270,10 @@ def cmd_execute(args):
def cmd_session(args): def cmd_session(args):
"""Manage interactive sessions""" """Manage interactive sessions"""
api_key = get_api_key(args.api_key) public_key, secret_key = get_api_keys(args.api_key)
if args.list: if args.list:
result = api_request("/sessions", api_key=api_key) result = api_request("/sessions", public_key=public_key, secret_key=secret_key)
sessions = result.get("sessions", []) sessions = result.get("sessions", [])
if not sessions: if not sessions:
print("No active sessions") print("No active sessions")
@ -239,7 +284,7 @@ def cmd_session(args):
return return
if args.kill: if args.kill:
result = api_request(f"/sessions/{args.kill}", method="DELETE", api_key=api_key) result = api_request(f"/sessions/{args.kill}", method="DELETE", public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Session terminated: {args.kill}{RESET}") print(f"{GREEN}Session terminated: {args.kill}{RESET}")
return return
@ -264,16 +309,30 @@ def cmd_session(args):
payload["audit"] = True payload["audit"] = True
print(f"{YELLOW}Creating session...{RESET}") print(f"{YELLOW}Creating session...{RESET}")
result = api_request("/sessions", method="POST", data=payload, api_key=api_key) result = api_request("/sessions", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Session created: {result.get('id', 'N/A')}{RESET}") print(f"{GREEN}Session created: {result.get('id', 'N/A')}{RESET}")
print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}") print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}")
def validate_key(api_key, extend=False): def validate_key(public_key, secret_key, extend=False):
"""Validate API key and display information""" """Validate API key and display information"""
url = f"{PORTAL_BASE}/keys/validate" url = f"{PORTAL_BASE}/keys/validate"
# Generate HMAC signature for portal request
timestamp = str(int(time.time()))
endpoint = "/keys/validate"
body = ""
signature_input = f"{timestamp}:POST:{endpoint}:{body}"
signature = hmac.new(
secret_key.encode('utf-8'),
signature_input.encode('utf-8'),
hashlib.sha256
).hexdigest()
headers = { headers = {
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {public_key}",
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
@ -332,16 +391,16 @@ def validate_key(api_key, extend=False):
def cmd_key(args): def cmd_key(args):
"""Validate API key""" """Validate API key"""
api_key = get_api_key(args.key) public_key, secret_key = get_api_keys(args.key)
validate_key(api_key, extend=args.extend) validate_key(public_key, secret_key, extend=args.extend)
def cmd_service(args): def cmd_service(args):
"""Manage persistent services""" """Manage persistent services"""
api_key = get_api_key(args.api_key) public_key, secret_key = get_api_keys(args.api_key)
if args.list: if args.list:
result = api_request("/services", api_key=api_key) result = api_request("/services", public_key=public_key, secret_key=secret_key)
services = result.get("services", []) services = result.get("services", [])
if not services: if not services:
print("No services") print("No services")
@ -354,38 +413,38 @@ def cmd_service(args):
return return
if args.info: if args.info:
result = api_request(f"/services/{args.info}", api_key=api_key) result = api_request(f"/services/{args.info}", public_key=public_key, secret_key=secret_key)
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
return return
if args.logs: if args.logs:
result = api_request(f"/services/{args.logs}/logs", api_key=api_key) result = api_request(f"/services/{args.logs}/logs", public_key=public_key, secret_key=secret_key)
print(result.get("logs", "")) print(result.get("logs", ""))
return return
if args.tail: if args.tail:
result = api_request(f"/services/{args.tail}/logs?lines=9000", api_key=api_key) result = api_request(f"/services/{args.tail}/logs?lines=9000", public_key=public_key, secret_key=secret_key)
print(result.get("logs", "")) print(result.get("logs", ""))
return return
if args.sleep: if args.sleep:
result = api_request(f"/services/{args.sleep}/sleep", method="POST", api_key=api_key) result = api_request(f"/services/{args.sleep}/sleep", method="POST", public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Service sleeping: {args.sleep}{RESET}") print(f"{GREEN}Service sleeping: {args.sleep}{RESET}")
return return
if args.wake: if args.wake:
result = api_request(f"/services/{args.wake}/wake", method="POST", api_key=api_key) result = api_request(f"/services/{args.wake}/wake", method="POST", public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Service waking: {args.wake}{RESET}") print(f"{GREEN}Service waking: {args.wake}{RESET}")
return return
if args.destroy: if args.destroy:
result = api_request(f"/services/{args.destroy}", method="DELETE", api_key=api_key) result = api_request(f"/services/{args.destroy}", method="DELETE", public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Service destroyed: {args.destroy}{RESET}") print(f"{GREEN}Service destroyed: {args.destroy}{RESET}")
return return
if args.execute: if args.execute:
payload = {"command": args.command} payload = {"command": args.command}
result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, api_key=api_key) result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
if result.get("stdout"): if result.get("stdout"):
print(f"{BLUE}{result['stdout']}{RESET}", end='') print(f"{BLUE}{result['stdout']}{RESET}", end='')
if result.get("stderr"): if result.get("stderr"):
@ -395,7 +454,7 @@ def cmd_service(args):
if args.dump_bootstrap: if args.dump_bootstrap:
print(f"Fetching bootstrap script from {args.dump_bootstrap}...", file=sys.stderr) print(f"Fetching bootstrap script from {args.dump_bootstrap}...", file=sys.stderr)
payload = {"command": "cat /tmp/bootstrap.sh"} payload = {"command": "cat /tmp/bootstrap.sh"}
result = api_request(f"/services/{args.dump_bootstrap}/execute", method="POST", data=payload, api_key=api_key) result = api_request(f"/services/{args.dump_bootstrap}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
if result.get("stdout"): if result.get("stdout"):
bootstrap = result["stdout"] bootstrap = result["stdout"]
@ -438,7 +497,7 @@ def cmd_service(args):
if args.vcpu: if args.vcpu:
payload["vcpu"] = args.vcpu payload["vcpu"] = args.vcpu
result = api_request("/services", method="POST", data=payload, api_key=api_key) result = api_request("/services", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Service created: {result.get('id', 'N/A')}{RESET}") print(f"{GREEN}Service created: {result.get('id', 'N/A')}{RESET}")
print(f"Name: {result.get('name', 'N/A')}") print(f"Name: {result.get('name', 'N/A')}")
if result.get('url'): if result.get('url'):

112
un.r
View file

@ -38,6 +38,7 @@
library(httr) library(httr)
library(jsonlite) library(jsonlite)
library(digest)
# Extension to language mapping # Extension to language mapping
ext_map <- list( ext_map <- list(
@ -75,28 +76,53 @@ detect_language <- function(filename) {
return(lang) return(lang)
} }
get_api_key <- function(args_key = NULL) { get_api_keys <- function(args_key = NULL) {
key <- if (!is.null(args_key)) args_key else Sys.getenv("UNSANDBOX_API_KEY") public_key <- Sys.getenv("UNSANDBOX_PUBLIC_KEY")
if (key == "") { secret_key <- Sys.getenv("UNSANDBOX_SECRET_KEY")
cat(sprintf("%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET), file = stderr())
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if (public_key == "" && Sys.getenv("UNSANDBOX_API_KEY") != "") {
public_key <- Sys.getenv("UNSANDBOX_API_KEY")
secret_key <- ""
}
if (public_key == "") {
cat(sprintf("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s\n", RED, RESET), file = stderr())
quit(status = 1) quit(status = 1)
} }
return(key) return(list(public_key = public_key, secret_key = secret_key))
} }
api_request <- function(endpoint, api_key, method = "GET", data = NULL) { api_request <- function(endpoint, public_key, secret_key, method = "GET", data = NULL) {
url <- paste0(API_BASE, endpoint) url <- paste0(API_BASE, endpoint)
headers <- add_headers( headers <- add_headers(
`Content-Type` = "application/json", `Content-Type` = "application/json",
`Authorization` = paste("Bearer", api_key) `Authorization` = paste("Bearer", public_key)
) )
body_content <- ""
if (!is.null(data)) {
body_content <- toJSON(data, auto_unbox = TRUE)
}
# Add HMAC signature if secret_key is present
if (secret_key != "") {
timestamp <- as.integer(Sys.time())
sig_input <- paste0(timestamp, ":", method, ":", endpoint, ":", body_content)
signature <- hmac(sig_input, secret_key, algo = "sha256")
headers <- add_headers(
`Content-Type` = "application/json",
`Authorization` = paste("Bearer", public_key),
`X-Timestamp` = as.character(timestamp),
`X-Signature` = signature
)
}
tryCatch({ tryCatch({
if (method == "GET") { if (method == "GET") {
response <- GET(url, headers, timeout(300)) response <- GET(url, headers, timeout(300))
} else if (method == "POST") { } else if (method == "POST") {
body <- if (!is.null(data)) toJSON(data, auto_unbox = TRUE) else "" response <- POST(url, headers, body = body_content, encode = "raw", timeout(300))
response <- POST(url, headers, body = body, encode = "raw", timeout(300))
} else if (method == "DELETE") { } else if (method == "DELETE") {
response <- DELETE(url, headers, timeout(300)) response <- DELETE(url, headers, timeout(300))
} else { } else {
@ -112,7 +138,9 @@ api_request <- function(endpoint, api_key, method = "GET", data = NULL) {
} }
cmd_execute <- function(args) { cmd_execute <- function(args) {
api_key <- get_api_key(args$api_key) keys <- get_api_keys(args$api_key)
public_key <- keys$public_key
secret_key <- keys$secret_key
filename <- args$source_file filename <- args$source_file
if (!file.exists(filename)) { if (!file.exists(filename)) {
@ -175,7 +203,7 @@ cmd_execute <- function(args) {
} }
# Execute # Execute
result <- api_request("/execute", api_key, method = "POST", data = payload) result <- api_request("/execute", public_key, secret_key, method = "POST", data = payload)
# Print output # Print output
if (!is.null(result$stdout) && result$stdout != "") { if (!is.null(result$stdout) && result$stdout != "") {
@ -204,10 +232,12 @@ cmd_execute <- function(args) {
} }
cmd_session <- function(args) { cmd_session <- function(args) {
api_key <- get_api_key(args$api_key) keys <- get_api_keys(args$api_key)
public_key <- keys$public_key
secret_key <- keys$secret_key
if (!is.null(args$list) && args$list) { if (!is.null(args$list) && args$list) {
result <- api_request("/sessions", api_key) result <- api_request("/sessions", public_key, secret_key)
sessions <- if (!is.null(result$sessions)) result$sessions else list() sessions <- if (!is.null(result$sessions)) result$sessions else list()
if (length(sessions) == 0) { if (length(sessions) == 0) {
cat("No active sessions\n") cat("No active sessions\n")
@ -225,7 +255,7 @@ cmd_session <- function(args) {
} }
if (!is.null(args$kill)) { if (!is.null(args$kill)) {
result <- api_request(paste0("/sessions/", args$kill), api_key, method = "DELETE") result <- api_request(paste0("/sessions/", args$kill), public_key, secret_key, method = "DELETE")
cat(sprintf("%sSession terminated: %s%s\n", GREEN, args$kill, RESET)) cat(sprintf("%sSession terminated: %s%s\n", GREEN, args$kill, RESET))
return() return()
} }
@ -235,16 +265,31 @@ cmd_session <- function(args) {
} }
cmd_key <- function(args) { cmd_key <- function(args) {
api_key <- get_api_key(args$api_key) keys <- get_api_keys(args$api_key)
public_key <- keys$public_key
secret_key <- keys$secret_key
if (!is.null(args$extend) && args$extend) { if (!is.null(args$extend) && args$extend) {
# First validate to get public_key # First validate to get public_key
url <- paste0(PORTAL_BASE, "/keys/validate") url <- paste0(PORTAL_BASE, "/keys/validate")
headers <- add_headers( headers <- add_headers(
`Content-Type` = "application/json", `Content-Type` = "application/json",
`Authorization` = paste("Bearer", api_key) `Authorization` = paste("Bearer", public_key)
) )
# Add HMAC signature if secret_key is present
if (secret_key != "") {
timestamp <- as.integer(Sys.time())
sig_input <- paste0(timestamp, ":POST:/keys/validate:")
signature <- hmac(sig_input, secret_key, algo = "sha256")
headers <- add_headers(
`Content-Type` = "application/json",
`Authorization` = paste("Bearer", public_key),
`X-Timestamp` = as.character(timestamp),
`X-Signature` = signature
)
}
tryCatch({ tryCatch({
response <- POST(url, headers, encode = "json", timeout(10)) response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8")) result <- fromJSON(content(response, "text", encoding = "UTF-8"))
@ -268,9 +313,22 @@ cmd_key <- function(args) {
url <- paste0(PORTAL_BASE, "/keys/validate") url <- paste0(PORTAL_BASE, "/keys/validate")
headers <- add_headers( headers <- add_headers(
`Content-Type` = "application/json", `Content-Type` = "application/json",
`Authorization` = paste("Bearer", api_key) `Authorization` = paste("Bearer", public_key)
) )
# Add HMAC signature if secret_key is present
if (secret_key != "") {
timestamp <- as.integer(Sys.time())
sig_input <- paste0(timestamp, ":POST:/keys/validate:")
signature <- hmac(sig_input, secret_key, algo = "sha256")
headers <- add_headers(
`Content-Type` = "application/json",
`Authorization` = paste("Bearer", public_key),
`X-Timestamp` = as.character(timestamp),
`X-Signature` = signature
)
}
tryCatch({ tryCatch({
response <- POST(url, headers, encode = "json", timeout(10)) response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8")) result <- fromJSON(content(response, "text", encoding = "UTF-8"))
@ -302,10 +360,12 @@ cmd_key <- function(args) {
} }
cmd_service <- function(args) { cmd_service <- function(args) {
api_key <- get_api_key(args$api_key) keys <- get_api_keys(args$api_key)
public_key <- keys$public_key
secret_key <- keys$secret_key
if (!is.null(args$list) && args$list) { if (!is.null(args$list) && args$list) {
result <- api_request("/services", api_key) result <- api_request("/services", public_key, secret_key)
services <- if (!is.null(result$services)) result$services else list() services <- if (!is.null(result$services)) result$services else list()
if (length(services) == 0) { if (length(services) == 0) {
cat("No services\n") cat("No services\n")
@ -325,31 +385,31 @@ cmd_service <- function(args) {
} }
if (!is.null(args$info)) { if (!is.null(args$info)) {
result <- api_request(paste0("/services/", args$info), api_key) result <- api_request(paste0("/services/", args$info), public_key, secret_key)
cat(toJSON(result, pretty = TRUE, auto_unbox = TRUE), "\n") cat(toJSON(result, pretty = TRUE, auto_unbox = TRUE), "\n")
return() return()
} }
if (!is.null(args$logs)) { if (!is.null(args$logs)) {
result <- api_request(paste0("/services/", args$logs, "/logs"), api_key) result <- api_request(paste0("/services/", args$logs, "/logs"), public_key, secret_key)
cat(if (!is.null(result$logs)) result$logs else "", "\n") cat(if (!is.null(result$logs)) result$logs else "", "\n")
return() return()
} }
if (!is.null(args$sleep)) { if (!is.null(args$sleep)) {
result <- api_request(paste0("/services/", args$sleep, "/sleep"), api_key, method = "POST") result <- api_request(paste0("/services/", args$sleep, "/sleep"), public_key, secret_key, method = "POST")
cat(sprintf("%sService sleeping: %s%s\n", GREEN, args$sleep, RESET)) cat(sprintf("%sService sleeping: %s%s\n", GREEN, args$sleep, RESET))
return() return()
} }
if (!is.null(args$wake)) { if (!is.null(args$wake)) {
result <- api_request(paste0("/services/", args$wake, "/wake"), api_key, method = "POST") result <- api_request(paste0("/services/", args$wake, "/wake"), public_key, secret_key, method = "POST")
cat(sprintf("%sService waking: %s%s\n", GREEN, args$wake, RESET)) cat(sprintf("%sService waking: %s%s\n", GREEN, args$wake, RESET))
return() return()
} }
if (!is.null(args$destroy)) { if (!is.null(args$destroy)) {
result <- api_request(paste0("/services/", args$destroy), api_key, method = "DELETE") result <- api_request(paste0("/services/", args$destroy), public_key, secret_key, method = "DELETE")
cat(sprintf("%sService destroyed: %s%s\n", GREEN, args$destroy, RESET)) cat(sprintf("%sService destroyed: %s%s\n", GREEN, args$destroy, RESET))
return() return()
} }
@ -357,7 +417,7 @@ cmd_service <- function(args) {
if (!is.null(args$dump_bootstrap)) { if (!is.null(args$dump_bootstrap)) {
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr()) cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
payload <- list(command = "cat /tmp/bootstrap.sh") payload <- list(command = "cat /tmp/bootstrap.sh")
result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), api_key, method = "POST", data = payload) result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), public_key, secret_key, method = "POST", data = payload)
if (!is.null(result$stdout) && result$stdout != "") { if (!is.null(result$stdout) && result$stdout != "") {
bootstrap <- result$stdout bootstrap <- result$stdout
@ -415,7 +475,7 @@ cmd_service <- function(args) {
payload$vcpu <- args$vcpu payload$vcpu <- args$vcpu
} }
result <- api_request("/services", api_key, method = "POST", data = payload) result <- api_request("/services", public_key, secret_key, method = "POST", data = payload)
cat(sprintf("%sService created: %s%s\n", GREEN, if (!is.null(result$id)) result$id else "N/A", RESET)) cat(sprintf("%sService created: %s%s\n", GREEN, if (!is.null(result$id)) result$id else "N/A", RESET))
cat(sprintf("Name: %s\n", if (!is.null(result$name)) result$name else "N/A")) cat(sprintf("Name: %s\n", if (!is.null(result$name)) result$name else "N/A"))
if (!is.null(result$url)) { if (!is.null(result$url)) {

80
un.raku
View file

@ -40,6 +40,7 @@
# Full-featured CLI matching un.c/un.py capabilities # Full-featured CLI matching un.c/un.py capabilities
use JSON::Fast; use JSON::Fast;
use Digest::SHA;
constant $API_BASE = "https://api.unsandbox.com"; constant $API_BASE = "https://api.unsandbox.com";
constant $PORTAL_BASE = "https://unsandbox.com"; constant $PORTAL_BASE = "https://unsandbox.com";
@ -66,13 +67,21 @@ my %EXT_MAP = (
m => 'objc' m => 'objc'
); );
sub get-api-key() { sub get-api-keys() {
my $key = %*ENV<UNSANDBOX_API_KEY>; my $public-key = %*ENV<UNSANDBOX_PUBLIC_KEY> // '';
unless $key { my $secret-key = %*ENV<UNSANDBOX_SECRET_KEY> // '';
note "{$RED}Error: UNSANDBOX_API_KEY not set{$RESET}";
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if !$public-key && %*ENV<UNSANDBOX_API_KEY> {
$public-key = %*ENV<UNSANDBOX_API_KEY>;
$secret-key = '';
}
unless $public-key {
note "{$RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set{$RESET}";
exit 1; exit 1;
} }
return $key; return ($public-key, $secret-key);
} }
sub detect-language(Str $filename --> Str) { sub detect-language(Str $filename --> Str) {
@ -98,9 +107,10 @@ sub detect-language(Str $filename --> Str) {
exit 1; exit 1;
} }
sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) { sub api-request(Str $endpoint, Str $method, %data?, Str :$public-key!, Str :$secret-key!) {
my $url = $API_BASE ~ $endpoint; my $url = $API_BASE ~ $endpoint;
my @args = 'curl', '-s'; my @args = 'curl', '-s';
my $body = '';
if $method eq 'GET' { if $method eq 'GET' {
@args.append: '-X', 'GET'; @args.append: '-X', 'GET';
@ -110,11 +120,22 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) {
@args.append: '-X', 'POST'; @args.append: '-X', 'POST';
@args.append: '-H', 'Content-Type: application/json'; @args.append: '-H', 'Content-Type: application/json';
if %data { if %data {
@args.append: '-d', to-json(%data); $body = to-json(%data);
@args.append: '-d', $body;
} }
} }
@args.append: '-H', "Authorization: Bearer $api-key"; @args.append: '-H', "Authorization: Bearer $public-key";
# Add HMAC signature if secret-key is present
if $secret-key {
my $timestamp = now.Int;
my $sig-input = "{$timestamp}:{$method}:{$endpoint}:{$body}";
my $signature = hmac-hex($sig-input, $secret-key, &sha256);
@args.append: '-H', "X-Timestamp: $timestamp";
@args.append: '-H', "X-Signature: $signature";
}
@args.append: $url; @args.append: $url;
my $proc = run |@args, :out, :err; my $proc = run |@args, :out, :err;
@ -131,7 +152,7 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) {
} }
sub cmd-execute(@args) { sub cmd-execute(@args) {
my $api-key = get-api-key(); my ($public-key, $secret-key) = get-api-keys();
my $source-file = ''; my $source-file = '';
my %env-vars; my %env-vars;
my @input-files; my @input-files;
@ -218,7 +239,7 @@ sub cmd-execute(@args) {
%payload<vcpu> = $vcpu if $vcpu > 0; %payload<vcpu> = $vcpu if $vcpu > 0;
# Execute # Execute
my %result = api-request('/execute', 'POST', %payload, :$api-key); my %result = api-request('/execute', 'POST', %payload, :$public-key, :$secret-key);
# Print output # Print output
if %result<stdout> { if %result<stdout> {
@ -245,7 +266,7 @@ sub cmd-execute(@args) {
} }
sub cmd-session(@args) { sub cmd-session(@args) {
my $api-key = get-api-key(); my ($public-key, $secret-key) = get-api-keys();
my $list-mode = False; my $list-mode = False;
my $kill-id = ''; my $kill-id = '';
my $shell = ''; my $shell = '';
@ -280,7 +301,7 @@ sub cmd-session(@args) {
} }
if $list-mode { if $list-mode {
my %result = api-request('/sessions', 'GET', :$api-key); my %result = api-request('/sessions', 'GET', :$public-key, :$secret-key);
my @sessions = %result<sessions>.list; my @sessions = %result<sessions>.list;
unless @sessions { unless @sessions {
say "No active sessions"; say "No active sessions";
@ -295,7 +316,7 @@ sub cmd-session(@args) {
} }
if $kill-id { if $kill-id {
api-request("/sessions/$kill-id", 'DELETE', :$api-key); api-request("/sessions/$kill-id", 'DELETE', :$public-key, :$secret-key);
say "{$GREEN}Session terminated: $kill-id{$RESET}"; say "{$GREEN}Session terminated: $kill-id{$RESET}";
return; return;
} }
@ -306,13 +327,13 @@ sub cmd-session(@args) {
%payload<vcpu> = $vcpu if $vcpu > 0; %payload<vcpu> = $vcpu if $vcpu > 0;
say "{$YELLOW}Creating session...{$RESET}"; say "{$YELLOW}Creating session...{$RESET}";
my %result = api-request('/sessions', 'POST', %payload, :$api-key); my %result = api-request('/sessions', 'POST', %payload, :$public-key, :$secret-key);
say "{$GREEN}Session created: {%result<id>}{$RESET}"; say "{$GREEN}Session created: {%result<id>}{$RESET}";
say "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}"; say "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}";
} }
sub cmd-service(@args) { sub cmd-service(@args) {
my $api-key = get-api-key(); my ($public-key, $secret-key) = get-api-keys();
my $list-mode = False; my $list-mode = False;
my $info-id = ''; my $info-id = '';
my $logs-id = ''; my $logs-id = '';
@ -392,7 +413,7 @@ sub cmd-service(@args) {
} }
if $list-mode { if $list-mode {
my %result = api-request('/services', 'GET', :$api-key); my %result = api-request('/services', 'GET', :$public-key, :$secret-key);
my @services = %result<services>.list; my @services = %result<services>.list;
unless @services { unless @services {
say "No services"; say "No services";
@ -409,31 +430,31 @@ sub cmd-service(@args) {
} }
if $info-id { if $info-id {
my %result = api-request("/services/$info-id", 'GET', :$api-key); my %result = api-request("/services/$info-id", 'GET', :$public-key, :$secret-key);
say to-json(%result, :pretty); say to-json(%result, :pretty);
return; return;
} }
if $logs-id { if $logs-id {
my %result = api-request("/services/$logs-id/logs", 'GET', :$api-key); my %result = api-request("/services/$logs-id/logs", 'GET', :$public-key, :$secret-key);
say %result<logs>; say %result<logs>;
return; return;
} }
if $sleep-id { if $sleep-id {
api-request("/services/$sleep-id/sleep", 'POST', :$api-key); api-request("/services/$sleep-id/sleep", 'POST', :$public-key, :$secret-key);
say "{$GREEN}Service sleeping: $sleep-id{$RESET}"; say "{$GREEN}Service sleeping: $sleep-id{$RESET}";
return; return;
} }
if $wake-id { if $wake-id {
api-request("/services/$wake-id/wake", 'POST', :$api-key); api-request("/services/$wake-id/wake", 'POST', :$public-key, :$secret-key);
say "{$GREEN}Service waking: $wake-id{$RESET}"; say "{$GREEN}Service waking: $wake-id{$RESET}";
return; return;
} }
if $destroy-id { if $destroy-id {
api-request("/services/$destroy-id", 'DELETE', :$api-key); api-request("/services/$destroy-id", 'DELETE', :$public-key, :$secret-key);
say "{$GREEN}Service destroyed: $destroy-id{$RESET}"; say "{$GREEN}Service destroyed: $destroy-id{$RESET}";
return; return;
} }
@ -441,7 +462,7 @@ sub cmd-service(@args) {
if $dump-bootstrap-id { if $dump-bootstrap-id {
note "Fetching bootstrap script from $dump-bootstrap-id..."; note "Fetching bootstrap script from $dump-bootstrap-id...";
my %payload = command => "cat /tmp/bootstrap.sh"; my %payload = command => "cat /tmp/bootstrap.sh";
my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$api-key); my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$public-key, :$secret-key);
if %result<stdout> && %result<stdout> ne '' { if %result<stdout> && %result<stdout> ne '' {
my $bootstrap = %result<stdout>; my $bootstrap = %result<stdout>;
@ -485,7 +506,7 @@ sub cmd-service(@args) {
%payload<network> = $network if $network; %payload<network> = $network if $network;
%payload<vcpu> = $vcpu if $vcpu > 0; %payload<vcpu> = $vcpu if $vcpu > 0;
my %result = api-request('/services', 'POST', %payload, :$api-key); my %result = api-request('/services', 'POST', %payload, :$public-key, :$secret-key);
say "{$GREEN}Service created: {%result<id>}{$RESET}"; say "{$GREEN}Service created: {%result<id>}{$RESET}";
say "Name: {%result<name>}"; say "Name: {%result<name>}";
say "URL: {%result<url>}" if %result<url>; say "URL: {%result<url>}" if %result<url>;
@ -497,13 +518,22 @@ sub cmd-service(@args) {
} }
sub validate-key(Bool $extend) { sub validate-key(Bool $extend) {
my $api-key = get-api-key(); my ($public-key, $secret-key) = get-api-keys();
# Build curl command # Build curl command
my @args = 'curl', '-s', '-X', 'POST'; my @args = 'curl', '-s', '-X', 'POST';
@args.append: "$PORTAL_BASE/keys/validate"; @args.append: "$PORTAL_BASE/keys/validate";
@args.append: '-H', 'Content-Type: application/json'; @args.append: '-H', 'Content-Type: application/json';
@args.append: '-H', "Authorization: Bearer $api-key"; @args.append: '-H', "Authorization: Bearer $public-key";
# Add HMAC signature if secret-key is present
if $secret-key {
my $timestamp = now.Int;
my $sig-input = "{$timestamp}:POST:/keys/validate:";
my $signature = hmac-hex($sig-input, $secret-key, &sha256);
@args.append: '-H', "X-Timestamp: $timestamp";
@args.append: '-H', "X-Signature: $signature";
}
my $proc = run |@args, :out, :err; my $proc = run |@args, :out, :err;
my $body = $proc.out.slurp; my $body = $proc.out.slurp;

90
un.rb
View file

@ -80,13 +80,23 @@ EXT_MAP = {
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc' '.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
}.freeze }.freeze
def get_api_key(args_key = nil) def get_api_keys(args_key = nil)
key = args_key || ENV['UNSANDBOX_API_KEY'] public_key = ENV['UNSANDBOX_PUBLIC_KEY']
unless key secret_key = ENV['UNSANDBOX_SECRET_KEY']
warn "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
exit 1 unless public_key && secret_key
old_key = args_key || ENV['UNSANDBOX_API_KEY']
if old_key
public_key = old_key
secret_key = old_key
else
warn "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
warn "#{RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)#{RESET}"
exit 1
end
end end
key
{ public_key: public_key, secret_key: secret_key }
end end
def detect_language(filename) def detect_language(filename)
@ -112,12 +122,19 @@ def detect_language(filename)
lang lang
end end
def api_request(endpoint, method: 'GET', data: nil, api_key:) def api_request(endpoint, method: 'GET', data: nil, keys:)
require 'openssl'
uri = URI("#{API_BASE}#{endpoint}") uri = URI("#{API_BASE}#{endpoint}")
http = Net::HTTP.new(uri.host, uri.port) http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true http.use_ssl = true
http.read_timeout = 300 http.read_timeout = 300
timestamp = Time.now.to_i.to_s
body = data ? JSON.generate(data) : ''
message = "#{timestamp}:#{method}:#{uri.path}#{uri.query ? "?#{uri.query}" : ''}:#{body}"
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
request = case method request = case method
when 'GET' then Net::HTTP::Get.new(uri) when 'GET' then Net::HTTP::Get.new(uri)
when 'POST' then Net::HTTP::Post.new(uri) when 'POST' then Net::HTTP::Post.new(uri)
@ -125,9 +142,11 @@ def api_request(endpoint, method: 'GET', data: nil, api_key:)
else raise "Unknown method: #{method}" else raise "Unknown method: #{method}"
end end
request['Authorization'] = "Bearer #{api_key}" request['Authorization'] = "Bearer #{keys[:public_key]}"
request['X-Timestamp'] = timestamp
request['X-Signature'] = signature
request['Content-Type'] = 'application/json' request['Content-Type'] = 'application/json'
request.body = JSON.generate(data) if data request.body = body if data
response = http.request(request) response = http.request(request)
unless response.is_a?(Net::HTTPSuccess) unless response.is_a?(Net::HTTPSuccess)
@ -142,7 +161,7 @@ rescue => e
end end
def cmd_execute(options) def cmd_execute(options)
api_key = get_api_key(options[:api_key]) keys = get_api_keys(options[:api_key])
unless File.exist?(options[:source_file]) unless File.exist?(options[:source_file])
warn "#{RED}Error: File not found: #{options[:source_file]}#{RESET}" warn "#{RED}Error: File not found: #{options[:source_file]}#{RESET}"
@ -181,7 +200,7 @@ def cmd_execute(options)
payload[:network] = options[:network] if options[:network] payload[:network] = options[:network] if options[:network]
payload[:vcpu] = options[:vcpu] if options[:vcpu] payload[:vcpu] = options[:vcpu] if options[:vcpu]
result = api_request('/execute', method: 'POST', data: payload, api_key: api_key) result = api_request('/execute', method: 'POST', data: payload, keys: keys)
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout'] print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr'] $stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
@ -203,10 +222,10 @@ def cmd_execute(options)
end end
def cmd_session(options) def cmd_session(options)
api_key = get_api_key(options[:api_key]) keys = get_api_keys(options[:api_key])
if options[:list] if options[:list]
result = api_request('/sessions', api_key: api_key) result = api_request('/sessions', keys: keys)
sessions = result['sessions'] || [] sessions = result['sessions'] || []
if sessions.empty? if sessions.empty?
puts 'No active sessions' puts 'No active sessions'
@ -222,7 +241,7 @@ def cmd_session(options)
end end
if options[:kill] if options[:kill]
api_request("/sessions/#{options[:kill]}", method: 'DELETE', api_key: api_key) api_request("/sessions/#{options[:kill]}", method: 'DELETE', keys: keys)
puts "#{GREEN}Session terminated: #{options[:kill]}#{RESET}" puts "#{GREEN}Session terminated: #{options[:kill]}#{RESET}"
return return
end end
@ -241,19 +260,28 @@ def cmd_session(options)
payload[:audit] = true if options[:audit] payload[:audit] = true if options[:audit]
puts "#{YELLOW}Creating session...#{RESET}" puts "#{YELLOW}Creating session...#{RESET}"
result = api_request('/sessions', method: 'POST', data: payload, api_key: api_key) result = api_request('/sessions', method: 'POST', data: payload, keys: keys)
puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}" puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}"
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}" puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
end end
def validate_key(api_key) def validate_key(keys)
require 'openssl'
uri = URI("#{PORTAL_BASE}/keys/validate") uri = URI("#{PORTAL_BASE}/keys/validate")
http = Net::HTTP.new(uri.host, uri.port) http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true http.use_ssl = true
http.read_timeout = 30 http.read_timeout = 30
timestamp = Time.now.to_i.to_s
body = ''
message = "#{timestamp}:POST:#{uri.path}:#{body}"
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
request = Net::HTTP::Post.new(uri) request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{api_key}" request['Authorization'] = "Bearer #{keys[:public_key]}"
request['X-Timestamp'] = timestamp
request['X-Signature'] = signature
request['Content-Type'] = 'application/json' request['Content-Type'] = 'application/json'
response = http.request(request) response = http.request(request)
@ -308,10 +336,10 @@ def open_browser(url)
end end
def cmd_key(options) def cmd_key(options)
api_key = get_api_key(options[:api_key]) keys = get_api_keys(options[:api_key])
if options[:extend] if options[:extend]
result = validate_key(api_key) result = validate_key(keys)
public_key = result['public_key'] public_key = result['public_key']
if public_key if public_key
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}" url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
@ -322,15 +350,15 @@ def cmd_key(options)
exit 1 exit 1
end end
else else
validate_key(api_key) validate_key(keys)
end end
end end
def cmd_service(options) def cmd_service(options)
api_key = get_api_key(options[:api_key]) keys = get_api_keys(options[:api_key])
if options[:list] if options[:list]
result = api_request('/services', api_key: api_key) result = api_request('/services', keys: keys)
services = result['services'] || [] services = result['services'] || []
if services.empty? if services.empty?
puts 'No services' puts 'No services'
@ -348,44 +376,44 @@ def cmd_service(options)
end end
if options[:info] if options[:info]
result = api_request("/services/#{options[:info]}", api_key: api_key) result = api_request("/services/#{options[:info]}", keys: keys)
puts JSON.pretty_generate(result) puts JSON.pretty_generate(result)
return return
end end
if options[:logs] if options[:logs]
result = api_request("/services/#{options[:logs]}/logs", api_key: api_key) result = api_request("/services/#{options[:logs]}/logs", keys: keys)
puts result['logs'] || '' puts result['logs'] || ''
return return
end end
if options[:tail] if options[:tail]
result = api_request("/services/#{options[:tail]}/logs?lines=9000", api_key: api_key) result = api_request("/services/#{options[:tail]}/logs?lines=9000", keys: keys)
puts result['logs'] || '' puts result['logs'] || ''
return return
end end
if options[:sleep] if options[:sleep]
api_request("/services/#{options[:sleep]}/sleep", method: 'POST', api_key: api_key) api_request("/services/#{options[:sleep]}/sleep", method: 'POST', keys: keys)
puts "#{GREEN}Service sleeping: #{options[:sleep]}#{RESET}" puts "#{GREEN}Service sleeping: #{options[:sleep]}#{RESET}"
return return
end end
if options[:wake] if options[:wake]
api_request("/services/#{options[:wake]}/wake", method: 'POST', api_key: api_key) api_request("/services/#{options[:wake]}/wake", method: 'POST', keys: keys)
puts "#{GREEN}Service waking: #{options[:wake]}#{RESET}" puts "#{GREEN}Service waking: #{options[:wake]}#{RESET}"
return return
end end
if options[:destroy] if options[:destroy]
api_request("/services/#{options[:destroy]}", method: 'DELETE', api_key: api_key) api_request("/services/#{options[:destroy]}", method: 'DELETE', keys: keys)
puts "#{GREEN}Service destroyed: #{options[:destroy]}#{RESET}" puts "#{GREEN}Service destroyed: #{options[:destroy]}#{RESET}"
return return
end end
if options[:execute] if options[:execute]
payload = { command: options[:command] } payload = { command: options[:command] }
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, api_key: api_key) result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, keys: keys)
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout'] print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr'] $stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
return return
@ -394,7 +422,7 @@ def cmd_service(options)
if options[:dump_bootstrap] if options[:dump_bootstrap]
warn "Fetching bootstrap script from #{options[:dump_bootstrap]}..." warn "Fetching bootstrap script from #{options[:dump_bootstrap]}..."
payload = { command: 'cat /tmp/bootstrap.sh' } payload = { command: 'cat /tmp/bootstrap.sh' }
result = api_request("/services/#{options[:dump_bootstrap]}/execute", method: 'POST', data: payload, api_key: api_key) result = api_request("/services/#{options[:dump_bootstrap]}/execute", method: 'POST', data: payload, keys: keys)
if result['stdout'] if result['stdout']
bootstrap = result['stdout'] bootstrap = result['stdout']
@ -434,7 +462,7 @@ def cmd_service(options)
payload[:network] = options[:network] if options[:network] payload[:network] = options[:network] if options[:network]
payload[:vcpu] = options[:vcpu] if options[:vcpu] payload[:vcpu] = options[:vcpu] if options[:vcpu]
result = api_request('/services', method: 'POST', data: payload, api_key: api_key) result = api_request('/services', method: 'POST', data: payload, keys: keys)
puts "#{GREEN}Service created: #{result['id'] || 'N/A'}#{RESET}" puts "#{GREEN}Service created: #{result['id'] || 'N/A'}#{RESET}"
puts "Name: #{result['name'] || 'N/A'}" puts "Name: #{result['name'] || 'N/A'}"
puts "URL: #{result['url']}" if result['url'] puts "URL: #{result['url']}" if result['url']

118
un.rs
View file

@ -49,6 +49,7 @@ use std::fs;
use std::path::Path; use std::path::Path;
use std::process::{self, Command}; use std::process::{self, Command};
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
const API_BASE: &str = "https://api.unsandbox.com"; const API_BASE: &str = "https://api.unsandbox.com";
const PORTAL_BASE: &str = "https://unsandbox.com"; const PORTAL_BASE: &str = "https://unsandbox.com";
@ -109,14 +110,38 @@ fn detect_language(filename: &str) -> Option<&'static str> {
} }
} }
fn get_api_key(key_arg: Option<&str>) -> String { fn get_api_keys(key_arg: Option<&str>) -> (String, String) {
if let Some(k) = key_arg { let public_key = env::var("UNSANDBOX_PUBLIC_KEY").ok();
return k.to_string(); let secret_key = env::var("UNSANDBOX_SECRET_KEY").ok();
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
if public_key.is_none() || secret_key.is_none() {
let fallback_key = if let Some(k) = key_arg {
k.to_string()
} else {
env::var("UNSANDBOX_API_KEY").unwrap_or_else(|_| {
eprintln!("{}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat){}", RED, RESET);
process::exit(1);
})
};
return (fallback_key.clone(), fallback_key);
} }
env::var("UNSANDBOX_API_KEY").unwrap_or_else(|_| {
eprintln!("{}Error: UNSANDBOX_API_KEY not set{}", RED, RESET); (public_key.unwrap(), secret_key.unwrap())
process::exit(1); }
})
fn compute_hmac(secret_key: &str, timestamp: &str, method: &str, path: &str, body: &str) -> String {
use std::process::Command;
let message = format!("{}:{}:{}:{}", timestamp, method, path, body);
// Use openssl for HMAC-SHA256
let output = Command::new("sh")
.arg("-c")
.arg(format!("printf '%s' '{}' | openssl dgst -sha256 -hmac '{}' | cut -d' ' -f2", message, secret_key))
.output()
.expect("Failed to compute HMAC");
String::from_utf8_lossy(&output.stdout).trim().to_string()
} }
fn escape_json(s: &str) -> String { fn escape_json(s: &str) -> String {
@ -163,8 +188,18 @@ fn extract_json_int(json: &str, key: &str) -> i32 {
1 1
} }
fn api_request(endpoint: &str, method: &str, body: Option<&str>, api_key: &str) -> String { fn api_request(endpoint: &str, method: &str, body: Option<&str>, public_key: &str, secret_key: &str) -> String {
let url = format!("{}{}", API_BASE, endpoint); let url = format!("{}{}", API_BASE, endpoint);
let body_str = body.unwrap_or("");
// Compute HMAC signature
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let signature = compute_hmac(secret_key, &timestamp, method, endpoint, body_str);
let mut cmd = Command::new("curl"); let mut cmd = Command::new("curl");
cmd.arg("-s") cmd.arg("-s")
.arg("-X") .arg("-X")
@ -173,7 +208,11 @@ fn api_request(endpoint: &str, method: &str, body: Option<&str>, api_key: &str)
.arg("-H") .arg("-H")
.arg("Content-Type: application/json") .arg("Content-Type: application/json")
.arg("-H") .arg("-H")
.arg(format!("Authorization: Bearer {}", api_key)); .arg(format!("Authorization: Bearer {}", public_key))
.arg("-H")
.arg(format!("X-Timestamp: {}", timestamp))
.arg("-H")
.arg(format!("X-Signature: {}", signature));
if let Some(b) = body { if let Some(b) = body {
cmd.arg("-d").arg(b); cmd.arg("-d").arg(b);
@ -200,7 +239,8 @@ fn cmd_execute(
output_dir: Option<&str>, output_dir: Option<&str>,
network: Option<&str>, network: Option<&str>,
vcpu: Option<i32>, vcpu: Option<i32>,
api_key: &str, public_key: &str,
secret_key: &str,
) { ) {
let code = fs::read_to_string(source_file).unwrap_or_else(|e| { let code = fs::read_to_string(source_file).unwrap_or_else(|e| {
eprintln!("{}Error reading file: {}{}", RED, e, RESET); eprintln!("{}Error reading file: {}{}", RED, e, RESET);
@ -265,7 +305,7 @@ fn cmd_execute(
json.push('}'); json.push('}');
let result = api_request("/execute", "POST", Some(&json), api_key); let result = api_request("/execute", "POST", Some(&json), public_key, secret_key);
// Print output // Print output
let stdout_str = extract_json_string(&result, "stdout"); let stdout_str = extract_json_string(&result, "stdout");
@ -295,16 +335,17 @@ fn cmd_session(
vcpu: Option<i32>, vcpu: Option<i32>,
tmux: bool, tmux: bool,
screen: bool, screen: bool,
api_key: &str, public_key: &str,
secret_key: &str,
) { ) {
if list { if list {
let result = api_request("/sessions", "GET", None, api_key); let result = api_request("/sessions", "GET", None, public_key, secret_key);
println!("{}", result); println!("{}", result);
return; return;
} }
if let Some(id) = kill { if let Some(id) = kill {
api_request(&format!("/sessions/{}", id), "DELETE", None, api_key); api_request(&format!("/sessions/{}", id), "DELETE", None, public_key, secret_key);
println!("{}Session terminated: {}{}", GREEN, id, RESET); println!("{}Session terminated: {}{}", GREEN, id, RESET);
return; return;
} }
@ -330,7 +371,7 @@ fn cmd_session(
json.push('}'); json.push('}');
println!("{}Creating session...{}", YELLOW, RESET); println!("{}Creating session...{}", YELLOW, RESET);
let result = api_request("/sessions", "POST", Some(&json), api_key); let result = api_request("/sessions", "POST", Some(&json), public_key, secret_key);
let id = extract_json_string(&result, "id"); let id = extract_json_string(&result, "id");
println!("{}Session created: {}{}", GREEN, id, RESET); println!("{}Session created: {}{}", GREEN, id, RESET);
} }
@ -354,46 +395,47 @@ fn cmd_service(
dump_file: Option<&str>, dump_file: Option<&str>,
network: Option<&str>, network: Option<&str>,
vcpu: Option<i32>, vcpu: Option<i32>,
api_key: &str, public_key: &str,
secret_key: &str,
) { ) {
if list { if list {
let result = api_request("/services", "GET", None, api_key); let result = api_request("/services", "GET", None, public_key, secret_key);
println!("{}", result); println!("{}", result);
return; return;
} }
if let Some(id) = info { if let Some(id) = info {
let result = api_request(&format!("/services/{}", id), "GET", None, api_key); let result = api_request(&format!("/services/{}", id), "GET", None, public_key, secret_key);
println!("{}", result); println!("{}", result);
return; return;
} }
if let Some(id) = logs { if let Some(id) = logs {
let result = api_request(&format!("/services/{}/logs", id), "GET", None, api_key); let result = api_request(&format!("/services/{}/logs", id), "GET", None, public_key, secret_key);
println!("{}", extract_json_string(&result, "logs")); println!("{}", extract_json_string(&result, "logs"));
return; return;
} }
if let Some(id) = tail { if let Some(id) = tail {
let result = api_request(&format!("/services/{}/logs?lines=9000", id), "GET", None, api_key); let result = api_request(&format!("/services/{}/logs?lines=9000", id), "GET", None, public_key, secret_key);
println!("{}", extract_json_string(&result, "logs")); println!("{}", extract_json_string(&result, "logs"));
return; return;
} }
if let Some(id) = sleep { if let Some(id) = sleep {
api_request(&format!("/services/{}/sleep", id), "POST", None, api_key); api_request(&format!("/services/{}/sleep", id), "POST", None, public_key, secret_key);
println!("{}Service sleeping: {}{}", GREEN, id, RESET); println!("{}Service sleeping: {}{}", GREEN, id, RESET);
return; return;
} }
if let Some(id) = wake { if let Some(id) = wake {
api_request(&format!("/services/{}/wake", id), "POST", None, api_key); api_request(&format!("/services/{}/wake", id), "POST", None, public_key, secret_key);
println!("{}Service waking: {}{}", GREEN, id, RESET); println!("{}Service waking: {}{}", GREEN, id, RESET);
return; return;
} }
if let Some(id) = destroy { if let Some(id) = destroy {
api_request(&format!("/services/{}", id), "DELETE", None, api_key); api_request(&format!("/services/{}", id), "DELETE", None, public_key, secret_key);
println!("{}Service destroyed: {}{}", GREEN, id, RESET); println!("{}Service destroyed: {}{}", GREEN, id, RESET);
return; return;
} }
@ -401,7 +443,7 @@ fn cmd_service(
if let Some(id) = execute { if let Some(id) = execute {
let cmd = command.unwrap_or(""); let cmd = command.unwrap_or("");
let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd)); let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd));
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), api_key); let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), public_key, secret_key);
let stdout_str = extract_json_string(&result, "stdout"); let stdout_str = extract_json_string(&result, "stdout");
let stderr_str = extract_json_string(&result, "stderr"); let stderr_str = extract_json_string(&result, "stderr");
if !stdout_str.is_empty() { if !stdout_str.is_empty() {
@ -416,7 +458,7 @@ fn cmd_service(
if let Some(id) = dump_bootstrap { if let Some(id) = dump_bootstrap {
eprintln!("Fetching bootstrap script from {}...", id); eprintln!("Fetching bootstrap script from {}...", id);
let json = r#"{"command":"cat /tmp/bootstrap.sh"}"#; let json = r#"{"command":"cat /tmp/bootstrap.sh"}"#;
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), api_key); let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), public_key, secret_key);
let bootstrap = extract_json_string(&result, "stdout"); let bootstrap = extract_json_string(&result, "stdout");
if !bootstrap.is_empty() { if !bootstrap.is_empty() {
@ -495,7 +537,7 @@ fn cmd_service(
json.push('}'); json.push('}');
let result = api_request("/services", "POST", Some(&json), api_key); let result = api_request("/services", "POST", Some(&json), public_key, secret_key);
let id = extract_json_string(&result, "id"); let id = extract_json_string(&result, "id");
println!("{}Service created: {}{}", GREEN, id, RESET); println!("{}Service created: {}{}", GREEN, id, RESET);
return; return;
@ -505,8 +547,8 @@ fn cmd_service(
process::exit(1); process::exit(1);
} }
fn cmd_key(extend: bool, api_key: &str) { fn cmd_key(extend: bool, public_key: &str, secret_key: &str) {
let result = api_request("/keys/validate", "POST", Some("{}"), api_key); let result = api_request("/keys/validate", "POST", Some("{}"), public_key, secret_key);
let status = extract_json_string(&result, "status"); let status = extract_json_string(&result, "status");
let public_key = extract_json_string(&result, "public_key"); let public_key = extract_json_string(&result, "public_key");
@ -647,7 +689,7 @@ fn main() {
} }
} }
"session" => { "session" => {
let key = get_api_key(api_key.as_deref()); let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_session( cmd_session(
args.contains(&"--list".to_string()), args.contains(&"--list".to_string()),
args.iter().position(|x| x == "--kill").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--kill").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
@ -656,12 +698,13 @@ fn main() {
vcpu, vcpu,
args.contains(&"--tmux".to_string()), args.contains(&"--tmux".to_string()),
args.contains(&"--screen".to_string()), args.contains(&"--screen".to_string()),
&key, &public_key,
&secret_key,
); );
return; return;
} }
"service" => { "service" => {
let key = get_api_key(api_key.as_deref()); let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_service( cmd_service(
args.iter().position(|x| x == "--name").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--name").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--ports").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--ports").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
@ -681,15 +724,17 @@ fn main() {
args.iter().position(|x| x == "--dump-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--dump-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
network.as_deref(), network.as_deref(),
vcpu, vcpu,
&key, &public_key,
&secret_key,
); );
return; return;
} }
"key" => { "key" => {
let key = get_api_key(api_key.as_deref()); let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_key( cmd_key(
args.contains(&"--extend".to_string()), args.contains(&"--extend".to_string()),
&key, &public_key,
&secret_key,
); );
return; return;
} }
@ -704,7 +749,7 @@ fn main() {
// Execute mode // Execute mode
if let Some(file) = source_file { if let Some(file) = source_file {
let key = get_api_key(api_key.as_deref()); let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_execute( cmd_execute(
&file, &file,
envs, envs,
@ -713,7 +758,8 @@ fn main() {
output_dir.as_deref(), output_dir.as_deref(),
network.as_deref(), network.as_deref(),
vcpu, vcpu,
&key, &public_key,
&secret_key,
); );
} else { } else {
eprintln!("{}Error: No source file specified{}", RED, RESET); eprintln!("{}Error: No source file specified{}", RED, RESET);

75
un.scm
View file

@ -101,8 +101,14 @@
(define (curl-post api-key endpoint json-data) (define (curl-post api-key endpoint json-data)
(let* ((tmp-file (write-temp-file json-data)) (let* ((tmp-file (write-temp-file json-data))
(cmd (format #f "curl -s -X POST https://api.unsandbox.com~a -H 'Content-Type: application/json' -H 'Authorization: Bearer ~a' -d @~a" (keys (get-api-keys))
endpoint api-key tmp-file)) (public-key (car keys))
(secret-key (cadr keys))
(auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(cmd (string-append "curl -s -X POST https://api.unsandbox.com" endpoint
" -H 'Content-Type: application/json' "
(string-join auth-headers " ")
" -d @" tmp-file))
(port (open-input-pipe cmd)) (port (open-input-pipe cmd))
(output (let loop ((lines '())) (output (let loop ((lines '()))
(let ((line (read-line port))) (let ((line (read-line port)))
@ -114,8 +120,12 @@
output)) output))
(define (curl-get api-key endpoint) (define (curl-get api-key endpoint)
(let* ((cmd (format #f "curl -s https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'" (let* ((keys (get-api-keys))
endpoint api-key)) (public-key (car keys))
(secret-key (cadr keys))
(auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
(cmd (string-append "curl -s https://api.unsandbox.com" endpoint
" " (string-join auth-headers " ")))
(port (open-input-pipe cmd)) (port (open-input-pipe cmd))
(output (let loop ((lines '())) (output (let loop ((lines '()))
(let ((line (read-line port))) (let ((line (read-line port)))
@ -126,8 +136,12 @@
output)) output))
(define (curl-delete api-key endpoint) (define (curl-delete api-key endpoint)
(let* ((cmd (format #f "curl -s -X DELETE https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'" (let* ((keys (get-api-keys))
endpoint api-key)) (public-key (car keys))
(secret-key (cadr keys))
(auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
(cmd (string-append "curl -s -X DELETE https://api.unsandbox.com" endpoint
" " (string-join auth-headers " ")))
(port (open-input-pipe cmd)) (port (open-input-pipe cmd))
(output (let loop ((lines '())) (output (let loop ((lines '()))
(let ((line (read-line port))) (let ((line (read-line port)))
@ -139,8 +153,14 @@
(define (curl-post-portal api-key endpoint json-data) (define (curl-post-portal api-key endpoint json-data)
(let* ((tmp-file (write-temp-file 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" (keys (get-api-keys))
portal-base endpoint api-key tmp-file)) (public-key (car keys))
(secret-key (cadr keys))
(auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(cmd (string-append "curl -s -X POST " portal-base endpoint
" -H 'Content-Type: application/json' "
(string-join auth-headers " ")
" -d @" tmp-file))
(port (open-input-pipe cmd)) (port (open-input-pipe cmd))
(output (let loop ((lines '())) (output (let loop ((lines '()))
(let ((line (read-line port))) (let ((line (read-line port)))
@ -151,11 +171,42 @@
(delete-file tmp-file) (delete-file tmp-file)
output)) output))
(define (get-api-keys)
(let ((public-key (getenv "UNSANDBOX_PUBLIC_KEY"))
(secret-key (getenv "UNSANDBOX_SECRET_KEY"))
(api-key (getenv "UNSANDBOX_API_KEY")))
(cond
((and public-key secret-key) (list public-key secret-key))
(api-key (list api-key #f))
(else (begin
(display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port))
(exit 1))))))
(define (get-api-key) (define (get-api-key)
(or (getenv "UNSANDBOX_API_KEY") (car (get-api-keys)))
(begin
(display "Error: UNSANDBOX_API_KEY not set\n" (current-error-port)) (define (hmac-sha256 secret message)
(exit 1)))) "Compute HMAC-SHA256 using openssl command"
(let* ((cmd (format #f "echo -n '~a' | openssl dgst -sha256 -hmac '~a' | awk '{print $2}'"
(string-append (list->string (map (lambda (c) (if (char=? c #\') #\space c)) (string->list message))))
(string-append (list->string (map (lambda (c) (if (char=? c #\') #\space c)) (string->list secret))))))
(port (open-input-pipe cmd))
(result (read-line port)))
(close-pipe port)
(string-trim-both result)))
(define (make-signature secret-key timestamp method path body)
(let ((message (format #f "~a:~a:~a:~a" timestamp method path body)))
(hmac-sha256 secret-key message)))
(define (build-auth-headers public-key secret-key method path body)
(if secret-key
(let* ((timestamp (number->string (quotient (current-time) 1)))
(signature (make-signature secret-key timestamp method path body)))
(list "-H" (format #f "Authorization: Bearer ~a" public-key)
"-H" (format #f "X-Timestamp: ~a" timestamp)
"-H" (format #f "X-Signature: ~a" signature)))
(list "-H" (format #f "Authorization: Bearer ~a" public-key))))
(define (json-extract-string json key) (define (json-extract-string json key)
"Extract string value for key from JSON (simple parser)" "Extract string value for key from JSON (simple parser)"

63
un.sh Normal file → Executable file
View file

@ -130,27 +130,60 @@ api_request() {
local endpoint="$1" local endpoint="$1"
local method="${2:-GET}" local method="${2:-GET}"
local data="${3:-}" local data="${3:-}"
local api_key="${4:-${UNSANDBOX_API_KEY}}" local public_key="${4:-${UNSANDBOX_PUBLIC_KEY:-}}"
local secret_key="${5:-${UNSANDBOX_SECRET_KEY:-}}"
if [[ -z "$api_key" ]]; then # Fallback to old UNSANDBOX_API_KEY for backwards compat
echo -e "${RED}Error: UNSANDBOX_API_KEY not set${RESET}" >&2 if [[ -z "$public_key" ]] && [[ -n "${UNSANDBOX_API_KEY:-}" ]]; then
public_key="${UNSANDBOX_API_KEY}"
secret_key=""
fi
if [[ -z "$public_key" ]]; then
echo -e "${RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${RESET}" >&2
exit 1 exit 1
fi fi
local url="${API_BASE}${endpoint}" local url="${API_BASE}${endpoint}"
local tmpfile=$(mktemp) local timestamp=$(date +%s)
local body="${data:-}"
# Build HMAC signature: timestamp:METHOD:path:body
local sig_input="${timestamp}:${method}:${endpoint}:${body}"
local signature=""
if [[ -n "$secret_key" ]]; then
signature=$(echo -n "$sig_input" | openssl dgst -sha256 -hmac "$secret_key" | sed 's/^.* //')
fi
if [[ -n "$data" ]]; then if [[ -n "$data" ]]; then
local response local response
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \ if [[ -n "$signature" ]]; then
-H "Authorization: Bearer $api_key" \ response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Content-Type: application/json" \ -H "Authorization: Bearer $public_key" \
-d "$data" 2>&1) -H "X-Timestamp: $timestamp" \
-H "X-Signature: $signature" \
-H "Content-Type: application/json" \
-d "$data" 2>&1)
else
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $public_key" \
-H "Content-Type: application/json" \
-d "$data" 2>&1)
fi
else else
local response local response
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \ if [[ -n "$signature" ]]; then
-H "Authorization: Bearer $api_key" \ response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Content-Type: application/json" 2>&1) -H "Authorization: Bearer $public_key" \
-H "X-Timestamp: $timestamp" \
-H "X-Signature: $signature" \
-H "Content-Type: application/json" 2>&1)
else
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $public_key" \
-H "Content-Type: application/json" 2>&1)
fi
fi fi
local http_code=$(echo "$response" | tail -n1) local http_code=$(echo "$response" | tail -n1)
@ -172,7 +205,7 @@ cmd_execute() {
local output_dir="." local output_dir="."
local network="" local network=""
local vcpu="" local vcpu=""
local api_key="${UNSANDBOX_API_KEY}" local api_key="${UNSANDBOX_API_KEY:-}"
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@ -300,7 +333,7 @@ cmd_session() {
local screen=false local screen=false
local network="" local network=""
local vcpu="" local vcpu=""
local api_key="${UNSANDBOX_API_KEY}" local api_key="${UNSANDBOX_API_KEY:-}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -409,7 +442,7 @@ cmd_service() {
local command="" local command=""
local network="" local network=""
local vcpu="" local vcpu=""
local api_key="${UNSANDBOX_API_KEY}" local api_key="${UNSANDBOX_API_KEY:-}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -743,7 +776,7 @@ validate_key() {
} }
cmd_key() { cmd_key() {
local api_key="${UNSANDBOX_API_KEY}" local api_key="${UNSANDBOX_API_KEY:-}"
local extend=false local extend=false
# Parse arguments # Parse arguments

92
un.tcl Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env tclsh
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY # PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# #
# This is free public domain software for the public good of a permacomputer hosted # This is free public domain software for the public good of a permacomputer hosted
@ -34,8 +35,6 @@
# https://www.foxhop.net # https://www.foxhop.net
# https://www.unturf.com/software # https://www.unturf.com/software
#!/usr/bin/env tclsh
# unsandbox CLI - TCL implementation # unsandbox CLI - TCL implementation
# Full-featured CLI matching un.c/un.py capabilities # Full-featured CLI matching un.c/un.py capabilities
@ -43,6 +42,7 @@ package require http
package require json package require json
package require tls package require tls
package require base64 package require base64
package require sha256
# Register https support # Register https support
::http::register https 443 ::tls::socket ::http::register https 443 ::tls::socket
@ -72,12 +72,28 @@ array set EXT_MAP {
.tcl tcl .raku raku .m objc .tcl tcl .raku raku .m objc
} }
proc get_api_key {} { proc get_api_keys {} {
if {[info exists ::env(UNSANDBOX_API_KEY)]} { set public_key ""
return $::env(UNSANDBOX_API_KEY) set secret_key ""
if {[info exists ::env(UNSANDBOX_PUBLIC_KEY)]} {
set public_key $::env(UNSANDBOX_PUBLIC_KEY)
} }
puts stderr "${::RED}Error: UNSANDBOX_API_KEY not set${::RESET}" if {[info exists ::env(UNSANDBOX_SECRET_KEY)]} {
exit 1 set secret_key $::env(UNSANDBOX_SECRET_KEY)
}
# Fallback to old UNSANDBOX_API_KEY for backwards compat
if {$public_key eq "" && [info exists ::env(UNSANDBOX_API_KEY)]} {
set public_key $::env(UNSANDBOX_API_KEY)
set secret_key ""
}
if {$public_key eq ""} {
puts stderr "${::RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${::RESET}"
exit 1
}
return [list $public_key $secret_key]
} }
proc detect_language {filename} { proc detect_language {filename} {
@ -103,16 +119,29 @@ proc detect_language {filename} {
exit 1 exit 1
} }
proc api_request {endpoint method data api_key} { proc api_request {endpoint method data public_key secret_key} {
set url "${::API_BASE}${endpoint}" set url "${::API_BASE}${endpoint}"
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"] set headers [list Authorization "Bearer $public_key" Content-Type "application/json"]
set json_data ""
if {$method ne "GET" && $method ne "DELETE" && [llength $data] > 0} {
set json_data [::json::write object {*}$data]
}
# Add HMAC signature if secret_key is present
if {$secret_key ne ""} {
set timestamp [clock seconds]
set sig_input "${timestamp}:${method}:${endpoint}:${json_data}"
set signature [::sha2::hmac -hex -key $secret_key $sig_input]
lappend headers X-Timestamp $timestamp
lappend headers X-Signature $signature
}
if {$method eq "GET"} { if {$method eq "GET"} {
set token [::http::geturl $url -headers $headers -timeout 300000] set token [::http::geturl $url -headers $headers -timeout 300000]
} elseif {$method eq "DELETE"} { } elseif {$method eq "DELETE"} {
set token [::http::geturl $url -method DELETE -headers $headers -timeout 300000] set token [::http::geturl $url -method DELETE -headers $headers -timeout 300000]
} else { } else {
set json_data [::json::write object {*}$data]
set token [::http::geturl $url -method $method -headers $headers -query $json_data -timeout 300000] set token [::http::geturl $url -method $method -headers $headers -query $json_data -timeout 300000]
} }
@ -131,7 +160,7 @@ proc api_request {endpoint method data api_key} {
} }
proc cmd_execute {args} { proc cmd_execute {args} {
set api_key [get_api_key] lassign [get_api_keys] public_key secret_key
set source_file "" set source_file ""
set env_vars [dict create] set env_vars [dict create]
set input_files [list] set input_files [list]
@ -236,7 +265,7 @@ proc cmd_execute {args} {
} }
# Execute # Execute
set result [api_request "/execute" "POST" $payload $api_key] set result [api_request "/execute" "POST" $payload $public_key $secret_key]
# Print output # Print output
if {[dict exists $result stdout]} { if {[dict exists $result stdout]} {
@ -275,7 +304,7 @@ proc cmd_execute {args} {
} }
proc cmd_session {args} { proc cmd_session {args} {
set api_key [get_api_key] lassign [get_api_keys] public_key secret_key
set list_mode 0 set list_mode 0
set kill_id "" set kill_id ""
set shell "" set shell ""
@ -309,7 +338,7 @@ proc cmd_session {args} {
} }
if {$list_mode} { if {$list_mode} {
set result [api_request "/sessions" "GET" {} $api_key] set result [api_request "/sessions" "GET" {} $public_key $secret_key]
set sessions [dict get $result sessions] set sessions [dict get $result sessions]
if {[llength $sessions] == 0} { if {[llength $sessions] == 0} {
puts "No active sessions" puts "No active sessions"
@ -327,7 +356,7 @@ proc cmd_session {args} {
} }
if {$kill_id ne ""} { if {$kill_id ne ""} {
api_request "/sessions/$kill_id" "DELETE" {} $api_key api_request "/sessions/$kill_id" "DELETE" {} $public_key $secret_key
puts "${::GREEN}Session terminated: $kill_id${::RESET}" puts "${::GREEN}Session terminated: $kill_id${::RESET}"
return return
} }
@ -347,13 +376,13 @@ proc cmd_session {args} {
} }
puts "${::YELLOW}Creating session...${::RESET}" puts "${::YELLOW}Creating session...${::RESET}"
set result [api_request "/sessions" "POST" $payload $api_key] set result [api_request "/sessions" "POST" $payload $public_key $secret_key]
puts "${::GREEN}Session created: [dict get $result id]${::RESET}" puts "${::GREEN}Session created: [dict get $result id]${::RESET}"
puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}" puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}"
} }
proc cmd_key {args} { proc cmd_key {args} {
set api_key [get_api_key] lassign [get_api_keys] public_key secret_key
set extend_mode 0 set extend_mode 0
# Parse arguments # Parse arguments
@ -368,7 +397,16 @@ proc cmd_key {args} {
# POST to /keys/validate with Bearer auth # POST to /keys/validate with Bearer auth
set url "${::PORTAL_BASE}/keys/validate" set url "${::PORTAL_BASE}/keys/validate"
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"] set headers [list Authorization "Bearer $public_key" Content-Type "application/json"]
# Add HMAC signature if secret_key is present
if {$secret_key ne ""} {
set timestamp [clock seconds]
set sig_input "${timestamp}:POST:/keys/validate:"
set signature [::sha2::hmac -hex -key $secret_key $sig_input]
lappend headers X-Timestamp $timestamp
lappend headers X-Signature $signature
}
set token [::http::geturl $url -method POST -headers $headers -timeout 30000] set token [::http::geturl $url -method POST -headers $headers -timeout 30000]
set status [::http::status $token] set status [::http::status $token]
@ -438,7 +476,7 @@ proc cmd_key {args} {
} }
proc cmd_service {args} { proc cmd_service {args} {
set api_key [get_api_key] lassign [get_api_keys] public_key secret_key
set list_mode 0 set list_mode 0
set info_id "" set info_id ""
set logs_id "" set logs_id ""
@ -517,7 +555,7 @@ proc cmd_service {args} {
} }
if {$list_mode} { if {$list_mode} {
set result [api_request "/services" "GET" {} $api_key] set result [api_request "/services" "GET" {} $public_key $secret_key]
set services [dict get $result services] set services [dict get $result services]
if {[llength $services] == 0} { if {[llength $services] == 0} {
puts "No services" puts "No services"
@ -538,31 +576,31 @@ proc cmd_service {args} {
} }
if {$info_id ne ""} { if {$info_id ne ""} {
set result [api_request "/services/$info_id" "GET" {} $api_key] set result [api_request "/services/$info_id" "GET" {} $public_key $secret_key]
puts [::json::write object {*}[dict_to_json_list $result]] puts [::json::write object {*}[dict_to_json_list $result]]
return return
} }
if {$logs_id ne ""} { if {$logs_id ne ""} {
set result [api_request "/services/$logs_id/logs" "GET" {} $api_key] set result [api_request "/services/$logs_id/logs" "GET" {} $public_key $secret_key]
puts [dict get $result logs] puts [dict get $result logs]
return return
} }
if {$sleep_id ne ""} { if {$sleep_id ne ""} {
api_request "/services/$sleep_id/sleep" "POST" {} $api_key api_request "/services/$sleep_id/sleep" "POST" {} $public_key $secret_key
puts "${::GREEN}Service sleeping: $sleep_id${::RESET}" puts "${::GREEN}Service sleeping: $sleep_id${::RESET}"
return return
} }
if {$wake_id ne ""} { if {$wake_id ne ""} {
api_request "/services/$wake_id/wake" "POST" {} $api_key api_request "/services/$wake_id/wake" "POST" {} $public_key $secret_key
puts "${::GREEN}Service waking: $wake_id${::RESET}" puts "${::GREEN}Service waking: $wake_id${::RESET}"
return return
} }
if {$destroy_id ne ""} { if {$destroy_id ne ""} {
api_request "/services/$destroy_id" "DELETE" {} $api_key api_request "/services/$destroy_id" "DELETE" {} $public_key $secret_key
puts "${::GREEN}Service destroyed: $destroy_id${::RESET}" puts "${::GREEN}Service destroyed: $destroy_id${::RESET}"
return return
} }
@ -570,7 +608,7 @@ proc cmd_service {args} {
if {$dump_bootstrap_id ne ""} { if {$dump_bootstrap_id ne ""} {
puts stderr "Fetching bootstrap script from $dump_bootstrap_id..." puts stderr "Fetching bootstrap script from $dump_bootstrap_id..."
set payload [list command [::json::write string "cat /tmp/bootstrap.sh"]] set payload [list command [::json::write string "cat /tmp/bootstrap.sh"]]
set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $api_key] set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $public_key $secret_key]
if {[dict exists $result stdout] && [dict get $result stdout] ne ""} { if {[dict exists $result stdout] && [dict get $result stdout] ne ""} {
set bootstrap [dict get $result stdout] set bootstrap [dict get $result stdout]
@ -628,7 +666,7 @@ proc cmd_service {args} {
lappend payload vcpu $vcpu lappend payload vcpu $vcpu
} }
set result [api_request "/services" "POST" $payload $api_key] set result [api_request "/services" "POST" $payload $public_key $secret_key]
puts "${::GREEN}Service created: [dict get $result id]${::RESET}" puts "${::GREEN}Service created: [dict get $result id]${::RESET}"
puts "Name: [dict get $result name]" puts "Name: [dict get $result name]"
if {[dict exists $result url]} { if {[dict exists $result url]} {

96
un.ts
View file

@ -54,6 +54,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as https from 'https'; import * as https from 'https';
import * as path from 'path'; import * as path from 'path';
import * as crypto from 'crypto';
const API_BASE = "https://api.unsandbox.com"; const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com"; const PORTAL_BASE = "https://unsandbox.com";
@ -112,13 +113,28 @@ interface Args {
extend: boolean; extend: boolean;
} }
function getApiKey(argsKey: string | null): string { interface ApiKeys {
const key = argsKey || process.env.UNSANDBOX_API_KEY; publicKey: string;
if (!key) { secretKey: string;
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`); }
process.exit(1);
function getApiKeys(argsKey: string | null): ApiKeys {
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
if (!publicKey || !secretKey) {
const oldKey = argsKey || process.env.UNSANDBOX_API_KEY;
if (oldKey) {
publicKey = oldKey;
secretKey = oldKey;
} else {
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
process.exit(1);
}
} }
return key;
return { publicKey, secretKey };
} }
function detectLanguage(filename: string): string { function detectLanguage(filename: string): string {
@ -143,15 +159,22 @@ function detectLanguage(filename: string): string {
return lang; return lang;
} }
function apiRequest(endpoint: string, method: string = "GET", data: any = null, apiKey: string): Promise<any> { function apiRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(API_BASE + endpoint); const url = new URL(API_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = { const options: https.RequestOptions = {
hostname: url.hostname, hostname: url.hostname,
path: url.pathname + url.search, path: url.pathname + url.search,
method: method, method: method,
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 300000 timeout: 300000
@ -180,21 +203,28 @@ function apiRequest(endpoint: string, method: string = "GET", data: any = null,
}); });
if (data) { if (data) {
req.write(JSON.stringify(data)); req.write(body);
} }
req.end(); req.end();
}); });
} }
function portalRequest(endpoint: string, method: string = "GET", data: any = null, apiKey: string): Promise<any> { function portalRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(PORTAL_BASE + endpoint); const url = new URL(PORTAL_BASE + endpoint);
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
const options: https.RequestOptions = { const options: https.RequestOptions = {
hostname: url.hostname, hostname: url.hostname,
path: url.pathname + url.search, path: url.pathname + url.search,
method: method, method: method,
headers: { headers: {
'Authorization': `Bearer ${apiKey}`, 'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 30000 timeout: 30000
@ -218,14 +248,14 @@ function portalRequest(endpoint: string, method: string = "GET", data: any = nul
}); });
if (data) { if (data) {
req.write(JSON.stringify(data)); req.write(body);
} }
req.end(); req.end();
}); });
} }
async function cmdExecute(args: Args): Promise<void> { async function cmdExecute(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey); const keys = getApiKeys(args.apiKey);
let code: string; let code: string;
try { try {
@ -267,7 +297,7 @@ async function cmdExecute(args: Args): Promise<void> {
if (args.network) payload.network = args.network; if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu; if (args.vcpu) payload.vcpu = args.vcpu;
const result = await apiRequest("/execute", "POST", payload, apiKey); const result = await apiRequest("/execute", "POST", payload, keys);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`); if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`); if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
@ -289,10 +319,10 @@ async function cmdExecute(args: Args): Promise<void> {
} }
async function cmdSession(args: Args): Promise<void> { async function cmdSession(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey); const keys = getApiKeys(args.apiKey);
if (args.list) { if (args.list) {
const result = await apiRequest("/sessions", "GET", null, apiKey); const result = await apiRequest("/sessions", "GET", null, keys);
const sessions = result.sessions || []; const sessions = result.sessions || [];
if (sessions.length === 0) { if (sessions.length === 0) {
console.log("No active sessions"); console.log("No active sessions");
@ -306,7 +336,7 @@ async function cmdSession(args: Args): Promise<void> {
} }
if (args.kill) { if (args.kill) {
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, apiKey); await apiRequest(`/sessions/${args.kill}`, "DELETE", null, keys);
console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`); console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`);
return; return;
} }
@ -325,16 +355,16 @@ async function cmdSession(args: Args): Promise<void> {
if (args.audit) payload.audit = true; if (args.audit) payload.audit = true;
console.log(`${YELLOW}Creating session...${RESET}`); console.log(`${YELLOW}Creating session...${RESET}`);
const result = await apiRequest("/sessions", "POST", payload, apiKey); const result = await apiRequest("/sessions", "POST", payload, keys);
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`); console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`); console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
} }
async function cmdService(args: Args): Promise<void> { async function cmdService(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey); const keys = getApiKeys(args.apiKey);
if (args.list) { if (args.list) {
const result = await apiRequest("/services", "GET", null, apiKey); const result = await apiRequest("/services", "GET", null, keys);
const services = result.services || []; const services = result.services || [];
if (services.length === 0) { if (services.length === 0) {
console.log("No services"); console.log("No services");
@ -350,44 +380,44 @@ async function cmdService(args: Args): Promise<void> {
} }
if (args.info) { if (args.info) {
const result = await apiRequest(`/services/${args.info}`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.info}`, "GET", null, keys);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
return; return;
} }
if (args.logs) { if (args.logs) {
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, keys);
console.log(result.logs || ""); console.log(result.logs || "");
return; return;
} }
if (args.tail) { if (args.tail) {
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, apiKey); const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, keys);
console.log(result.logs || ""); console.log(result.logs || "");
return; return;
} }
if (args.sleep) { if (args.sleep) {
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, apiKey); await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, keys);
console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`); console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`);
return; return;
} }
if (args.wake) { if (args.wake) {
await apiRequest(`/services/${args.wake}/wake`, "POST", null, apiKey); await apiRequest(`/services/${args.wake}/wake`, "POST", null, keys);
console.log(`${GREEN}Service waking: ${args.wake}${RESET}`); console.log(`${GREEN}Service waking: ${args.wake}${RESET}`);
return; return;
} }
if (args.destroy) { if (args.destroy) {
await apiRequest(`/services/${args.destroy}`, "DELETE", null, apiKey); await apiRequest(`/services/${args.destroy}`, "DELETE", null, keys);
console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`); console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`);
return; return;
} }
if (args.execute) { if (args.execute) {
const payload = { command: args.command_arg }; const payload = { command: args.command_arg };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, apiKey); const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`); if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`); if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
return; return;
@ -396,7 +426,7 @@ async function cmdService(args: Args): Promise<void> {
if (args.dumpBootstrap) { if (args.dumpBootstrap) {
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`); console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
const payload = { command: "cat /tmp/bootstrap.sh" }; const payload = { command: "cat /tmp/bootstrap.sh" };
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, apiKey); const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, keys);
if (result.stdout) { if (result.stdout) {
const bootstrap = result.stdout; const bootstrap = result.stdout;
@ -436,7 +466,7 @@ async function cmdService(args: Args): Promise<void> {
if (args.network) payload.network = args.network; if (args.network) payload.network = args.network;
if (args.vcpu) payload.vcpu = args.vcpu; if (args.vcpu) payload.vcpu = args.vcpu;
const result = await apiRequest("/services", "POST", payload, apiKey); const result = await apiRequest("/services", "POST", payload, keys);
console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`); console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`);
console.log(`Name: ${result.name || 'N/A'}`); console.log(`Name: ${result.name || 'N/A'}`);
if (result.url) console.log(`URL: ${result.url}`); if (result.url) console.log(`URL: ${result.url}`);
@ -468,9 +498,9 @@ function openBrowser(url: string): void {
}); });
} }
async function validateKey(apiKey: string, shouldExtend: boolean): Promise<void> { async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise<void> {
try { try {
const result = await portalRequest("/keys/validate", "POST", {}, apiKey); const result = await portalRequest("/keys/validate", "POST", {}, keys);
// Handle --extend flag first // Handle --extend flag first
if (shouldExtend) { if (shouldExtend) {
@ -513,8 +543,8 @@ async function validateKey(apiKey: string, shouldExtend: boolean): Promise<void>
} }
async function cmdKey(args: Args): Promise<void> { async function cmdKey(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey); const keys = getApiKeys(args.apiKey);
await validateKey(apiKey, args.extend); await validateKey(keys, args.extend);
} }
function parseArgs(argv: string[]): Args { function parseArgs(argv: string[]): Args {

69
un.v
View file

@ -126,7 +126,10 @@ fn extract_json_string(json string, key string) string {
} }
fn cmd_key(extend bool, api_key string) { fn cmd_key(extend bool, api_key string) {
cmd := "curl -s -X POST '${portal_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{}'" pub_key := get_public_key()
secret_key := get_secret_key()
body := '{}'
cmd := "BODY='${body}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/keys/validate:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${portal_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
result := exec_curl(cmd) result := exec_curl(cmd)
public_key := extract_json_string(result, 'public_key') public_key := extract_json_string(result, 'public_key')
@ -229,19 +232,24 @@ fn cmd_execute(source_file string, envs []string, artifacts bool, network string
} }
json += '}' json += '}'
cmd := "curl -s -X POST '${api_base}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'" pub_key := get_public_key()
secret_key := get_secret_key()
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
println(exec_curl(cmd)) println(exec_curl(cmd))
} }
fn cmd_session(list bool, kill string, shell string, network string, vcpu int, tmux bool, screen bool, api_key string) { fn cmd_session(list bool, kill string, shell string, network string, vcpu int, tmux bool, screen bool, api_key string) {
pub_key := get_public_key()
secret_key := get_secret_key()
if list { if list {
cmd := "curl -s -X GET '${api_base}/sessions' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/sessions:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/sessions' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
println(exec_curl(cmd)) println(exec_curl(cmd))
return return
} }
if kill != '' { if kill != '' {
cmd := "curl -s -X DELETE '${api_base}/sessions/${kill}' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:DELETE:/sessions/${kill}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X DELETE '${api_base}/sessions/${kill}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
exec_curl(cmd) exec_curl(cmd)
println('${green}Session terminated: ${kill}${reset}') println('${green}Session terminated: ${kill}${reset}')
return return
@ -264,51 +272,54 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
json += '}' json += '}'
println('${yellow}Creating session...${reset}') println('${yellow}Creating session...${reset}')
cmd := "curl -s -X POST '${api_base}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'" cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/sessions:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
println(exec_curl(cmd)) println(exec_curl(cmd))
} }
fn cmd_service(name string, ports string, service_type string, bootstrap string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, api_key string) { fn cmd_service(name string, ports string, service_type string, bootstrap string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, api_key string) {
pub_key := get_public_key()
secret_key := get_secret_key()
if list { if list {
cmd := "curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
println(exec_curl(cmd)) println(exec_curl(cmd))
return return
} }
if info != '' { if info != '' {
cmd := "curl -s -X GET '${api_base}/services/${info}' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${info}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${info}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
println(exec_curl(cmd)) println(exec_curl(cmd))
return return
} }
if logs != '' { if logs != '' {
cmd := "curl -s -X GET '${api_base}/services/${logs}/logs' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${logs}/logs:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${logs}/logs' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
print(exec_curl(cmd)) print(exec_curl(cmd))
return return
} }
if tail != '' { if tail != '' {
cmd := "curl -s -X GET '${api_base}/services/${tail}/logs?lines=9000' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${tail}/logs:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${tail}/logs?lines=9000' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
print(exec_curl(cmd)) print(exec_curl(cmd))
return return
} }
if sleep != '' { if sleep != '' {
cmd := "curl -s -X POST '${api_base}/services/${sleep}/sleep' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${sleep}/sleep:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${sleep}/sleep' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
exec_curl(cmd) exec_curl(cmd)
println('${green}Service sleeping: ${sleep}${reset}') println('${green}Service sleeping: ${sleep}${reset}')
return return
} }
if wake != '' { if wake != '' {
cmd := "curl -s -X POST '${api_base}/services/${wake}/wake' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${wake}/wake:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${wake}/wake' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
exec_curl(cmd) exec_curl(cmd)
println('${green}Service waking: ${wake}${reset}') println('${green}Service waking: ${wake}${reset}')
return return
} }
if destroy != '' { if destroy != '' {
cmd := "curl -s -X DELETE '${api_base}/services/${destroy}' -H 'Authorization: Bearer ${api_key}'" cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:DELETE:/services/${destroy}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X DELETE '${api_base}/services/${destroy}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
exec_curl(cmd) exec_curl(cmd)
println('${green}Service destroyed: ${destroy}${reset}') println('${green}Service destroyed: ${destroy}${reset}')
return return
@ -316,7 +327,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
if execute != '' { if execute != '' {
json := '{"command":"${escape_json(command)}"}' json := '{"command":"${escape_json(command)}"}'
cmd := "curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'" cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${execute}/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
result := exec_curl(cmd) result := exec_curl(cmd)
stdout_str := extract_json_string(result, 'stdout') stdout_str := extract_json_string(result, 'stdout')
@ -332,7 +343,8 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
if dump_bootstrap != '' { if dump_bootstrap != '' {
eprintln('Fetching bootstrap script from ${dump_bootstrap}...') eprintln('Fetching bootstrap script from ${dump_bootstrap}...')
cmd := "curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'" json := '{"command":"cat /tmp/bootstrap.sh"}'
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${dump_bootstrap}/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
result := exec_curl(cmd) result := exec_curl(cmd)
bootstrap_script := extract_json_string(result, 'stdout') bootstrap_script := extract_json_string(result, 'stdout')
@ -379,7 +391,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
json += '}' json += '}'
println('${yellow}Creating service...${reset}') println('${yellow}Creating service...${reset}')
cmd := "curl -s -X POST '${api_base}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'" cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
println(exec_curl(cmd)) println(exec_curl(cmd))
return return
} }
@ -388,8 +400,33 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
exit(1) exit(1)
} }
fn get_public_key() string {
pub_key := os.getenv('UNSANDBOX_PUBLIC_KEY')
if pub_key != '' {
return pub_key
}
api_key := os.getenv('UNSANDBOX_API_KEY')
if api_key != '' {
return api_key
}
eprintln('${red}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set${reset}')
exit(1)
}
fn get_secret_key() string {
sec_key := os.getenv('UNSANDBOX_SECRET_KEY')
if sec_key != '' {
return sec_key
}
api_key := os.getenv('UNSANDBOX_API_KEY')
if api_key != '' {
return api_key
}
return ''
}
fn main() { fn main() {
mut api_key := os.getenv('UNSANDBOX_API_KEY') mut api_key := get_public_key()
if os.args.len < 2 { if os.args.len < 2 {
eprintln('Usage: ${os.args[0]} [options] <source_file>') eprintln('Usage: ${os.args[0]} [options] <source_file>')

144
un.zig
View file

@ -50,10 +50,57 @@ const std = @import("std");
const fs = std.fs; const fs = std.fs;
const process = std.process; const process = std.process;
const mem = std.mem; const mem = std.mem;
const time = std.time;
const API_BASE = "https://api.unsandbox.com"; const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com"; const PORTAL_BASE = "https://unsandbox.com";
fn computeHmacCmd(allocator: std.mem.Allocator, secret_key: []const u8, message: []const u8) ![]const u8 {
return try std.fmt.allocPrint(allocator, "echo -n '{s}' | openssl dgst -sha256 -hmac '{s}' -hex 2>/dev/null | sed 's/.*= //'", .{ message, secret_key });
}
fn getTimestamp(allocator: std.mem.Allocator) ![]const u8 {
const timestamp = std.time.timestamp();
return try std.fmt.allocPrint(allocator, "{d}", .{timestamp});
}
fn buildAuthCmd(allocator: std.mem.Allocator, method: []const u8, path: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
if (secret_key.len == 0) {
// Legacy mode: use public_key as bearer token
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}'", .{public_key});
}
// HMAC mode
const timestamp_str = try getTimestamp(allocator);
defer allocator.free(timestamp_str);
const message = try std.fmt.allocPrint(allocator, "{s}:{s}:{s}:{s}", .{ timestamp_str, method, path, body });
defer allocator.free(message);
const hmac_cmd = try computeHmacCmd(allocator, secret_key, message);
defer allocator.free(hmac_cmd);
// Execute HMAC command to get signature
var signature_buf: [256]u8 = undefined;
var fbs = std.io.fixedBufferStream(&signature_buf);
const signature_len = blk: {
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "sh", "-c", hmac_cmd },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
@memcpy(signature_buf[0..trimmed.len], trimmed);
break :blk trimmed.len;
};
const signature = signature_buf[0..signature_len];
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}' -H 'X-Timestamp: {s}' -H 'X-Signature: {s}'", .{ public_key, timestamp_str, signature });
}
pub fn main() !u8 { pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); defer _ = gpa.deinit();
@ -70,10 +117,16 @@ pub fn main() !u8 {
return 1; return 1;
} }
const api_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch blk: { var public_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
break :blk std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
defer allocator.free(public_key);
const secret_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch blk: {
break :blk try allocator.dupe(u8, ""); break :blk try allocator.dupe(u8, "");
}; };
defer allocator.free(api_key); defer allocator.free(secret_key);
// Handle session command // Handle session command
if (mem.eql(u8, args[1], "session")) { if (mem.eql(u8, args[1], "session")) {
@ -90,22 +143,36 @@ pub fn main() !u8 {
} else if (mem.eql(u8, args[i], "--shell") and i + 1 < args.len) { } else if (mem.eql(u8, args[i], "--shell") and i + 1 < args.len) {
i += 1; i += 1;
shell = args[i]; shell = args[i];
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
} }
} }
if (list) { if (list) {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/sessions' -H 'Authorization: Bearer {s}'", .{ API_BASE, api_key }); const auth_headers = try buildAuthCmd(allocator, "GET", "/sessions", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/sessions' {s}", .{ API_BASE, auth_headers });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
} else if (kill) |k| { } else if (kill) |k| {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/sessions/{s}' -H 'Authorization: Bearer {s}'", .{ API_BASE, k, api_key }); const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{k});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "DELETE", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/sessions/{s}' {s}", .{ API_BASE, k, auth_headers });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k}); std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k});
} else { } else {
const sh = shell orelse "bash"; const sh = shell orelse "bash";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"shell\":\"{s}\"}}'", .{ API_BASE, api_key, sh }); const json = try std.fmt.allocPrint(allocator, "{{\"shell\":\"{s}\"}}", .{sh});
defer allocator.free(json);
const auth_headers = try buildAuthCmd(allocator, "POST", "/sessions", json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/sessions' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json });
defer allocator.free(cmd); defer allocator.free(cmd);
std.debug.print("\x1b[33mCreating session...\x1b[0m\n", .{}); std.debug.print("\x1b[33mCreating session...\x1b[0m\n", .{});
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
@ -153,29 +220,50 @@ pub fn main() !u8 {
} else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) { } else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) {
i += 1; i += 1;
dump_file = args[i]; dump_file = args[i];
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
} }
} }
if (list) { if (list) {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services' -H 'Authorization: Bearer {s}'", .{ API_BASE, api_key }); const auth_headers = try buildAuthCmd(allocator, "GET", "/services", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services' {s}", .{ API_BASE, auth_headers });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
} else if (info) |inf| { } else if (info) |inf| {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services/{s}' -H 'Authorization: Bearer {s}'", .{ API_BASE, inf, api_key }); const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{inf});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services/{s}' {s}", .{ API_BASE, inf, auth_headers });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
} else if (execute) |exec_id| { } else if (execute) |exec_id| {
const cmd_text = command orelse ""; const cmd_text = command orelse "";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"{s}\"}}'", .{ API_BASE, exec_id, api_key, cmd_text }); const json = try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{cmd_text});
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{exec_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, exec_id, auth_headers, json });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
std.debug.print("\n", .{}); std.debug.print("\n", .{});
} else if (dump_bootstrap) |bootstrap_id| { } else if (dump_bootstrap) |bootstrap_id| {
std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id}); std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id});
const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt"; const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"cat /tmp/bootstrap.sh\"}}' -o {s}", .{ API_BASE, bootstrap_id, api_key, tmp_file }); const json = "{{\"command\":\"cat /tmp/bootstrap.sh\"}}";
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{bootstrap_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}' -o {s}", .{ API_BASE, bootstrap_id, auth_headers, json, tmp_file });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
@ -231,7 +319,9 @@ pub fn main() !u8 {
try writer.writeAll("}"); try writer.writeAll("}");
const json_str = json_stream.getWritten(); const json_str = json_stream.getWritten();
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{s}'", .{ API_BASE, api_key, json_str }); const auth_headers = try buildAuthCmd(allocator, "POST", "/services", json_str, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json_str });
defer allocator.free(cmd); defer allocator.free(cmd);
std.debug.print("\x1b[33mCreating service...\x1b[0m\n", .{}); std.debug.print("\x1b[33mCreating service...\x1b[0m\n", .{});
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
@ -247,13 +337,19 @@ pub fn main() !u8 {
while (i < args.len) : (i += 1) { while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--extend")) { if (mem.eql(u8, args[i], "--extend")) {
extend = true; extend = true;
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
} }
} }
if (extend) { if (extend) {
// First validate to get the public_key // First validate to get the public_key
const json_file = "/tmp/unsandbox_key_validate.json"; 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 }); const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
defer allocator.free(cmd_validate); defer allocator.free(cmd_validate);
_ = std.c.system(cmd_validate.ptr); _ = std.c.system(cmd_validate.ptr);
@ -268,15 +364,15 @@ pub fn main() !u8 {
// Simple JSON parsing to find public_key (looking for "public_key":"value") // Simple JSON parsing to find public_key (looking for "public_key":"value")
const pk_prefix = "\"public_key\":\""; const pk_prefix = "\"public_key\":\"";
var public_key: ?[]const u8 = null; var public_key_value: ?[]const u8 = null;
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| { if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
const value_start = start_idx + pk_prefix.len; const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| { if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
public_key = json_content[value_start..end_idx]; public_key_value = json_content[value_start..end_idx];
} }
} }
if (public_key) |pk| { if (public_key_value) |pk| {
const url = try std.fmt.allocPrint(allocator, "{s}/keys/extend?pk={s}", .{ PORTAL_BASE, pk }); const url = try std.fmt.allocPrint(allocator, "{s}/keys/extend?pk={s}", .{ PORTAL_BASE, pk });
defer allocator.free(url); defer allocator.free(url);
std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{}); std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{});
@ -290,7 +386,9 @@ pub fn main() !u8 {
} else { } else {
// Regular validation // Regular validation
const json_file = "/tmp/unsandbox_key_validate.json"; 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 }); const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
defer allocator.free(cmd); defer allocator.free(cmd);
_ = std.c.system(cmd.ptr); _ = std.c.system(cmd.ptr);
@ -319,7 +417,7 @@ pub fn main() !u8 {
} }
// Extract other fields // Extract other fields
var public_key: ?[]const u8 = null; var pub_key: ?[]const u8 = null;
var tier: ?[]const u8 = null; var tier: ?[]const u8 = null;
var expires_at: ?[]const u8 = null; var expires_at: ?[]const u8 = null;
@ -327,7 +425,7 @@ pub fn main() !u8 {
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| { if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
const value_start = start_idx + pk_prefix.len; const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| { if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
public_key = json_content[value_start..end_idx]; pub_key = json_content[value_start..end_idx];
} }
} }
@ -351,12 +449,12 @@ pub fn main() !u8 {
if (status) |s| { if (status) |s| {
if (mem.eql(u8, s, "valid")) { if (mem.eql(u8, s, "valid")) {
std.debug.print("\x1b[32mValid\x1b[0m\n", .{}); std.debug.print("\x1b[32mValid\x1b[0m\n", .{});
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk}); if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t}); if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp}); if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp});
} else if (mem.eql(u8, s, "expired")) { } else if (mem.eql(u8, s, "expired")) {
std.debug.print("\x1b[31mExpired\x1b[0m\n", .{}); std.debug.print("\x1b[31mExpired\x1b[0m\n", .{});
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk}); if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t}); if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expired: {s}\n", .{exp}); 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}); std.debug.print("\x1b[33mTo renew: Visit {s}/keys/extend\x1b[0m\n", .{PORTAL_BASE});
@ -430,8 +528,14 @@ pub fn main() !u8 {
} }
try writer.writeAll("\"}"); try writer.writeAll("\"}");
// Read back the JSON to compute HMAC
const json_content = try fs.cwd().readFileAlloc(allocator, json_file, 10 * 1024 * 1024);
defer allocator.free(json_content);
// Execute with curl // Execute with curl
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d @{s}", .{ API_BASE, api_key, json_file }); const auth_headers = try buildAuthCmd(allocator, "POST", "/execute", json_content, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s}", .{ API_BASE, auth_headers, json_file });
defer allocator.free(cmd); defer allocator.free(cmd);
const result = std.c.system(cmd.ptr); const result = std.c.system(cmd.ptr);

View file

@ -63,13 +63,28 @@ const EXT_MAP: Record<string, string> = {
m: "objc", m: "objc",
}; };
function getApiKey(): string { interface ApiKeys {
const key = Deno.env.get("UNSANDBOX_API_KEY"); publicKey: string;
if (!key) { secretKey: string;
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`); }
Deno.exit(1);
function getApiKeys(): ApiKeys {
let publicKey = Deno.env.get("UNSANDBOX_PUBLIC_KEY");
let secretKey = Deno.env.get("UNSANDBOX_SECRET_KEY");
if (!publicKey || !secretKey) {
const oldKey = Deno.env.get("UNSANDBOX_API_KEY");
if (oldKey) {
publicKey = oldKey;
secretKey = oldKey;
} else {
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
Deno.exit(1);
}
} }
return key;
return { publicKey, secretKey };
} }
function detectLanguage(filename: string): string { function detectLanguage(filename: string): string {
@ -92,13 +107,40 @@ async function apiRequest(
endpoint: string, endpoint: string,
method: string, method: string,
data?: unknown, data?: unknown,
apiKey?: string, keys?: ApiKeys,
baseUrl?: string, baseUrl?: string,
): Promise<any> { ): Promise<any> {
const base = baseUrl || API_BASE; const base = baseUrl || API_BASE;
const url = `${base}${endpoint}`; const url = `${base}${endpoint}`;
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
// Parse URL to get pathname and search
const urlObj = new URL(url);
const message = `${timestamp}:${method}:${urlObj.pathname}${urlObj.search}:${body}`;
// Create HMAC signature using Web Crypto API
const encoder = new TextEncoder();
const keyData = encoder.encode(keys!.secretKey);
const messageData = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
const signature = Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const headers: Record<string, string> = { const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${keys!.publicKey}`,
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
@ -108,7 +150,7 @@ async function apiRequest(
}; };
if (data && method !== "GET") { if (data && method !== "GET") {
options.body = JSON.stringify(data); options.body = body;
} }
const response = await fetch(url, options); const response = await fetch(url, options);
@ -124,7 +166,7 @@ async function apiRequest(
} }
async function cmdExecute(args: string[]) { async function cmdExecute(args: string[]) {
const apiKey = getApiKey(); const keys = getApiKeys();
let sourceFile = ""; let sourceFile = "";
const envVars: Record<string, string> = {}; const envVars: Record<string, string> = {};
const inputFiles: string[] = []; const inputFiles: string[] = [];
@ -228,7 +270,7 @@ async function cmdExecute(args: string[]) {
} }
// Execute // Execute
const result = await apiRequest("/execute", "POST", payload, apiKey); const result = await apiRequest("/execute", "POST", payload, keys);
// Print output // Print output
if (result.stdout) { if (result.stdout) {
@ -255,7 +297,7 @@ async function cmdExecute(args: string[]) {
} }
async function cmdSession(args: string[]) { async function cmdSession(args: string[]) {
const apiKey = getApiKey(); const keys = getApiKeys();
let listMode = false; let listMode = false;
let killId = ""; let killId = "";
let shell = ""; let shell = "";
@ -293,7 +335,7 @@ async function cmdSession(args: string[]) {
} }
if (listMode) { if (listMode) {
const result = await apiRequest("/sessions", "GET", undefined, apiKey); const result = await apiRequest("/sessions", "GET", undefined, keys);
const sessions = result.sessions || []; const sessions = result.sessions || [];
if (sessions.length === 0) { if (sessions.length === 0) {
console.log("No active sessions"); console.log("No active sessions");
@ -311,7 +353,7 @@ async function cmdSession(args: string[]) {
} }
if (killId) { if (killId) {
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, apiKey); await apiRequest(`/sessions/${killId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Session terminated: ${killId}${RESET}`); console.log(`${GREEN}Session terminated: ${killId}${RESET}`);
return; return;
} }
@ -324,7 +366,7 @@ async function cmdSession(args: string[]) {
if (vcpu > 0) payload.vcpu = vcpu; if (vcpu > 0) payload.vcpu = vcpu;
console.log(`${YELLOW}Creating session...${RESET}`); console.log(`${YELLOW}Creating session...${RESET}`);
const result = await apiRequest("/sessions", "POST", payload, apiKey); const result = await apiRequest("/sessions", "POST", payload, keys);
console.log(`${GREEN}Session created: ${result.id}${RESET}`); console.log(`${GREEN}Session created: ${result.id}${RESET}`);
console.log( console.log(
`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`, `${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`,
@ -332,7 +374,7 @@ async function cmdSession(args: string[]) {
} }
async function cmdKey(args: string[]) { async function cmdKey(args: string[]) {
const apiKey = getApiKey(); const keys = getApiKeys();
let extend = false; let extend = false;
// Parse arguments // Parse arguments
@ -343,7 +385,7 @@ async function cmdKey(args: string[]) {
} }
try { try {
const result = await apiRequest("/keys/validate", "POST", undefined, apiKey, PORTAL_BASE); const result = await apiRequest("/keys/validate", "POST", undefined, keys, PORTAL_BASE);
// Handle --extend flag // Handle --extend flag
if (extend) { if (extend) {
@ -395,7 +437,7 @@ async function cmdKey(args: string[]) {
} }
async function cmdService(args: string[]) { async function cmdService(args: string[]) {
const apiKey = getApiKey(); const keys = getApiKeys();
let listMode = false; let listMode = false;
let infoId = ""; let infoId = "";
let logsId = ""; let logsId = "";
@ -475,7 +517,7 @@ async function cmdService(args: string[]) {
} }
if (listMode) { if (listMode) {
const result = await apiRequest("/services", "GET", undefined, apiKey); const result = await apiRequest("/services", "GET", undefined, keys);
const services = result.services || []; const services = result.services || [];
if (services.length === 0) { if (services.length === 0) {
console.log("No services"); console.log("No services");
@ -495,31 +537,31 @@ async function cmdService(args: string[]) {
} }
if (infoId) { if (infoId) {
const result = await apiRequest(`/services/${infoId}`, "GET", undefined, apiKey); const result = await apiRequest(`/services/${infoId}`, "GET", undefined, keys);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
return; return;
} }
if (logsId) { if (logsId) {
const result = await apiRequest(`/services/${logsId}/logs`, "GET", undefined, apiKey); const result = await apiRequest(`/services/${logsId}/logs`, "GET", undefined, keys);
console.log(result.logs || ""); console.log(result.logs || "");
return; return;
} }
if (sleepId) { if (sleepId) {
await apiRequest(`/services/${sleepId}/sleep`, "POST", undefined, apiKey); await apiRequest(`/services/${sleepId}/sleep`, "POST", undefined, keys);
console.log(`${GREEN}Service sleeping: ${sleepId}${RESET}`); console.log(`${GREEN}Service sleeping: ${sleepId}${RESET}`);
return; return;
} }
if (wakeId) { if (wakeId) {
await apiRequest(`/services/${wakeId}/wake`, "POST", undefined, apiKey); await apiRequest(`/services/${wakeId}/wake`, "POST", undefined, keys);
console.log(`${GREEN}Service waking: ${wakeId}${RESET}`); console.log(`${GREEN}Service waking: ${wakeId}${RESET}`);
return; return;
} }
if (destroyId) { if (destroyId) {
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, apiKey); await apiRequest(`/services/${destroyId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Service destroyed: ${destroyId}${RESET}`); console.log(`${GREEN}Service destroyed: ${destroyId}${RESET}`);
return; return;
} }
@ -553,7 +595,7 @@ async function cmdService(args: string[]) {
if (network) payload.network = network; if (network) payload.network = network;
if (vcpu > 0) payload.vcpu = vcpu; if (vcpu > 0) payload.vcpu = vcpu;
const result = await apiRequest("/services", "POST", payload, apiKey); const result = await apiRequest("/services", "POST", payload, keys);
console.log(`${GREEN}Service created: ${result.id}${RESET}`); console.log(`${GREEN}Service created: ${result.id}${RESET}`);
console.log(`Name: ${result.name}`); console.log(`Name: ${result.name}`);
if (result.url) { if (result.url) {

View file

@ -48,6 +48,7 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <time.h>
#define API_BASE "https://api.unsandbox.com" #define API_BASE "https://api.unsandbox.com"
#define PORTAL_BASE "https://unsandbox.com" #define PORTAL_BASE "https://unsandbox.com"
@ -56,6 +57,7 @@
#define GREEN "\033[32m" #define GREEN "\033[32m"
#define YELLOW "\033[33m" #define YELLOW "\033[33m"
#define RESET "\033[0m" #define RESET "\033[0m"
#define MAX_CMD_LEN 16384
const char* detect_language(const char *filename) { const char* detect_language(const char *filename) {
const char *ext = strrchr(filename, '.'); const char *ext = strrchr(filename, '.');
@ -113,7 +115,54 @@ void escape_json_char(FILE *out, char c) {
} }
} }
void cmd_execute(const char *source_file, char **envs, int env_count, char **files, int file_count, int artifacts, const char *output_dir, const char *network, int vcpu, const char *api_key) { char* compute_hmac(const char *secret_key, const char *message) {
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd), "echo -n '%s' | openssl dgst -sha256 -hmac '%s' -hex | sed 's/.*= //'", message, secret_key);
FILE *pipe = popen(cmd, "r");
if (!pipe) return NULL;
static char result[128];
if (fgets(result, sizeof(result), pipe)) {
// Trim newline
size_t len = strlen(result);
while (len > 0 && (result[len-1] == '\n' || result[len-1] == '\r')) {
result[--len] = '\0';
}
}
pclose(pipe);
return result;
}
void get_timestamp(char *buffer, size_t bufsize) {
snprintf(buffer, bufsize, "%ld", (long)time(NULL));
}
void build_auth_headers(char *buffer, size_t bufsize, const char *method, const char *path, const char *body, const char *public_key, const char *secret_key) {
if (!secret_key || secret_key[0] == '\0') {
// Legacy mode: use public_key as bearer token
snprintf(buffer, bufsize, "-H 'Authorization: Bearer %s'", public_key);
return;
}
// HMAC mode
char timestamp[32];
get_timestamp(timestamp, sizeof(timestamp));
char message[MAX_CMD_LEN];
snprintf(message, sizeof(message), "%s:%s:%s:%s", timestamp, method, path, body ? body : "");
char *signature = compute_hmac(secret_key, message);
if (!signature) {
fprintf(stderr, "%sError computing HMAC signature%s\n", RED, RESET);
exit(1);
}
snprintf(buffer, bufsize, "-H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'",
public_key, timestamp, signature);
}
void cmd_execute(const char *source_file, char **envs, int env_count, char **files, int file_count, int artifacts, const char *output_dir, const char *network, int vcpu, const char *public_key, const char *secret_key) {
const char *language = detect_language(source_file); const char *language = detect_language(source_file);
if (!language) { if (!language) {
fprintf(stderr, "%sError: Cannot detect language%s\n", RED, RESET); fprintf(stderr, "%sError: Cannot detect language%s\n", RED, RESET);
@ -179,13 +228,16 @@ void cmd_execute(const char *source_file, char **envs, int env_count, char **fil
fclose(jsonf); fclose(jsonf);
// Make API request using curl // Make API request using curl
char cmd[8192]; char auth_headers[1024];
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/execute", json_body, public_key, secret_key);
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/execute' " "curl -s -X POST '%s/execute' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' " "%s "
"-d @- << 'EOF'\n%s\nEOF", "-d @- << 'EOF'\n%s\nEOF",
API_BASE, api_key, json_body); API_BASE, auth_headers, json_body);
FILE *curl = popen(cmd, "r"); FILE *curl = popen(cmd, "r");
if (!curl) { if (!curl) {
@ -262,22 +314,27 @@ void cmd_execute(const char *source_file, char **envs, int env_count, char **fil
exit(exit_code); exit(exit_code);
} }
void cmd_session(int list, const char *kill, const char *shell, const char *network, int vcpu, int tmux, int screen, const char *api_key) { void cmd_session(int list, const char *kill, const char *shell, const char *network, int vcpu, int tmux, int screen, const char *public_key, const char *secret_key) {
char cmd[4096]; char cmd[8192];
char auth_headers[1024];
if (list) { if (list) {
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/sessions", "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/sessions' -H 'Authorization: Bearer %s'", "curl -s -X GET '%s/sessions' %s",
API_BASE, api_key); API_BASE, auth_headers);
system(cmd); system(cmd);
printf("\n"); printf("\n");
return; return;
} }
if (kill) { if (kill) {
char path[512];
snprintf(path, sizeof(path), "/sessions/%s", kill);
build_auth_headers(auth_headers, sizeof(auth_headers), "DELETE", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X DELETE '%s/sessions/%s' -H 'Authorization: Bearer %s'", "curl -s -X DELETE '%s/sessions/%s' %s",
API_BASE, kill, api_key); API_BASE, kill, auth_headers);
system(cmd); system(cmd);
printf("%sSession terminated: %s%s\n", GREEN, kill, RESET); printf("%sSession terminated: %s%s\n", GREEN, kill, RESET);
return; return;
@ -305,22 +362,25 @@ void cmd_session(int list, const char *kill, const char *shell, const char *netw
strcat(json, "}"); strcat(json, "}");
printf("%sCreating session...%s\n", YELLOW, RESET); printf("%sCreating session...%s\n", YELLOW, RESET);
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/sessions", json, public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' " "curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' -d '%s'", "%s -d '%s'",
API_BASE, api_key, json); API_BASE, auth_headers, json);
system(cmd); system(cmd);
printf("\n%sSession created%s\n", GREEN, RESET); printf("\n%sSession created%s\n", GREEN, RESET);
} }
void cmd_key(int extend, const char *api_key) { void cmd_key(int extend, const char *public_key, const char *secret_key) {
char cmd[4096]; char cmd[8192];
char auth_headers[1024];
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/keys/validate", "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/keys/validate' " "curl -s -X POST '%s/keys/validate' "
"-H 'Content-Type: application/json' " "-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s'", "%s",
PORTAL_BASE, api_key); PORTAL_BASE, auth_headers);
FILE *curl = popen(cmd, "r"); FILE *curl = popen(cmd, "r");
if (!curl) { if (!curl) {
@ -449,65 +509,85 @@ void cmd_key(int extend, const char *api_key) {
} }
} }
void cmd_service(const char *name, const char *ports, const char *domains, const char *service_type, const char *bootstrap, int list, const char *info, const char *logs, const char *tail, const char *sleep_svc, const char *wake, const char *destroy, const char *network, int vcpu, const char *api_key) { void cmd_service(const char *name, const char *ports, const char *domains, const char *service_type, const char *bootstrap, int list, const char *info, const char *logs, const char *tail, const char *sleep_svc, const char *wake, const char *destroy, const char *network, int vcpu, const char *public_key, const char *secret_key) {
char cmd[8192]; char cmd[16384];
char auth_headers[1024];
if (list) { if (list) {
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/services", "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'", "curl -s -X GET '%s/services' %s",
API_BASE, api_key); API_BASE, auth_headers);
system(cmd); system(cmd);
printf("\n"); printf("\n");
return; return;
} }
if (info) { if (info) {
char path[512];
snprintf(path, sizeof(path), "/services/%s", info);
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/services/%s' -H 'Authorization: Bearer %s'", "curl -s -X GET '%s/services/%s' %s",
API_BASE, info, api_key); API_BASE, info, auth_headers);
system(cmd); system(cmd);
printf("\n"); printf("\n");
return; return;
} }
if (logs) { if (logs) {
char path[512];
snprintf(path, sizeof(path), "/services/%s/logs", logs);
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/services/%s/logs' -H 'Authorization: Bearer %s'", "curl -s -X GET '%s/services/%s/logs' %s",
API_BASE, logs, api_key); API_BASE, logs, auth_headers);
system(cmd); system(cmd);
return; return;
} }
if (tail) { if (tail) {
char path[512];
snprintf(path, sizeof(path), "/services/%s/logs", tail);
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/services/%s/logs?lines=9000' -H 'Authorization: Bearer %s'", "curl -s -X GET '%s/services/%s/logs?lines=9000' %s",
API_BASE, tail, api_key); API_BASE, tail, auth_headers);
system(cmd); system(cmd);
return; return;
} }
if (sleep_svc) { if (sleep_svc) {
char path[512];
snprintf(path, sizeof(path), "/services/%s/sleep", sleep_svc);
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/services/%s/sleep' -H 'Authorization: Bearer %s'", "curl -s -X POST '%s/services/%s/sleep' %s",
API_BASE, sleep_svc, api_key); API_BASE, sleep_svc, auth_headers);
system(cmd); system(cmd);
printf("%sService sleeping: %s%s\n", GREEN, sleep_svc, RESET); printf("%sService sleeping: %s%s\n", GREEN, sleep_svc, RESET);
return; return;
} }
if (wake) { if (wake) {
char path[512];
snprintf(path, sizeof(path), "/services/%s/wake", wake);
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/services/%s/wake' -H 'Authorization: Bearer %s'", "curl -s -X POST '%s/services/%s/wake' %s",
API_BASE, wake, api_key); API_BASE, wake, auth_headers);
system(cmd); system(cmd);
printf("%sService waking: %s%s\n", GREEN, wake, RESET); printf("%sService waking: %s%s\n", GREEN, wake, RESET);
return; return;
} }
if (destroy) { if (destroy) {
char path[512];
snprintf(path, sizeof(path), "/services/%s", destroy);
build_auth_headers(auth_headers, sizeof(auth_headers), "DELETE", path, "", public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X DELETE '%s/services/%s' -H 'Authorization: Bearer %s'", "curl -s -X DELETE '%s/services/%s' %s",
API_BASE, destroy, api_key); API_BASE, destroy, auth_headers);
system(cmd); system(cmd);
printf("%sService destroyed: %s%s\n", GREEN, destroy, RESET); printf("%sService destroyed: %s%s\n", GREEN, destroy, RESET);
return; return;
@ -551,10 +631,11 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
strcat(json, "}"); strcat(json, "}");
printf("%sCreating service...%s\n", YELLOW, RESET); printf("%sCreating service...%s\n", YELLOW, RESET);
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/services", json, public_key, secret_key);
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"curl -s -X POST '%s/services' -H 'Content-Type: application/json' " "curl -s -X POST '%s/services' -H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' -d '%s'", "%s -d '%s'",
API_BASE, api_key, json); API_BASE, auth_headers, json);
system(cmd); system(cmd);
printf("\n%sService created%s\n", GREEN, RESET); printf("\n%sService created%s\n", GREEN, RESET);
return; return;
@ -565,10 +646,15 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
const char *api_key = getenv("UNSANDBOX_API_KEY"); const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
if (!api_key && argc > 1 && strcmp(argv[1], "session") != 0 && strcmp(argv[1], "service") != 0) { const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET);
return 1; // Fall back to UNSANDBOX_API_KEY for backwards compatibility
if (!public_key) {
public_key = getenv("UNSANDBOX_API_KEY");
}
if (!secret_key) {
secret_key = "";
} }
if (argc < 2) { if (argc < 2) {
@ -584,9 +670,9 @@ int main(int argc, char *argv[]) {
int extend = 0; int extend = 0;
for (int i = 2; i < argc; i++) { for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "--extend") == 0) extend = 1; if (strcmp(argv[i], "--extend") == 0) extend = 1;
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i]; else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
} }
cmd_key(extend, api_key); cmd_key(extend, public_key, secret_key);
return 0; return 0;
} }
@ -607,10 +693,10 @@ int main(int argc, char *argv[]) {
else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]); else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);
else if (strcmp(argv[i], "--tmux") == 0) tmux = 1; else if (strcmp(argv[i], "--tmux") == 0) tmux = 1;
else if (strcmp(argv[i], "--screen") == 0) screen = 1; else if (strcmp(argv[i], "--screen") == 0) screen = 1;
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i]; else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
} }
cmd_session(list, kill, shell, network, vcpu, tmux, screen, api_key); cmd_session(list, kill, shell, network, vcpu, tmux, screen, public_key, secret_key);
return 0; return 0;
} }
@ -645,10 +731,10 @@ int main(int argc, char *argv[]) {
else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) destroy = argv[++i]; else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) destroy = argv[++i];
else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) network = argv[++i]; else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) network = argv[++i];
else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]); else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i]; else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
} }
cmd_service(name, ports, domains, service_type, bootstrap, list, info, logs, tail, sleep_svc, wake, destroy, network, vcpu, api_key); cmd_service(name, ports, domains, service_type, bootstrap, list, info, logs, tail, sleep_svc, wake, destroy, network, vcpu, public_key, secret_key);
return 0; return 0;
} }
@ -677,7 +763,7 @@ int main(int argc, char *argv[]) {
} else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) {
vcpu = atoi(argv[++i]); vcpu = atoi(argv[++i]);
} else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
api_key = argv[++i]; public_key = argv[++i];
} else if (!source_file && argv[i][0] != '-') { } else if (!source_file && argv[i][0] != '-') {
source_file = argv[i]; source_file = argv[i];
} }
@ -688,6 +774,6 @@ int main(int argc, char *argv[]) {
return 1; return 1;
} }
cmd_execute(source_file, envs, env_count, files, file_count, artifacts, output_dir, network, vcpu, api_key); cmd_execute(source_file, envs, env_count, files, file_count, artifacts, output_dir, network, vcpu, public_key, secret_key);
return 0; return 0;
} }