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
cd un-inception
# Set your API key
export UNSANDBOX_API_KEY=your_key_here
# Set your API keys (HMAC authentication)
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run any implementation
python3 un.py test/fib.py
@ -24,6 +25,41 @@ go run un.go test/fib.py
# 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
| Language | File | Category |
@ -121,9 +157,35 @@ Each implementation supports:
# Output: fib(10) = 55
# 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
- [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.Net;
using System.Text;
using System.Security.Cryptography;
class Un
{
@ -110,7 +111,7 @@ class Un
static void CmdExecute(Args args)
{
string apiKey = GetApiKey(args.ApiKey);
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
string code = File.ReadAllText(args.SourceFile);
string language = DetectLanguage(args.SourceFile);
@ -165,7 +166,7 @@ class Un
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"]))
{
@ -197,11 +198,11 @@ class Un
static void CmdSession(Args args)
{
string apiKey = GetApiKey(args.ApiKey);
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
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;
if (sessions == null || sessions.Count == 0)
{
@ -224,7 +225,7 @@ class Un
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}");
return;
}
@ -243,16 +244,16 @@ class Un
}
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($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
}
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"))
{
@ -335,11 +336,11 @@ class Un
static void CmdService(Args args)
{
string apiKey = GetApiKey(args.ApiKey);
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
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;
if (services == null || services.Count == 0)
{
@ -366,42 +367,42 @@ class Un
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));
return;
}
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"] : "");
return;
}
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"] : "");
return;
}
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}");
return;
}
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}");
return;
}
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}");
return;
}
@ -412,7 +413,7 @@ class Un
{
["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"]))
{
Console.Write($"{BLUE}{result["stdout"]}{RESET}");
@ -431,7 +432,7 @@ class Un
{
["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;
if (!string.IsNullOrEmpty(bootstrap))
@ -494,7 +495,7 @@ class Un
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($"Name: {result["name"]}");
if (result.ContainsKey("url"))
@ -508,15 +509,24 @@ class Un
Environment.Exit(1);
}
static string GetApiKey(string argsKey)
static (string, string) GetApiKeys(string argsKey)
{
string key = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
if (string.IsNullOrEmpty(key))
string publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_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}");
Environment.Exit(1);
string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
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)
@ -534,20 +544,46 @@ class Un
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;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(API_BASE + endpoint);
request.Method = method;
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.ContentType = "application/json";
request.Timeout = 300000;
string body = "";
if (data != null)
{
string json = ToJson(data);
byte[] bytes = Encoding.UTF8.GetBytes(json);
body = ToJson(data);
}
// 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;
using (Stream stream = request.GetRequestStream())
{

136
Un.java
View file

@ -45,6 +45,8 @@ import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Un {
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 {
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 language = detectLanguage(args.sourceFile);
@ -137,7 +141,7 @@ public class Un {
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 stderr = (String) result.get("stderr");
@ -168,10 +172,12 @@ public class Un {
}
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) {
Map<String, Object> result = apiRequest("/sessions", "GET", null, apiKey);
Map<String, Object> result = apiRequest("/sessions", "GET", null, publicKey, secretKey);
@SuppressWarnings("unchecked")
List<Map<String, Object>> sessions = (List<Map<String, Object>>) result.get("sessions");
if (sessions == null || sessions.isEmpty()) {
@ -190,7 +196,7 @@ public class Un {
}
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);
return;
}
@ -205,16 +211,18 @@ public class Un {
}
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(YELLOW + "(Interactive sessions require WebSocket - use un2 for full support)" + RESET);
}
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) {
Map<String, Object> result = apiRequest("/services", "GET", null, apiKey);
Map<String, Object> result = apiRequest("/services", "GET", null, publicKey, secretKey);
@SuppressWarnings("unchecked")
List<Map<String, Object>> services = (List<Map<String, Object>>) result.get("services");
if (services == null || services.isEmpty()) {
@ -239,37 +247,37 @@ public class Un {
}
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));
return;
}
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", ""));
return;
}
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", ""));
return;
}
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);
return;
}
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);
return;
}
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);
return;
}
@ -277,7 +285,7 @@ public class Un {
if (args.serviceExecute != null) {
Map<String, Object> payload = new HashMap<>();
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 stderr = (String) result.get("stderr");
if (stdout != null && !stdout.isEmpty()) {
@ -293,7 +301,7 @@ public class Un {
System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "...");
Map<String, Object> payload = new HashMap<>();
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");
if (bootstrap != null && !bootstrap.isEmpty()) {
@ -340,7 +348,7 @@ public class Un {
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("Name: " + result.getOrDefault("name", "N/A"));
if (result.containsKey("url")) {
@ -354,18 +362,20 @@ public class Un {
}
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) {
// First validate to get public_key
Map<String, Object> result = validateKey(apiKey);
String publicKey = (String) result.get("public_key");
if (publicKey == null || publicKey.isEmpty()) {
Map<String, Object> result = validateKey(publicKey, secretKey);
String pubKey = (String) result.get("public_key");
if (pubKey == null || pubKey.isEmpty()) {
System.err.println(RED + "Error: Could not retrieve public key" + RESET);
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(extendUrl);
@ -386,9 +396,9 @@ public class Un {
}
// Default: validate key
Map<String, Object> result = validateKey(apiKey);
Map<String, Object> result = validateKey(publicKey, secretKey);
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 status = (String) result.get("status");
String expiresAt = (String) result.get("expires_at");
@ -399,7 +409,7 @@ public class Un {
if (expired != null && expired) {
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("Expired: " + (expiresAt != null ? expiresAt : "N/A"));
System.out.println(YELLOW + "To renew: Visit " + PORTAL_BASE + "/keys/extend" + RESET);
@ -408,7 +418,7 @@ public class Un {
// Valid key
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("Status: " + (status != null ? status : "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"));
}
private static Map<String, Object> validateKey(String apiKey) throws Exception {
URL url = new URL(PORTAL_BASE + "/keys/validate");
private static Map<String, Object> validateKey(String publicKey, String secretKey) throws Exception {
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();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
conn.setRequestMethod(method);
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.setConnectTimeout(30000);
conn.setReadTimeout(30000);
@ -455,13 +474,46 @@ public class Un {
}
}
private static String getApiKey(String argsKey) {
String key = argsKey != null ? argsKey : System.getenv("UNSANDBOX_API_KEY");
if (key == null || key.isEmpty()) {
System.err.println(RED + "Error: UNSANDBOX_API_KEY not set" + RESET);
private static String[] getApiKeys(String argsKey) {
String publicKey = null;
String secretKey = null;
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);
}
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 {
@ -477,20 +529,26 @@ public class Un {
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);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
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.setConnectTimeout(30000);
conn.setReadTimeout(300000);
if (data != null) {
conn.setDoOutput(true);
String json = toJson(data);
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'
NC='\033[0m' # No Color
# Check if UNSANDBOX_API_KEY is set
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo -e "${RED}Error: UNSANDBOX_API_KEY environment variable not set${NC}"
echo "Please set it with: export UNSANDBOX_API_KEY=usk_your_key_here"
exit 1
# Check if UNSANDBOX auth keys are set (HMAC or legacy)
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
# Fall back to legacy API key
if [ -z "$UNSANDBOX_API_KEY" ]; then
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
# 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 ""
# Check API key
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo -e "${YELLOW}WARNING:${NC} UNSANDBOX_API_KEY not set"
echo "Integration and functional tests will be skipped"
echo "Run: source ../../vars.sh"
echo ""
# Check API auth keys
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo -e "${YELLOW}WARNING:${NC} UNSANDBOX authentication not configured"
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 "Run: source ../../vars.sh"
echo ""
fi
fi
# Counters

View file

@ -40,11 +40,15 @@ run_test() {
echo ""
}
# Check for API key
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo "⚠ WARNING: UNSANDBOX_API_KEY not set"
echo " Integration and functional tests will be skipped"
echo ""
# Check for API auth keys
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo "⚠ WARNING: UNSANDBOX authentication not configured"
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
# Run tests for each language

View file

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

View file

@ -25,10 +25,14 @@ echo -e "${CYAN}╚════════════════════
echo ""
# Check requirements
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo -e "${RED}ERROR:${NC} UNSANDBOX_API_KEY not set"
echo "Run: source ../../vars.sh"
exit 1
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
if [ -z "$UNSANDBOX_API_KEY" ]; then
echo -e "${RED}ERROR:${NC} UNSANDBOX authentication not configured"
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
if [ ! -x "$CLI_DIR/un2" ]; then

View file

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

View file

@ -133,25 +133,40 @@ async function runTests() {
results.failTest('Extension detection: .unknown -> undefined', e.message);
}
// Test 7: API call test (requires UNSANDBOX_API_KEY)
if (!process.env.UNSANDBOX_API_KEY) {
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set');
// Test 7: API call test (requires UNSANDBOX auth)
const hasHMAC = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
const hasLegacy = process.env.UNSANDBOX_API_KEY;
if (!hasHMAC && !hasLegacy) {
results.skipTest('API call test', 'UNSANDBOX authentication not configured');
} else {
try {
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({
language: 'python',
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 options = {
hostname: 'api.unsandbox.com',
path: '/execute',
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
@ -185,8 +200,10 @@ async function runTests() {
}
// Test 8: End-to-end test with fib.py
if (!process.env.UNSANDBOX_API_KEY) {
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set');
const hasHMAC2 = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
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)) {
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
} else {

View file

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

View file

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

View file

@ -21,14 +21,14 @@ TESTS_FAILED=0
# Test result tracking
test_passed() {
((TESTS_PASSED++))
((TESTS_RUN++))
TESTS_PASSED=$((TESTS_PASSED + 1))
TESTS_RUN=$((TESTS_RUN + 1))
echo -e "${GREEN}✓ PASS${NC}: $1"
}
test_failed() {
((TESTS_FAILED++))
((TESTS_RUN++))
TESTS_FAILED=$((TESTS_FAILED + 1))
TESTS_RUN=$((TESTS_RUN + 1))
echo -e "${RED}✗ FAIL${NC}: $1"
if [ -n "${2:-}" ]; then
echo -e "${RED} Error: $2${NC}"
@ -69,10 +69,10 @@ fi
if output=$("$UN_SH" /tmp/nonexistent_file_12345.xyz 2>&1); then
test_failed "Handles non-existent file" "Should exit with error"
else
if echo "$output" | grep -q "not found"; then
if echo "$output" | grep -qi "not found\|error"; then
test_passed "Handles non-existent file"
else
test_failed "Handles non-existent file" "Expected 'not found' message"
test_failed "Handles non-existent file" "Expected error message, got: $output"
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"
rm -f "$UNKNOWN_FILE"
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"
else
test_failed "Handles unknown file extension" "Expected 'Unknown file extension' message"
test_failed "Handles unknown file extension" "Expected error message, got: $output"
fi
rm -f "$UNKNOWN_FILE"
fi
# Test: Error when API key not set
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
# Test: Error when auth not set
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"
if [ -f "$TEST_FILE" ]; then
# Temporarily unset API key
OLD_KEY="$UNSANDBOX_API_KEY"
# Temporarily unset auth keys
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
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
if echo "$output" | grep -q "UNSANDBOX_API_KEY"; then
test_passed "Requires API key"
if echo "$output" | grep -qE "UNSANDBOX_(API_KEY|PUBLIC_KEY|SECRET_KEY)"; then
test_passed "Requires authentication"
else
test_failed "Requires API key" "Expected API key error message"
test_failed "Requires authentication" "Expected auth error message"
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
test_skipped "Requires API key (test file not found)"
test_skipped "Requires authentication (test file not found)"
fi
else
test_skipped "Requires API key (API key already not set)"
test_skipped "Requires authentication (auth already not set)"
fi
# Integration Tests (require API key)
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
# Integration Tests (require auth)
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}"
# 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)"
fi
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
# 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
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: 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"
if {!$has_required_packages} {
test_skipped "Shows usage message with no arguments (missing TCL packages)"
test_skipped "Handles non-existent file (missing TCL packages)"
test_skipped "Handles unknown file extension (missing TCL packages)"
} 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
set result [run_command [list $UN_TCL /tmp/nonexistent_file_12345.xyz]]
set exit_code [lindex $result 0]
set output [lindex $result 1]
# Test: Error when authentication not set
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 has_legacy [expr {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""}]
if {$exit_code != 0 && [string match "*not found*" $output]} {
test_passed "Handles non-existent file"
} else {
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 ""} {
if {!$has_required_packages} {
test_skipped "Requires authentication (missing TCL packages)"
} elseif {$has_hmac || $has_legacy} {
set test_file [file join $TEST_DIR fib.py]
if {[file exists $test_file]} {
# Temporarily unset API key
set old_key $::env(UNSANDBOX_API_KEY)
unset ::env(UNSANDBOX_API_KEY)
# Temporarily unset auth keys
if {$has_hmac} {
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 exit_code [lindex $result 0]
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]} {
test_passed "Requires API key"
if {$exit_code != 0 && ([string match "*UNSANDBOX_PUBLIC_KEY*" $output] || [string match "*UNSANDBOX_API_KEY*" $output])} {
test_passed "Requires authentication"
} else {
test_failed "Requires API key" "Expected API key error message"
test_failed "Requires authentication" "Expected auth error message"
}
} else {
test_skipped "Requires API key (test file not found)"
test_skipped "Requires authentication (test file not found)"
}
} else {
test_skipped "Requires API key (API key already not set)"
test_skipped "Requires authentication (auth already not set)"
}
# Integration Tests (require API key)
if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
# Integration Tests (require authentication and packages)
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 ==="]"
# 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)"
}
} 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

View file

@ -11,12 +11,16 @@ import * as path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import * as https from 'https';
import * as crypto from 'crypto';
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
const UN_SCRIPT = path.join(__dirname, '..', 'un.ts');
const FIB_PY = path.join(__dirname, '..', '..', 'test', 'fib.py');
const UN_SCRIPT = path.join(SCRIPT_DIR, '..', 'un.ts');
const FIB_PY = path.join(SCRIPT_DIR, '..', '..', 'test', 'fib.py');
class TestResults {
passed: number = 0;
@ -140,26 +144,37 @@ async function runTests(): Promise<void> {
results.failTest('Extension detection: .unknown -> undefined', (e as Error).message);
}
// Test 7: API call test (requires UNSANDBOX_API_KEY)
if (!process.env.UNSANDBOX_API_KEY) {
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set');
// Test 7: API call test (requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY)
const hasHmac = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
const hasLegacy = process.env.UNSANDBOX_API_KEY;
if (!hasHmac && !hasLegacy) {
results.skipTest('API call test', 'UNSANDBOX authentication not set');
} else {
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({
language: 'python',
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 options: https.RequestOptions = {
hostname: 'api.unsandbox.com',
path: '/execute',
method: 'POST',
path: apiPath,
method: method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${publicKey}`,
'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
if (!process.env.UNSANDBOX_API_KEY) {
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set');
if (!hasHmac && !hasLegacy) {
results.skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not set');
} else if (!fs.existsSync(FIB_PY)) {
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
} else {

174
un.awk
View file

@ -60,15 +60,32 @@ BEGIN {
RESET = "\033[0m"
}
function get_api_key() {
cmd = "echo $UNSANDBOX_API_KEY"
cmd | getline api_key
function get_api_keys( public_key, secret_key, cmd) {
# Get public key
cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"
cmd | getline public_key
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
}
return api_key
GLOBAL_PUBLIC_KEY = public_key
GLOBAL_SECRET_KEY = secret_key
}
function get_extension(filename) {
@ -88,8 +105,9 @@ function escape_json(s) {
return s
}
function execute(filename) {
api_key = get_api_key()
function execute(filename , api_key) {
get_api_keys()
api_key = GLOBAL_PUBLIC_KEY
# Get extension and language
ext = get_extension(filename)
@ -119,10 +137,27 @@ function execute(filename) {
print json > 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
cmd = "curl -s -X POST '" API_BASE "/execute' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \
sig_headers \
"-d '@" tmp "'"
response = ""
@ -160,43 +195,94 @@ function execute(filename) {
}
}
function session_list() {
api_key = get_api_key()
cmd = "curl -s '" API_BASE "/sessions' -H 'Authorization: Bearer " api_key "'"
function session_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
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
close(cmd)
}
function session_kill(id) {
api_key = get_api_key()
cmd = "curl -s -X DELETE '" API_BASE "/sessions/" id "' -H 'Authorization: Bearer " api_key "'"
function session_kill(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
get_api_keys()
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)
print GREEN "Session terminated: " id RESET
}
function service_list() {
api_key = get_api_key()
cmd = "curl -s '" API_BASE "/services' -H 'Authorization: Bearer " api_key "'"
function service_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
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
close(cmd)
}
function service_destroy(id) {
api_key = get_api_key()
cmd = "curl -s -X DELETE '" API_BASE "/services/" id "' -H 'Authorization: Bearer " api_key "'"
function service_destroy(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
get_api_keys()
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)
print GREEN "Service destroyed: " id RESET
}
function service_dump_bootstrap(id, dump_file) {
api_key = get_api_key()
function service_dump_bootstrap(id, dump_file , endpoint, json_body, timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
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
cmd = "curl -s -X POST '" API_BASE "/services/" id "/execute' " \
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers \
"-d '" json_body "'"
response = ""
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) {
api_key = get_api_key()
function service_create(name, ports, domains, service_type, bootstrap , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
# Build JSON payload
json = "{\"name\":\"" escape_json(name) "\""
@ -265,10 +351,22 @@ function service_create(name, ports, domains, service_type, bootstrap) {
print json > 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
cmd = "curl -s -X POST '" API_BASE "/services' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "' " \
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers \
"-d '@" tmp "'"
response = ""
@ -284,11 +382,24 @@ function service_create(name, ports, domains, service_type, bootstrap) {
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
cmd = "curl -s -X POST '" PORTAL_BASE "/keys/validate' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " api_key "'"
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers
response = ""
while ((cmd | getline line) > 0) {
@ -366,8 +477,7 @@ function validate_key(api_key, do_extend) {
}
function cmd_key(do_extend) {
api_key = get_api_key()
validate_key(api_key, do_extend)
validate_key(do_extend)
}
function show_help() {

85
un.clj
View file

@ -101,41 +101,82 @@
(or (second (re-find pattern-str 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 []
(or (System/getenv "UNSANDBOX_API_KEY")
(do (binding [*out* *err*]
(println "Error: UNSANDBOX_API_KEY not set"))
(System/exit 1))))
(first (get-api-keys)))
(defn hmac-sha256 [secret message]
(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]
(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)
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST"
(str "https://api.unsandbox.com" endpoint)
"-H" "Content-Type: application/json"
"-H" (str "Authorization: Bearer " api-key)
"-d" (str "@" tmp-file))]
(let [args (concat ["curl" "-s" "-X" "POST"
(str "https://api.unsandbox.com" endpoint)
"-H" "Content-Type: application/json"]
auth-headers
["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true)
out)))
(defn curl-get [api-key endpoint]
(:out (sh "curl" "-s"
(str "https://api.unsandbox.com" endpoint)
"-H" (str "Authorization: Bearer " api-key))))
(let [[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")
args (concat ["curl" "-s"
(str "https://api.unsandbox.com" endpoint)]
auth-headers)]
(:out (apply sh args))))
(defn curl-delete [api-key endpoint]
(:out (sh "curl" "-s" "-X" "DELETE"
(str "https://api.unsandbox.com" endpoint)
"-H" (str "Authorization: Bearer " api-key))))
(let [[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")
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]
(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)
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST"
(str portal-base endpoint)
"-H" "Content-Type: application/json"
"-H" (str "Authorization: Bearer " api-key)
"-d" (str "@" tmp-file))]
(let [args (concat ["curl" "-s" "-X" "POST"
(str portal-base endpoint)
"-H" "Content-Type: application/json"]
auth-headers
["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true)
out)))

44
un.cob
View file

@ -55,6 +55,8 @@
01 WS-FILENAME PIC X(256).
01 WS-FILE-STATUS PIC XX.
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-EXTENSION PIC X(16).
01 WS-CURL-CMD PIC X(4096).
@ -259,16 +261,46 @@
END-IF.
MAKE-EXECUTE-REQUEST.
* Build curl command using shell
STRING "curl -s -X POST https://api.unsandbox.com/execute "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
"' --data-binary @- -o /tmp/unsandbox_resp.json "
"< <(jq -Rs '{language: """
* Get public/secret keys with fallback
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
IF WS-PUBLIC-KEY NOT = SPACES
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
IF WS-SECRET-KEY = SPACES
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)
""", code: .}' < '"
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 | "
"sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; "
"jq -r '.stderr // empty' /tmp/unsandbox_resp.json | "

111
un.cpp
View file

@ -50,6 +50,7 @@
#include <vector>
#include <map>
#include <cstdlib>
#include <ctime>
#include <sys/stat.h>
using namespace std;
@ -111,7 +112,37 @@ string exec_curl(const string& cmd) {
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);
if (lang.empty()) {
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;
json << "}";
string auth_headers = build_auth_headers("POST", "/execute", json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + "/execute' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
+ auth_headers + " "
"-d '" + json.str() + "'";
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);
}
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) {
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;
return;
}
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);
cout << GREEN << "Session terminated: " << kill << RESET << endl;
return;
@ -211,54 +245,62 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
json << "}";
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' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
+ auth_headers + " "
"-d '" + json.str() + "'";
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) {
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;
return;
}
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;
return;
}
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);
return;
}
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);
return;
}
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);
cout << GREEN << "Service sleeping: " << sleep << RESET << endl;
return;
}
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);
cout << GREEN << "Service waking: " << wake << RESET << endl;
return;
}
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);
cout << GREEN << "Service destroyed: " << destroy << RESET << endl;
return;
@ -267,9 +309,10 @@ void cmd_service(const string& name, const string& ports, const string& type, co
if (!execute.empty()) {
ostringstream json;
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' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
+ auth_headers + " "
"-d '" + json.str() + "'";
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()) {
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' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'";
+ auth_headers + " "
"-d '" + json_body + "'";
string result = exec_curl(cmd);
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 << "}";
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' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "' "
+ auth_headers + " "
"-d '" + json.str() + "'";
cout << exec_curl(cmd) << endl;
return;
@ -382,10 +428,11 @@ void cmd_service(const string& name, const string& ports, const string& type, co
exit(1);
}
void cmd_validate_key(bool extend, const string& api_key) {
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' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " + api_key + "'";
+ auth_headers;
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[]) {
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) {
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 == "--tmux") tmux = 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;
}
@ -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 == "-n" && i+1 < argc) network = 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;
}
@ -536,10 +589,10 @@ int main(int argc, char* argv[]) {
for (int i = 2; i < argc; i++) {
string arg = argv[i];
if (arg == "--extend") extend = true;
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
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;
}
@ -556,7 +609,7 @@ int main(int argc, char* argv[]) {
else if (arg == "-a") artifacts = true;
else if (arg == "-n" && i+1 < argc) network = 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;
}
@ -565,6 +618,6 @@ int main(int argc, char* argv[]) {
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;
}

98
un.cr
View file

@ -41,6 +41,7 @@ require "http/client"
require "json"
require "base64"
require "option_parser"
require "openssl/hmac"
# Extension to language mapping
EXT_MAP = {
@ -74,28 +75,51 @@ def detect_language(filename : String) : String
EXT_MAP.fetch(ext, "unknown")
end
def get_api_key(args_key : String?) : String
key = args_key || ENV["UNSANDBOX_API_KEY"]?
if key.nil? || key.empty?
STDERR.puts "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
exit 1
def get_api_keys(args_key : String?) : {String, String?}
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
# 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
key
{public_key, secret_key}
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)
headers = HTTP::Headers{
"Content-Type" => "application/json",
"Authorization" => "Bearer #{api_key}"
"Content-Type" => "application/json"
}
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
response = case method
when "GET"
HTTP::Client.get(url, headers: headers)
when "POST"
body = data ? data.to_json : ""
HTTP::Client.post(url, headers: headers, body: body)
when "DELETE"
HTTP::Client.delete(url, headers: headers)
@ -112,7 +136,7 @@ def api_request(endpoint : String, api_key : String, method = "GET", data : JSON
end
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)
unless File.exists?(filename)
@ -173,7 +197,7 @@ def cmd_execute(args)
end
# 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
if stdout = result["stdout"]?.try(&.as_s?)
@ -202,10 +226,10 @@ def cmd_execute(args)
end
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)
result = api_request("/sessions", api_key)
result = api_request("/sessions", public_key, secret_key)
sessions = result["sessions"]?.try(&.as_a?) || [] of JSON::Any
if sessions.empty?
puts "No active sessions"
@ -223,7 +247,7 @@ def cmd_session(args)
end
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}"
return
end
@ -233,17 +257,33 @@ def cmd_session(args)
end
def cmd_key(args)
api_key = get_api_key(args[:api_key]?)
public_key, secret_key = get_api_keys(args[:api_key]?)
# Validate key
url = URI.parse(PORTAL_BASE + "/keys/validate")
headers = HTTP::Headers{
"Content-Type" => "application/json",
"Authorization" => "Bearer #{api_key}"
"Content-Type" => "application/json"
}
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
response = HTTP::Client.post(url, headers: headers, body: "{}")
response = HTTP::Client.post(url, headers: headers, body: body)
result = JSON.parse(response.body)
status = result["status"]?.try(&.as_s?) || "unknown"
@ -311,10 +351,10 @@ def cmd_key(args)
end
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)
result = api_request("/services", api_key)
result = api_request("/services", public_key, secret_key)
services = result["services"]?.try(&.as_a?) || [] of JSON::Any
if services.empty?
puts "No services"
@ -334,31 +374,31 @@ def cmd_service(args)
end
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
return
end
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?) || ""
return
end
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}"
return
end
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}"
return
end
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}"
return
end
@ -366,7 +406,7 @@ def cmd_service(args)
if execute_id = args[:execute]?.as?(String)
command = args[:command]?.as?(String) || ""
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?)
print BLUE, stdout, RESET
end
@ -379,7 +419,7 @@ def cmd_service(args)
if dump_id = args[:dump_bootstrap]?.as?(String)
STDERR.puts "Fetching bootstrap script from #{dump_id}..."
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 file_path = args[:dump_file]?.as?(String)
@ -433,7 +473,7 @@ def cmd_service(args)
end
# 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 "Name: #{result["name"]?.try(&.as_s?) || "N/A"}"
if url = result["url"]?.try(&.as_s?)

122
un.d
View file

@ -88,12 +88,43 @@ string escapeJson(string s) {
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) {
auto result = executeShell(cmd);
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);
if (lang.empty) {
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);
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);
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) {
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));
return;
}
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);
writefln("%sSession terminated: %s%s", GREEN, kill, RESET);
return;
@ -148,51 +183,65 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
json ~= "}";
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));
}
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) {
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));
return;
}
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));
return;
}
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));
return;
}
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));
return;
}
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);
writefln("%sService sleeping: %s%s", GREEN, sleep, RESET);
return;
}
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);
writefln("%sService waking: %s%s", GREEN, wake, RESET);
return;
}
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);
writefln("%sService destroyed: %s%s", GREEN, destroy, RESET);
return;
@ -200,7 +249,9 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
if (!execute.empty) {
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);
// 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) {
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);
import std.algorithm : findSplitAfter;
@ -283,7 +337,8 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
json ~= "}";
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));
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.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);
auto lines = response.split("\n");
@ -413,7 +469,13 @@ void validateKey(string apiKey, bool extend) {
}
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) {
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] == "--tmux") tmux = 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;
}
@ -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] == "-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] == "-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;
}
@ -480,15 +542,15 @@ int main(string[] args) {
for (size_t i = 2; i < args.length; i++) {
if (args[i] == "--extend") extend = true;
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i];
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
}
if (apiKey.empty) {
stderr.writefln("%sError: UNSANDBOX_API_KEY not set%s", RED, RESET);
if (publicKey.empty) {
stderr.writefln("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s", RED, RESET);
return 1;
}
validateKey(apiKey, extend);
validateKey(publicKey, secretKey, extend);
return 0;
}
@ -503,7 +565,7 @@ int main(string[] args) {
else if (args[i] == "-a") artifacts = true;
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] == "-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];
}
@ -512,6 +574,6 @@ int main(string[] args) {
return 1;
}
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey);
cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey);
return 0;
}

92
un.dart
View file

@ -42,6 +42,7 @@
import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
const String apiBase = 'https://api.unsandbox.com';
const String portalBase = 'https://unsandbox.com';
@ -98,13 +99,21 @@ class Args {
bool keyExtend = false;
}
String getApiKey(String? argsKey) {
final key = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
if (key == null || key.isEmpty) {
stderr.writeln('${red}Error: UNSANDBOX_API_KEY not set$reset');
exit(1);
List<String?> getApiKeys(String? argsKey) {
final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY'];
// 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) {
@ -120,18 +129,37 @@ String detectLanguage(String filename) {
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 tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
try {
final body = jsonData ?? '';
if (jsonData != null) {
await tempFile.writeAsString(jsonData);
}
final args = ['curl', '-s', '-X', method, '$base$endpoint',
'-H', 'Content-Type: application/json',
'-H', 'Authorization: Bearer $apiKey'];
'-H', 'Content-Type: application/json'];
// 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) {
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 {
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 language = detectLanguage(args.sourceFile!);
@ -195,7 +225,7 @@ Future<void> cmdExecute(Args args) async {
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 stderrText = result['stderr'] as String?;
@ -227,10 +257,12 @@ Future<void> cmdExecute(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) {
final result = await apiRequestCurl('/sessions', 'GET', null, apiKey);
final result = await apiRequestCurl('/sessions', 'GET', null, publicKey, secretKey);
final sessions = result['sessions'] as List? ?? [];
if (sessions.isEmpty) {
print('No active sessions');
@ -245,7 +277,7 @@ Future<void> cmdSession(Args args) async {
}
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');
return;
}
@ -261,16 +293,18 @@ Future<void> cmdSession(Args args) async {
}
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('${yellow}(Interactive sessions require WebSocket - use un2 for full support)$reset');
}
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) {
final result = await apiRequestCurl('/services', 'GET', null, apiKey);
final result = await apiRequestCurl('/services', 'GET', null, publicKey, secretKey);
final services = result['services'] as List? ?? [];
if (services.isEmpty) {
print('No services');
@ -287,37 +321,37 @@ Future<void> cmdService(Args args) async {
}
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));
return;
}
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'] ?? '');
return;
}
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'] ?? '');
return;
}
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');
return;
}
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');
return;
}
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');
return;
}
@ -326,7 +360,7 @@ Future<void> cmdService(Args args) async {
final payload = <String, dynamic>{
'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 stderrText = result['stderr'] as String?;
if (stdoutText != null && stdoutText.isNotEmpty) {
@ -343,7 +377,7 @@ Future<void> cmdService(Args args) async {
final payload = <String, dynamic>{
'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?;
if (bootstrap != null && bootstrap.isNotEmpty) {
@ -386,7 +420,7 @@ Future<void> cmdService(Args args) async {
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('Name: ${result['name'] ?? 'N/A'}');
if (result.containsKey('url')) {
@ -400,10 +434,12 @@ Future<void> cmdService(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 {
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
if (args.keyExtend) {

60
un.erl
View file

@ -294,12 +294,44 @@ open_extend_page(PublicKey) ->
end.
%% 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() ->
case os:getenv("UNSANDBOX_API_KEY") of
false ->
io:format("Error: UNSANDBOX_API_KEY not set~n"),
halt(1);
Key -> Key
{PublicKey, _} = get_api_keys(),
PublicKey.
hmac_sha256(Secret, Message) ->
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.
ext_to_lang(".hs") -> {ok, "haskell"};
@ -364,30 +396,40 @@ write_temp_file(Data) ->
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 ++
" -H 'Content-Type: application/json'" ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
AuthHeaders ++
" -d @" ++ TmpFile,
os:cmd(Cmd).
curl_post_portal(ApiKey, Endpoint, 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 ++
" -H 'Content-Type: application/json'" ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
AuthHeaders ++
" -d @" ++ TmpFile,
Result = os:cmd(Cmd),
file:delete(TmpFile),
Result.
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 ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
AuthHeaders,
os:cmd(Cmd).
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 ++
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
AuthHeaders,
os:cmd(Cmd).
%% Argument parsing

99
un.ex
View file

@ -401,15 +401,39 @@ defmodule Un do
end
# Helpers
defp get_api_key do
case System.get_env("UNSANDBOX_API_KEY") do
nil ->
IO.puts(:stderr, "Error: UNSANDBOX_API_KEY not set")
defp get_api_keys do
public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
# 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)
key -> key
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
s
|> String.replace("\\", "\\\\")
@ -427,50 +451,79 @@ defmodule Un do
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.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",
"https://api.unsandbox.com#{endpoint}",
"-H", "Content-Type: application/json",
"-H", "Authorization: Bearer #{api_key}",
"-d", "@#{tmp_file}"
], stderr_to_stdout: true)
"-H", "Content-Type: application/json"
] ++ headers ++ ["-d", "@#{tmp_file}"]
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file)
output
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
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.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",
"#{@portal_base}#{endpoint}",
"-H", "Content-Type: application/json",
"-H", "Authorization: Bearer #{api_key}",
"-d", "@#{tmp_file}"
], stderr_to_stdout: true)
"-H", "Content-Type: application/json"
] ++ headers ++ ["-d", "@#{tmp_file}"]
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file)
output
end
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",
"https://api.unsandbox.com#{endpoint}",
"-H", "Authorization: Bearer #{api_key}"
], stderr_to_stdout: true)
"https://api.unsandbox.com#{endpoint}"
] ++ headers
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
output
end
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",
"https://api.unsandbox.com#{endpoint}",
"-H", "Authorization: Bearer #{api_key}"
], stderr_to_stdout: true)
"https://api.unsandbox.com#{endpoint}"
] ++ headers
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
output
end

85
un.f90
View file

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

211
un.forth
View file

@ -91,15 +91,30 @@
find-ext ext-lang
;
\ Get API key from environment
: get-api-key ( -- addr len )
s" UNSANDBOX_API_KEY" getenv
\ Get API keys from environment (HMAC or legacy)
: get-public-key ( -- addr len )
s" UNSANDBOX_PUBLIC_KEY" getenv
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)
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-file ( addr len -- )
\ Check file exists
@ -125,8 +140,11 @@
s" /tmp/unsandbox_script.sh" w/o create-file throw
>r
s" #!/bin/bash" r@ write-line throw
s" API_KEY='" r@ write-file throw
get-api-key r@ write-file throw
s" 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" LANG='" r@ write-file throw
2swap 2drop \ drop language, keep filename on stack
@ -135,7 +153,11 @@
s" FILE='" r@ write-file throw
r@ write-file 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
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
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/sessions -H 'Authorization: Bearer " r@ write-file throw
get-api-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" 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" 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
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
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mSession terminated: " r@ write-file throw
s" '" 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" 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
s" \\x1b[0m'" r@ write-line throw
r> close-file throw
@ -175,9 +212,16 @@
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" curl -s -X GET https://api.unsandbox.com/services -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' | jq -r '.services[] | \"\\(.id) \\(.name) \\(.status)\"' 2>/dev/null || echo 'No services'" 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" 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
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
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' | jq ." r@ write-line throw
s" '" 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" 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
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
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" /logs -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' | jq -r '.logs'" r@ write-line throw
s" '" 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" 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
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
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" /sleep -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService sleeping: " r@ write-file throw
s" '" 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" 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
s" \\x1b[0m'" r@ write-line throw
r> close-file throw
@ -231,11 +299,19 @@
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" /wake -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService waking: " r@ write-file throw
s" '" 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" 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
s" \\x1b[0m'" r@ write-line throw
r> close-file throw
@ -247,11 +323,19 @@
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' >/dev/null && echo -e '\\x1b[32mService destroyed: " r@ write-file throw
s" '" 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" 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
s" \\x1b[0m'" r@ write-line throw
r> close-file throw
@ -263,14 +347,21 @@
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
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
s" ...' >&2" r@ write-line throw
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw
2over r@ write-file throw
s" /execute -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}')" r@ write-line throw
s" '" 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" 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" if [ -n \"$STDOUT\" ]; then" r@ write-line throw
2dup 0 0 d= if
@ -302,6 +393,12 @@
\ 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" #!/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" for ((i=3; i<$#; i+=2)); do" 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 \"$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" curl -s -X POST https://api.unsandbox.com/services -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw
get-api-key r@ write-file throw
s" ' -d \"$PAYLOAD\" | jq ." r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/services:$PAYLOAD\"" 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
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
s" /tmp/unsandbox_key_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" API_KEY='" r@ write-file throw
get-api-key r@ write-file throw
s" 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" PORTAL_BASE='" r@ write-file throw
portal-base r@ write-file 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
0= if
\ Normal validation
s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw
s" 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" if [ $STATUS -ne 0 ]; then" 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
else
\ Extend mode
s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\")" r@ write-line throw
s" PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw
s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$PUBLIC_KEY\" 2>/dev/null" r@ write-line throw
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" FETCHED_PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw
s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$FETCHED_PUBLIC_KEY\" 2>/dev/null" r@ write-line throw
then
r> close-file throw

78
un.fs
View file

@ -44,6 +44,7 @@ open System
open System.IO
open System.Net
open System.Text
open System.Security.Cryptography
let apiBase = "https://api.unsandbox.com"
let portalBase = "https://unsandbox.com"
@ -101,12 +102,19 @@ type Args = {
mutable KeyExtend: bool
}
let getApiKey (argsKey: string option) =
let key = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
if String.IsNullOrEmpty(key) then
eprintfn "%sError: UNSANDBOX_API_KEY not set%s" red reset
exit 1
key
let getApiKeys (argsKey: string option) =
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
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 dotIndex = filename.LastIndexOf('.')
@ -222,19 +230,35 @@ let parseJson (json: string) =
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
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- method
request.ContentType <- "application/json"
request.Headers.Add("Authorization", sprintf "Bearer %s" apiKey)
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
| Some d ->
let json = toJson (box d)
let bytes = Encoding.UTF8.GetBytes(json)
let bytes = Encoding.UTF8.GetBytes(body)
request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream()
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
let cmdExecute (args: Args) =
let apiKey = getApiKey args.ApiKey
let (publicKey, secretKey) = getApiKeys args.ApiKey
let code = File.ReadAllText(args.SourceFile.Value)
let language = detectLanguage args.SourceFile.Value
@ -286,7 +310,7 @@ let cmdExecute (args: Args) =
if args.Vcpu > 0 then
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
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
@ -307,14 +331,14 @@ let cmdExecute (args: Args) =
exit exitCode
let cmdSession (args: Args) =
let apiKey = getApiKey args.ApiKey
let (publicKey, secretKey) = getApiKeys args.ApiKey
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 "No sessions (list parsing not implemented)"
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
else
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)]
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
| Some id -> printfn "%sSession created: %s%s" green (id.ToString()) reset
| None -> printfn "%sSession created%s" green reset
@ -412,37 +436,37 @@ let cmdKey (args: Args) =
exit 1
let cmdService (args: Args) =
let apiKey = getApiKey args.ApiKey
let (publicKey, secretKey) = getApiKeys args.ApiKey
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 "No services (list parsing not implemented)"
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))
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
| Some logs -> printfn "%s" (logs.ToString())
| None -> ()
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
| Some logs -> printfn "%s" (logs.ToString())
| None -> ()
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
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
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
elif args.ServiceExecute.IsSome then
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
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
printf "%s%s%s" blue (stdout.ToString()) reset
@ -454,7 +478,7 @@ let cmdService (args: Args) =
elif args.ServiceDumpBootstrap.IsSome then
eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value
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
| Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) ->
@ -485,7 +509,7 @@ let cmdService (args: Args) =
if args.Vcpu > 0 then
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
| Some id -> printfn "%sService created: %s%s" green (id.ToString()) reset
| None -> printfn "%sService created%s" green reset

109
un.go
View file

@ -47,7 +47,10 @@ package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
@ -133,21 +136,38 @@ func detectLanguage(filename string) (string, error) {
return "", fmt.Errorf("cannot detect language from extension")
}
func getAPIKey(keyArg string) string {
if keyArg != "" {
return keyArg
func getAPIKeys(keyArg string) (string, string) {
publicKey := os.Getenv("UNSANDBOX_PUBLIC_KEY")
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 == "" {
fmt.Fprintf(os.Stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", Red, Reset)
os.Exit(1)
}
return key
return publicKey, secretKey
}
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
var reqBody io.Reader
bodyStr := ""
if data != nil {
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)
os.Exit(1)
}
bodyStr = string(jsonData)
reqBody = bytes.NewBuffer(jsonData)
}
@ -164,7 +185,13 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
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")
client := &http.Client{}
@ -195,7 +222,7 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
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)
if err != nil {
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
}
result := apiRequest("/execute", "POST", payload, apiKey)
result := apiRequest("/execute", "POST", payload, publicKey, secretKey)
// Print output
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)
}
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 != "" {
result := apiRequest("/sessions", "GET", nil, apiKey)
result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
sessions := result["sessions"].([]interface{})
if len(sessions) == 0 {
fmt.Println("No active sessions")
@ -312,7 +339,7 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
}
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)
return
}
@ -338,13 +365,13 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
}
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)
}
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 != "" {
result := apiRequest("/services", "GET", nil, apiKey)
result := apiRequest("/services", "GET", nil, publicKey, secretKey)
services := result["services"].([]interface{})
if len(services) == 0 {
fmt.Println("No services")
@ -376,45 +403,45 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
}
if serviceInfo != "" {
result := apiRequest("/services/"+serviceInfo, "GET", nil, apiKey)
result := apiRequest("/services/"+serviceInfo, "GET", nil, publicKey, secretKey)
jsonData, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(jsonData))
return
}
if serviceLogs != "" {
result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, apiKey)
result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, publicKey, secretKey)
fmt.Print(result["logs"])
return
}
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"])
return
}
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)
return
}
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)
return
}
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)
return
}
if serviceExecute != "" {
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 {
fmt.Printf("%s%s%s", Blue, stdout, Reset)
}
@ -427,7 +454,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
if serviceDumpBootstrap != "" {
fmt.Fprintf(os.Stderr, "Fetching bootstrap script from %s...\n", serviceDumpBootstrap)
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 serviceDumpFile != "" {
@ -482,7 +509,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
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("Name: %s\n", result["name"])
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"
reqBody := bytes.NewBuffer(nil)
@ -534,7 +561,13 @@ func validateKey(apiKey string, extend bool) {
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")
client := &http.Client{}
@ -702,7 +735,7 @@ func main() {
switch os.Args[1] {
case "session":
sessionCmd.Parse(os.Args[2:])
key := getAPIKey(*sessionKey)
publicKey, secretKey := getAPIKeys(*sessionKey)
net := *sessionNetwork
if net == "" {
net = *network
@ -711,12 +744,12 @@ func main() {
if vc == 0 {
vc = *vcpu
}
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, key)
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, publicKey, secretKey)
return
case "service":
serviceCmd.Parse(os.Args[2:])
key := getAPIKey(*serviceKey)
publicKey, secretKey := getAPIKeys(*serviceKey)
net := *serviceNetwork
if net == "" {
net = *network
@ -725,13 +758,13 @@ func main() {
if vc == 0 {
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
case "key":
keyCmd.Parse(os.Args[2:])
key := getAPIKey(*keyKey)
validateKey(key, *keyExtend)
publicKey, secretKey := getAPIKeys(*keyKey)
validateKey(publicKey, secretKey, *keyExtend)
return
}
}
@ -746,6 +779,6 @@ func main() {
}
sourceFile := flag.Arg(0)
key := getAPIKey(*apiKey)
cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, key)
publicKey, secretKey := getAPIKeys(*apiKey)
cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, publicKey, secretKey)
}

104
un.groovy
View file

@ -92,13 +92,24 @@ class Args {
Boolean keyExtend = false
}
def getApiKey(argsKey) {
def key = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
if (!key) {
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}")
System.exit(1)
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
def getApiKeys(argsKey) {
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) {
@ -116,16 +127,33 @@ def detectLanguage(filename) {
return language
}
def apiRequest(endpoint, method, data, apiKey) {
def apiRequest(endpoint, method, data, publicKey, secretKey) {
def tempFile = File.createTempFile('un_request_', '.json')
try {
def body = data ?: ""
if (data) {
tempFile.text = data
}
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', 'Content-Type: application/json',
'-H', "Authorization: Bearer ${apiKey}"]
'-H', 'Content-Type: application/json']
// 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) {
curlCmd += ['-d', "@${tempFile.absolutePath}"]
@ -147,7 +175,7 @@ def apiRequest(endpoint, method, data, apiKey) {
}
def cmdExecute(args) {
def apiKey = getApiKey(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def file = new File(args.sourceFile)
if (!file.exists()) {
System.err.println("${RED}Error: File not found: ${args.sourceFile}${RESET}")
@ -203,7 +231,7 @@ def cmdExecute(args) {
json += '}'
def output = apiRequest('/execute', 'POST', json, apiKey)
def output = apiRequest('/execute', 'POST', json, publicKey, secretKey)
def stdoutMatch = output =~ /"stdout":"((?:[^"\\]|\\.)*)"/
def stderrMatch = output =~ /"stderr":"((?:[^"\\]|\\.)*)"/
@ -245,17 +273,17 @@ def cmdExecute(args) {
}
def cmdSession(args) {
def apiKey = getApiKey(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey)
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("No sessions (list parsing not implemented)")
return
}
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}")
return
}
@ -270,7 +298,7 @@ def cmdSession(args) {
json += '}'
println("${YELLOW}Creating session...${RESET}")
def output = apiRequest('/sessions', 'POST', json, apiKey)
def output = apiRequest('/sessions', 'POST', json, publicKey, secretKey)
def idMatch = output =~ /"id":"([^"]+)"/
if (idMatch.find()) {
println("${GREEN}Session created: ${idMatch.group(1)}${RESET}")
@ -296,12 +324,28 @@ def openBrowser(url) {
}
def cmdKey(args) {
def apiKey = getApiKey(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
'-H', 'Content-Type: application/json',
'-H', "Authorization: Bearer ${apiKey}",
'-d', '{}']
'-H', 'Content-Type: application/json']
// 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 output = proc.text
@ -361,23 +405,23 @@ def cmdKey(args) {
}
def cmdService(args) {
def apiKey = getApiKey(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey)
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("No services (list parsing not implemented)")
return
}
if (args.serviceInfo) {
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, apiKey)
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, publicKey, secretKey)
println(output)
return
}
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":"((?:[^"\\]|\\.)*)"/
if (logsMatch.find()) {
println(logsMatch.group(1).replace('\\n', '\n'))
@ -386,7 +430,7 @@ def cmdService(args) {
}
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":"((?:[^"\\]|\\.)*)"/
if (logsMatch.find()) {
println(logsMatch.group(1).replace('\\n', '\n'))
@ -395,26 +439,26 @@ def cmdService(args) {
}
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}")
return
}
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}")
return
}
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}")
return
}
if (args.serviceExecute) {
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 stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/
@ -441,7 +485,7 @@ def cmdService(args) {
if (args.serviceDumpBootstrap) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
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":"((?:[^"\\\\]|\\\\.)*)"/
if (stdoutMatch.find()) {
@ -491,7 +535,7 @@ def cmdService(args) {
}
json += '}'
def output = apiRequest('/services', 'POST', json, apiKey)
def output = apiRequest('/services', 'POST', json, publicKey, secretKey)
def idMatch = output =~ /"id":"([^"]+)"/
if (idMatch.find()) {
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.Directory (createDirectoryIfMissing, setPermissions, getPermissions, setOwnerExecutable)
import Data.List (isPrefixOf, intercalate)
import Data.Char (isDigit)
import Data.Char (isDigit, ord)
import Text.Printf (printf)
import Control.Monad (when, unless, forM_)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Base64 as B64
import Crypto.Hash.SHA256 (hmac)
import Numeric (showHex)
import Data.Time.Clock.POSIX (getPOSIXTime)
-- API constants
apiBase :: String
@ -382,39 +386,77 @@ serviceCommand opts = do
-- HTTP helpers using curl
curlPost :: String -> String -> String -> IO (ExitCode, String, String)
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"
[ "-s", "-X", "POST"
, url
, "-H", "Content-Type: application/json"
, "-H", "Authorization: Bearer " ++ apiKey
, "-d", body
] ""
([ "-s", "-X", "POST"
, url
, "-H", "Content-Type: application/json"
] ++ authHeaders ++ ["-d", body]) ""
return (exitCode, stdout, stderr)
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"
[ "-s", url
, "-H", "Authorization: Bearer " ++ apiKey
] ""
([ "-s", url ] ++ authHeaders) ""
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"
[ "-s", "-X", "DELETE"
, url
, "-H", "Authorization: Bearer " ++ apiKey
] ""
([ "-s", "-X", "DELETE", url ] ++ authHeaders) ""
-- 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 = do
maybeKey <- lookupEnv "UNSANDBOX_API_KEY"
case maybeKey of
Nothing -> do
hPutStrLn stderr "Error: UNSANDBOX_API_KEY not set"
exitFailure
Just key -> return key
(publicKey, _) <- getApiKeys
return publicKey
-- HMAC-SHA256
hmacSha256 :: String -> String -> String
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
parseExitCode :: String -> Int
@ -565,11 +607,12 @@ extendKey apiKey = do
-- HTTP helper for portal API
curlPostPortal :: String -> String -> String -> IO (ExitCode, String, String)
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"
[ "-s", "-X", "POST"
, url
, "-H", "Content-Type: application/json"
, "-H", "Authorization: Bearer " ++ apiKey
, "-d", body
] ""
([ "-s", "-X", "POST"
, url
, "-H", "Content-Type: application/json"
] ++ authHeaders ++ ["-d", body]) ""
return (exitCode, stdout, stderr)

83
un.jl
View file

@ -42,6 +42,7 @@ using JSON
using Base64
using ArgParse
using Printf
using SHA
# Extension to language mapping
const EXT_MAP = Dict(
@ -75,19 +76,54 @@ function detect_language(filename::String)::String
return get(EXT_MAP, ext, "unknown")
end
function get_api_key(args_key=nothing)::String
key = something(args_key, get(ENV, "UNSANDBOX_API_KEY", ""))
if isempty(key)
println(stderr, "$(RED)Error: UNSANDBOX_API_KEY not set$(RESET)")
function get_api_keys(args_key=nothing)::Tuple{String,String}
# Try new-style keys first
public_key = something(args_key, get(ENV, "UNSANDBOX_PUBLIC_KEY", ""))
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)
end
return key
return (public_key, secret_key)
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
# 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 = [
"Authorization" => "Bearer $api_key",
"Authorization" => "Bearer $public_key",
"X-Timestamp" => string(timestamp),
"X-Signature" => signature,
"Content-Type" => "application/json"
]
@ -95,7 +131,6 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
if method == "GET"
response = HTTP.get(url, headers, readtimeout=300)
elseif method == "POST"
body = data !== nothing ? JSON.json(data) : ""
response = HTTP.post(url, headers, body, readtimeout=300)
elseif method == "DELETE"
response = HTTP.delete(url, headers, readtimeout=300)
@ -115,7 +150,7 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
end
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"]
if !isfile(filename)
@ -176,7 +211,7 @@ function cmd_execute(args)
end
# 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
if haskey(result, "stdout") && !isempty(result["stdout"])
@ -205,10 +240,10 @@ function cmd_execute(args)
end
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"]
result = api_request("/sessions", api_key)
result = api_request("/sessions", public_key, secret_key)
sessions = get(result, "sessions", [])
if isempty(sessions)
println("No active sessions")
@ -226,7 +261,7 @@ function cmd_session(args)
end
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)")
return
end
@ -236,10 +271,10 @@ function cmd_session(args)
end
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"]
result = api_request("/services", api_key)
result = api_request("/services", public_key, secret_key)
services = get(result, "services", [])
if isempty(services)
println("No services")
@ -259,31 +294,31 @@ function cmd_service(args)
end
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))
return
end
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", ""))
return
end
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)")
return
end
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)")
return
end
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)")
return
end
@ -291,7 +326,7 @@ function cmd_service(args)
if args["dump-bootstrap"] !== nothing
println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...")
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"])
bootstrap = result["stdout"]
@ -351,7 +386,7 @@ function cmd_service(args)
payload["vcpu"] = args["vcpu"]
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("Name: $(get(result, "name", "N/A"))")
if haskey(result, "url")
@ -452,7 +487,9 @@ function validate_key(api_key::String)
end
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
if args["extend"]

129
un.js
View file

@ -55,6 +55,7 @@ const fs = require('fs');
const https = require('https');
const path = require('path');
const { exec } = require('child_process');
const crypto = require('crypto');
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
@ -80,13 +81,25 @@ const EXT_MAP = {
".tcl": "tcl", ".raku": "raku", ".m": "objc",
};
function getApiKey(argsKey) {
const key = argsKey || process.env.UNSANDBOX_API_KEY;
if (!key) {
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
process.exit(1);
function getApiKeys(argsKey) {
// Try new split key format first
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
// 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) {
@ -111,32 +124,45 @@ function detectLanguage(filename) {
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) => {
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 = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 300000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(body));
resolve(JSON.parse(responseBody));
} catch (e) {
resolve(body);
resolve(responseBody);
}
} else {
console.error(`${RED}Error: HTTP ${res.statusCode} - ${body}${RESET}`);
console.error(`${RED}Error: HTTP ${res.statusCode} - ${responseBody}${RESET}`);
process.exit(1);
}
});
@ -148,42 +174,55 @@ function apiRequest(endpoint, method = "GET", data = null, apiKey = null) {
});
if (data) {
req.write(JSON.stringify(data));
req.write(body);
}
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) => {
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 = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 30000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(body));
resolve(JSON.parse(responseBody));
} catch (e) {
resolve(body);
resolve(responseBody);
}
} else {
try {
const errorBody = JSON.parse(body);
resolve({ error: errorBody.error || body, status: res.statusCode });
const errorBody = JSON.parse(responseBody);
resolve({ error: errorBody.error || responseBody, status: res.statusCode });
} 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) {
req.write(JSON.stringify(data));
req.write(body);
}
req.end();
});
@ -220,9 +259,9 @@ function openBrowser(url) {
});
}
async function validateKey(apiKey, shouldExtend = false) {
async function validateKey(publicKey, secretKey, shouldExtend = false) {
try {
const result = await portalRequest("/keys/validate", "POST", {}, apiKey);
const result = await portalRequest("/keys/validate", "POST", {}, publicKey, secretKey);
// Handle --extend flag first
if (shouldExtend) {
@ -265,12 +304,12 @@ async function validateKey(apiKey, shouldExtend = false) {
}
async function cmdKey(args) {
const apiKey = getApiKey(args.apiKey);
await validateKey(apiKey, args.extend);
const { publicKey, secretKey } = getApiKeys(args.apiKey);
await validateKey(publicKey, secretKey, args.extend);
}
async function cmdExecute(args) {
const apiKey = getApiKey(args.apiKey);
const { publicKey, secretKey } = getApiKeys(args.apiKey);
let code;
try {
@ -312,7 +351,7 @@ async function cmdExecute(args) {
if (args.network) payload.network = args.network;
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.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
@ -334,10 +373,10 @@ async function cmdExecute(args) {
}
async function cmdSession(args) {
const apiKey = getApiKey(args.apiKey);
const { publicKey, secretKey } = getApiKeys(args.apiKey);
if (args.list) {
const result = await apiRequest("/sessions", "GET", null, apiKey);
const result = await apiRequest("/sessions", "GET", null, publicKey, secretKey);
const sessions = result.sessions || [];
if (sessions.length === 0) {
console.log("No active sessions");
@ -351,7 +390,7 @@ async function cmdSession(args) {
}
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}`);
return;
}
@ -370,16 +409,16 @@ async function cmdSession(args) {
if (args.audit) payload.audit = true;
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(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
}
async function cmdService(args) {
const apiKey = getApiKey(args.apiKey);
const { publicKey, secretKey } = getApiKeys(args.apiKey);
if (args.list) {
const result = await apiRequest("/services", "GET", null, apiKey);
const result = await apiRequest("/services", "GET", null, publicKey, secretKey);
const services = result.services || [];
if (services.length === 0) {
console.log("No services");
@ -395,44 +434,44 @@ async function cmdService(args) {
}
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));
return;
}
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 || "");
return;
}
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 || "");
return;
}
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}`);
return;
}
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}`);
return;
}
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}`);
return;
}
if (args.execute) {
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.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
return;
@ -441,7 +480,7 @@ async function cmdService(args) {
if (args.dumpBootstrap) {
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
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) {
const bootstrap = result.stdout;
@ -481,7 +520,7 @@ async function cmdService(args) {
if (args.network) payload.network = args.network;
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(`Name: ${result.name || 'N/A'}`);
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.util.Base64
import kotlin.system.exitProcess
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
val API_BASE = "https://api.unsandbox.com"
val PORTAL_BASE = "https://unsandbox.com"
@ -123,7 +125,7 @@ fun main(args: Array<String>) {
}
fun cmdExecute(args: Args) {
val apiKey = getApiKey(args.apiKey)
val (publicKey, secretKey) = getApiKeys(args.apiKey)
val code = File(args.sourceFile!!).readText()
val language = detectLanguage(args.sourceFile!!)
@ -167,7 +169,7 @@ fun cmdExecute(args: Args) {
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 stderr = result["stderr"] as? String
@ -198,10 +200,10 @@ fun cmdExecute(args: Args) {
}
fun cmdSession(args: Args) {
val apiKey = getApiKey(args.apiKey)
val (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.sessionList) {
val result = apiRequest("/sessions", "GET", null, apiKey)
val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST")
val sessions = result["sessions"] as? List<Map<String, Any>>
if (sessions.isNullOrEmpty()) {
@ -221,7 +223,7 @@ fun cmdSession(args: Args) {
}
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}")
return
}
@ -237,16 +239,16 @@ fun cmdSession(args: Args) {
}
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("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}")
}
fun cmdService(args: Args) {
val apiKey = getApiKey(args.apiKey)
val (publicKey, secretKey) = getApiKeys(args.apiKey)
if (args.serviceList) {
val result = apiRequest("/services", "GET", null, apiKey)
val result = apiRequest("/services", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST")
val services = result["services"] as? List<Map<String, Any>>
if (services.isNullOrEmpty()) {
@ -270,44 +272,44 @@ fun cmdService(args: Args) {
}
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))
return
}
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"] ?: "")
return
}
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"] ?: "")
return
}
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}")
return
}
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}")
return
}
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}")
return
}
if (args.serviceExecute != null) {
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")) {
val stdout = result["stdout"] as? String
if (stdout != null && stdout.isNotEmpty()) {
@ -326,7 +328,7 @@ fun cmdService(args: Args) {
if (args.serviceDumpBootstrap != null) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
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
if (bootstrap != null && bootstrap.isNotEmpty()) {
@ -368,7 +370,7 @@ fun cmdService(args: Args) {
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("Name: ${result["name"] ?: "N/A"}")
if (result.containsKey("url")) {
@ -382,21 +384,21 @@ fun cmdService(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 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 expiresAt = result["expires_at"] as? String ?: ""
if (args.keyExtend) {
if (publicKey.isEmpty()) {
if (pubKey.isEmpty()) {
System.err.println("${RED}Error: Could not retrieve public key${RESET}")
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(extendUrl)
@ -418,13 +420,13 @@ fun cmdKey(args: Args) {
if (expired) {
println("${RED}Status: Expired${RESET}")
println("Public Key: $publicKey")
println("Public Key: $pubKey")
println("Tier: $tier")
println("Expired: $expiresAt")
println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}")
} else if (valid) {
println("${GREEN}Status: Valid${RESET}")
println("Public Key: $publicKey")
println("Public Key: $pubKey")
println("Tier: $tier")
println("Expires: $expiresAt")
} else {
@ -433,12 +435,21 @@ fun cmdKey(args: Args) {
}
}
fun validateKey(apiKey: String): Map<String, Any> {
val url = URL("$PORTAL_BASE/keys/validate")
fun validateKey(publicKey: String?, secretKey: String): Map<String, Any> {
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
connection.requestMethod = "POST"
connection.setRequestProperty("Authorization", "Bearer $apiKey")
connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000
connection.readTimeout = 30000
@ -452,13 +463,39 @@ fun validateKey(apiKey: String): Map<String, Any> {
return parseJson(response)
}
fun getApiKey(argsKey: String?): String {
val key = argsKey ?: System.getenv("UNSANDBOX_API_KEY")
if (key.isNullOrEmpty()) {
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}")
fun getApiKeys(argsKey: String?): Pair<String?, String> {
var publicKey: String? = null
var secretKey: String? = null
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)
}
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 {
@ -469,20 +506,26 @@ fun detectLanguage(filename: String): String {
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 connection = url.openConnection() as HttpURLConnection
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.connectTimeout = 30000
connection.readTimeout = 300000
if (data != null) {
connection.doOutput = true
val json = toJson(data)
connection.outputStream.use { it.write(json.toByteArray()) }
connection.outputStream.use { it.write(body.toByteArray()) }
}
if (connection.responseCode !in 200..299) {

77
un.lisp
View file

@ -104,38 +104,75 @@
(defun curl-post (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data)))
(unwind-protect
(run-curl (list "curl" "-s" "-X" "POST"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" "Content-Type: application/json"
"-H" (format nil "Authorization: Bearer ~a" api-key)
"-d" (format nil "@~a" tmp-file)))
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(delete-file tmp-file))))
(defun curl-get (api-key endpoint)
(run-curl (list "curl" "-s"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" (format nil "Authorization: Bearer ~a" api-key))))
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
(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)
(run-curl (list "curl" "-s" "-X" "DELETE"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" (format nil "Authorization: Bearer ~a" api-key))))
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
(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)
(let ((tmp-file (write-temp-file json-data)))
(unwind-protect
(run-curl (list "curl" "-s" "-X" "POST"
(format nil "~a~a" *portal-base* endpoint)
"-H" "Content-Type: application/json"
"-H" (format nil "Authorization: Bearer ~a" api-key)
"-d" (format nil "@~a" tmp-file)))
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "~a~a" *portal-base* endpoint)
"-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" 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 ()
(or (uiop:getenv "UNSANDBOX_API_KEY")
(progn
(format t "Error: UNSANDBOX_API_KEY not set~%")
(uiop:quit 1))))
(first (get-api-keys)))
(defun hmac-sha256 (secret message)
"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)
(let* ((api-key (get-api-key))

136
un.lua
View file

@ -76,13 +76,23 @@ local EXT_MAP = {
[".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc"
}
local function get_api_key(args_key)
local key = args_key or os.getenv("UNSANDBOX_API_KEY")
if not key then
io.stderr:write(RED .. "Error: UNSANDBOX_API_KEY not set" .. RESET .. "\n")
os.exit(1)
local function get_api_keys(args_key)
local public_key = os.getenv("UNSANDBOX_PUBLIC_KEY")
local secret_key = os.getenv("UNSANDBOX_SECRET_KEY")
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
return key
return {public_key = public_key, secret_key = secret_key}
end
local function detect_language(filename)
@ -115,20 +125,46 @@ local function shell_escape(str)
return "'" .. str:gsub("'", "'\\''") .. "'"
end
local function api_request(endpoint, method, data, api_key)
local function api_request(endpoint, method, data, keys)
method = method or "GET"
local url = API_BASE .. endpoint
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) ..
" -H 'Authorization: Bearer " .. api_key .. "'" ..
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'"
local data_file
if data then
local payload = json.encode(data)
local data_file = os.tmpname()
data_file = os.tmpname()
local f = io.open(data_file, "w")
f:write(payload)
f:write(body)
f:close()
cmd = cmd .. " -d @" .. shell_escape(data_file)
end
@ -144,7 +180,7 @@ local function api_request(endpoint, method, data, api_key)
file:close()
os.remove(tmpfile)
if data then
if data_file then
os.remove(data_file)
end
@ -198,7 +234,7 @@ local function base64_decode(data)
end
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 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.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
io.write(BLUE .. result.stdout .. RESET)
@ -261,10 +297,10 @@ local function cmd_execute(options)
end
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
local result = api_request("/sessions", "GET", nil, api_key)
local result = api_request("/sessions", "GET", nil, keys)
local sessions = result.sessions or {}
if #sessions == 0 then
print("No active sessions")
@ -280,7 +316,7 @@ local function cmd_session(options)
end
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)
return
end
@ -299,21 +335,39 @@ local function cmd_session(options)
if options.audit then payload.audit = true end
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(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET)
end
local function cmd_key(options)
local api_key = get_api_key(options.api_key)
local keys = get_api_keys(options.api_key)
if options.extend then
-- Get public_key from validation response
local url = PORTAL_BASE .. "/keys/validate"
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) ..
" -H 'Authorization: Bearer " .. api_key .. "'" ..
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'" ..
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
@ -351,8 +405,26 @@ local function cmd_key(options)
local url = PORTAL_BASE .. "/keys/validate"
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) ..
" -H 'Authorization: Bearer " .. api_key .. "'" ..
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
" -H 'X-Timestamp: " .. timestamp .. "'" ..
" -H 'X-Signature: " .. signature .. "'" ..
" -H 'Content-Type: application/json'" ..
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
@ -390,10 +462,10 @@ local function cmd_key(options)
end
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
local result = api_request("/services", "GET", nil, api_key)
local result = api_request("/services", "GET", nil, keys)
local services = result.services or {}
if #services == 0 then
print("No services")
@ -411,44 +483,44 @@ local function cmd_service(options)
end
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))
return
end
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 "")
return
end
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 "")
return
end
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)
return
end
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)
return
end
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)
return
end
if options.execute then
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.stderr then io.stderr:write(RED .. result.stderr .. RESET) end
return
@ -457,7 +529,7 @@ local function cmd_service(options)
if options.dump_bootstrap then
io.stderr:write("Fetching bootstrap script from " .. options.dump_bootstrap .. "...\n")
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
local bootstrap = result.stdout
@ -514,7 +586,7 @@ local function cmd_service(options)
if options.network then payload.network = options.network 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("Name: " .. (result.name or "N/A"))
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
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonHMAC.h>
static NSString* API_BASE = @"https://api.unsandbox.com";
static NSString* PORTAL_BASE = @"https://unsandbox.com";
@ -69,13 +70,47 @@ NSDictionary* getExtMap() {
};
}
NSString* getApiKey() {
NSString* key = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
if (!key) {
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", [RED UTF8String], [RESET UTF8String]);
void getApiKeys(NSString** publicKey, NSString** secretKey) {
// Try new-style keys first
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
*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);
}
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) {
@ -92,15 +127,15 @@ NSString* detectLanguage(NSString* filename) {
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];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setValue:[@"Bearer " stringByAppendingString:apiKey] forHTTPHeaderField:@"Authorization"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setTimeoutInterval:300];
// Prepare body
NSString* bodyString = @"";
if (data) {
NSError* error = nil;
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]);
exit(1);
}
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[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;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
@ -139,7 +185,8 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
}
void cmdExecute(NSArray* args) {
NSString* apiKey = getApiKey();
NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
NSString* sourceFile = nil;
NSMutableDictionary* envVars = [NSMutableDictionary dictionary];
NSMutableArray* inputFiles = [NSMutableArray array];
@ -233,7 +280,7 @@ void cmdExecute(NSArray* args) {
}
// Execute
NSDictionary* result = apiRequest(@"/execute", @"POST", payload, apiKey);
NSDictionary* result = apiRequest(@"/execute", @"POST", payload, publicKey, secretKey);
// Print output
NSString* stdoutText = result[@"stdout"] ?: @"";
@ -361,7 +408,8 @@ void validateKey(NSString* apiKey, BOOL shouldExtend) {
}
void cmdKey(NSArray* args) {
NSString* apiKey = getApiKey();
NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL shouldExtend = NO;
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) {
NSString* apiKey = getApiKey();
NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* killId = nil;
NSString* shell = nil;
@ -398,7 +448,7 @@ void cmdSession(NSArray* args) {
}
if (listMode) {
NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, apiKey);
NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, publicKey, secretKey);
NSArray* sessions = result[@"sessions"];
if ([sessions count] == 0) {
printf("No active sessions\n");
@ -417,7 +467,7 @@ void cmdSession(NSArray* args) {
if (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]);
return;
}
@ -430,14 +480,15 @@ void cmdSession(NSArray* args) {
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
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("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n",
[YELLOW UTF8String], [RESET UTF8String]);
}
void cmdService(NSArray* args) {
NSString* apiKey = getApiKey();
NSString* publicKey, *secretKey;
getApiKeys(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* infoId = nil;
NSString* logsId = nil;
@ -488,7 +539,7 @@ void cmdService(NSArray* args) {
}
if (listMode) {
NSDictionary* result = apiRequest(@"/services", @"GET", nil, apiKey);
NSDictionary* result = apiRequest(@"/services", @"GET", nil, publicKey, secretKey);
NSArray* services = result[@"services"];
if ([services count] == 0) {
printf("No services\n");
@ -512,7 +563,7 @@ void cmdService(NSArray* args) {
if (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];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
@ -521,28 +572,28 @@ void cmdService(NSArray* args) {
if (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]);
return;
}
if (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]);
return;
}
if (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]);
return;
}
if (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]);
return;
}
@ -551,7 +602,7 @@ void cmdService(NSArray* args) {
fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]);
NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"};
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) {
NSString* bootstrap = result[@"stdout"];
@ -610,7 +661,7 @@ void cmdService(NSArray* args) {
if (network) payload[@"network"] = network;
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("Name: %s\n", [result[@"name"] UTF8String]);
if (result[@"url"]) {

91
un.ml
View file

@ -114,11 +114,22 @@ let escape_json s =
(* Execute curl command *)
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'"
endpoint api_key json in
let (public_key, secret_key) = get_api_keys () 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 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
Sys.remove tmp_file;
output
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
output_string oc json;
close_out oc;
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s"
portal_base endpoint api_key tmp_file in
let (public_key, secret_key) = get_api_keys () 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 rec read_all acc =
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
let curl_get api_key endpoint =
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s -H 'Authorization: Bearer %s'"
endpoint api_key in
let (public_key, secret_key) = get_api_keys () 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 rec read_all acc =
try
@ -153,17 +168,18 @@ let curl_get api_key endpoint =
output
let curl_delete api_key endpoint =
let cmd = Printf.sprintf "curl -s -X DELETE https://api.unsandbox.com%s -H 'Authorization: Bearer %s'"
endpoint api_key in
let (public_key, secret_key) = get_api_keys () 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 output = read_all "" where
let rec read_all acc =
try
let line = input_line ic in
read_all (acc ^ line ^ "\n")
with End_of_file -> acc
in read_all ""
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
output
@ -214,13 +230,46 @@ let unescape_json s =
let s = Str.global_replace (Str.regexp "\\\\\\\\") "\\" s in
s
(* Get API key *)
let get_api_key () =
try Sys.getenv "UNSANDBOX_API_KEY"
with Not_found ->
Printf.fprintf stderr "Error: UNSANDBOX_API_KEY not set\n";
(* Get API keys *)
let get_api_keys () =
let public_key = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
let secret_key = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
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
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 *)
let execute_command file env_vars artifacts out_dir network vcpu =
let api_key = get_api_key () in

124
un.nim
View file

@ -43,7 +43,7 @@
# un.nim session --list
# un.nim service --name web --ports 8080
import os, strutils, osproc, strformat
import os, strutils, osproc, strformat, times
const
API_BASE = "https://api.unsandbox.com"
@ -76,10 +76,29 @@ proc escapeJson(s: string): string =
of '\t': result.add("\\t")
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 =
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)
if lang == "":
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}""")
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)
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:
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)
return
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)
echo GREEN & "Session terminated: " & kill & RESET
return
@ -125,51 +148,67 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
json.add("}")
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)
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:
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)
return
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)
return
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))
return
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))
return
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)
echo GREEN & "Service sleeping: " & sleep & RESET
return
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)
echo GREEN & "Service waking: " & wake & RESET
return
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)
echo GREEN & "Service destroyed: " & destroy & RESET
return
if execute != "":
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)
# Simple parsing for stdout/stderr
@ -202,7 +241,10 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
if 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 stdoutStart = result.find("\"stdout\":\"")
@ -252,15 +294,17 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
json.add("}")
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)
return
stderr.writeLine(RED & "Error: Specify --name to create a service" & RESET)
quit(1)
proc cmdKey(extend: bool, apiKey: string) =
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' -H 'Authorization: Bearer {apiKey}'"""
proc cmdKey(extend: bool, publicKey: string, secretKey: string) =
let authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey)
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' {authHeaders}"""
let response = execCurl(cmd)
# Parse JSON response manually (simple approach)
@ -271,10 +315,10 @@ proc cmdKey(extend: bool, apiKey: string) =
let pkStart = response.find("\"public_key\":\"") + 14
let pkEnd = response.find("\"", pkStart)
if pkEnd > pkStart:
let publicKey = response[pkStart..<pkEnd]
echo "Public Key: " & publicKey
let pubKey = response[pkStart..<pkEnd]
echo "Public Key: " & pubKey
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
discard execProcess(fmt"xdg-open '{extendUrl}'")
@ -296,13 +340,13 @@ proc cmdKey(extend: bool, apiKey: string) =
echo RED & "Expired" & RESET
# Extract and display key info
var publicKey = ""
var pubKey = ""
if response.contains("\"public_key\":"):
let pkStart = response.find("\"public_key\":\"") + 14
let pkEnd = response.find("\"", pkStart)
if pkEnd > pkStart:
publicKey = response[pkStart..<pkEnd]
echo "Public Key: " & publicKey
pubKey = response[pkStart..<pkEnd]
echo "Public Key: " & pubKey
# Extract tier
if response.contains("\"tier\":"):
@ -321,8 +365,8 @@ proc cmdKey(extend: bool, apiKey: string) =
echo ""
echo YELLOW & "To renew: Visit " & PORTAL_BASE & "/keys/extend" & RESET
if extend and publicKey != "":
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}"
if extend and pubKey != "":
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={pubKey}"
echo BLUE & "Opening browser to extend key..." & RESET
discard execProcess(fmt"xdg-open '{extendUrl}'")
@ -335,7 +379,13 @@ proc cmdKey(extend: bool, apiKey: string) =
echo "Error: " & response[errStart..<errEnd]
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()
if args.len < 1:
@ -351,9 +401,9 @@ proc main() =
while i < args.len:
case args[i]
of "--extend": extend = true
of "-k": apiKey = args[i+1]; inc i
of "-k": publicKey = args[i+1]; inc i
inc i
cmdKey(extend, apiKey)
cmdKey(extend, publicKey, secretKey)
return
if args[0] == "session":
@ -371,9 +421,9 @@ proc main() =
of "-v": vcpu = parseInt(args[i+1]); inc i
of "--tmux": tmux = true
of "--screen": screen = true
of "-k": apiKey = args[i+1]; inc i
of "-k": publicKey = args[i+1]; inc i
inc i
cmdSession(list, kill, shell, network, vcpu, tmux, screen, apiKey)
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey)
return
if args[0] == "service":
@ -401,9 +451,9 @@ proc main() =
of "--dump-file": dumpFile = args[i+1]; inc i
of "-n": network = 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
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
# Execute mode
@ -418,7 +468,7 @@ proc main() =
of "-a": artifacts = true
of "-n": network = 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:
if not args[i].startsWith("-"):
sourceFile = args[i]
@ -428,7 +478,7 @@ proc main() =
stderr.writeLine(RED & "Error: No source file specified" & RESET)
quit(1)
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey)
cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey)
when isMainModule:
main()

103
un.php
View file

@ -76,13 +76,23 @@ const EXT_MAP = [
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
];
function get_api_key($args_key = null) {
$key = $args_key ?: getenv('UNSANDBOX_API_KEY');
if (!$key) {
fwrite(STDERR, RED . "Error: UNSANDBOX_API_KEY not set" . RESET . "\n");
exit(1);
function get_api_keys($args_key = null) {
$public_key = getenv('UNSANDBOX_PUBLIC_KEY');
$secret_key = getenv('UNSANDBOX_SECRET_KEY');
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) {
@ -109,12 +119,23 @@ function detect_language($filename) {
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;
$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 = [
'Authorization: Bearer ' . $api_key,
'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json'
];
@ -126,7 +147,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
]);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
@ -149,7 +170,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
}
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'])) {
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['vcpu']) $payload['vcpu'] = $options['vcpu'];
$result = api_request('/execute', 'POST', $payload, $api_key);
$result = api_request('/execute', 'POST', $payload, $keys);
if (!empty($result['stdout'])) {
echo BLUE . $result['stdout'] . RESET;
@ -221,10 +242,10 @@ function cmd_execute($options) {
}
function cmd_session($options) {
$api_key = get_api_key($options['api_key']);
$keys = get_api_keys($options['api_key']);
if ($options['list']) {
$result = api_request('/sessions', 'GET', null, $api_key);
$result = api_request('/sessions', 'GET', null, $keys);
$sessions = $result['sessions'] ?? [];
if (empty($sessions)) {
echo "No active sessions\n";
@ -240,7 +261,7 @@ function cmd_session($options) {
}
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";
return;
}
@ -259,17 +280,27 @@ function cmd_session($options) {
if ($options['audit']) $payload['audit'] = true;
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 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';
$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 = [
'Authorization: Bearer ' . $api_key,
'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json'
];
@ -322,15 +353,25 @@ function validate_key($api_key) {
}
function cmd_key($options) {
$api_key = get_api_key($options['api_key']);
$keys = get_api_keys($options['api_key']);
if ($options['extend']) {
// First validate to get public_key
$url = PORTAL_BASE . '/keys/validate';
$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 = [
'Authorization: Bearer ' . $api_key,
'Authorization: Bearer ' . $keys['public_key'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json'
];
@ -367,15 +408,15 @@ function cmd_key($options) {
echo "$extend_url\n";
}
} else {
validate_key($api_key);
validate_key($keys);
}
}
function cmd_service($options) {
$api_key = get_api_key($options['api_key']);
$keys = get_api_keys($options['api_key']);
if ($options['list']) {
$result = api_request('/services', 'GET', null, $api_key);
$result = api_request('/services', 'GET', null, $keys);
$services = $result['services'] ?? [];
if (empty($services)) {
echo "No services\n";
@ -393,44 +434,44 @@ function cmd_service($options) {
}
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";
return;
}
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'] ?? '';
return;
}
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'] ?? '';
return;
}
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";
return;
}
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";
return;
}
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";
return;
}
if ($options['execute']) {
$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['stderr'])) fwrite(STDERR, RED . $result['stderr'] . RESET);
return;
@ -439,7 +480,7 @@ function cmd_service($options) {
if ($options['dump_bootstrap']) {
fwrite(STDERR, "Fetching bootstrap script from {$options['dump_bootstrap']}...\n");
$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'])) {
$bootstrap = $result['stdout'];
@ -483,7 +524,7 @@ function cmd_service($options) {
if ($options['network']) $payload['network'] = $options['network'];
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 "Name: " . ($result['name'] ?? 'N/A') . "\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 MIME::Base64;
use File::Path qw(make_path);
use Digest::SHA qw(hmac_sha256_hex);
my $API_BASE = 'https://api.unsandbox.com';
my $PORTAL_BASE = 'https://unsandbox.com';
@ -84,12 +85,20 @@ my %EXT_MAP = (
sub get_api_key {
my ($args_key) = @_;
my $key = $args_key || $ENV{'UNSANDBOX_API_KEY'};
unless ($key) {
print STDERR "${RED}Error: UNSANDBOX_API_KEY not set${RESET}\n";
my $public_key = $ENV{'UNSANDBOX_PUBLIC_KEY'} || '';
my $secret_key = $ENV{'UNSANDBOX_SECRET_KEY'} || '';
# 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;
}
return $key;
return ($public_key, $secret_key);
}
sub detect_language {
@ -117,17 +126,28 @@ sub detect_language {
}
sub api_request {
my ($endpoint, $method, $data, $api_key) = @_;
my ($endpoint, $method, $data, $public_key, $secret_key) = @_;
$method //= 'GET';
my $url = "$API_BASE$endpoint";
my $ua = LWP::UserAgent->new(timeout => 300);
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');
my $body = '';
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);
@ -142,7 +162,7 @@ sub api_request {
sub cmd_execute {
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}) {
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->{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 STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
@ -215,10 +235,10 @@ sub cmd_execute {
sub cmd_session {
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}) {
my $result = api_request('/sessions', 'GET', undef, $api_key);
my $result = api_request('/sessions', 'GET', undef, $public_key, $secret_key);
my $sessions = $result->{sessions} || [];
if (@$sessions == 0) {
print "No active sessions\n";
@ -234,7 +254,7 @@ sub cmd_session {
}
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";
return;
}
@ -253,17 +273,17 @@ sub cmd_session {
$payload->{audit} = JSON::PP::true if $options->{audit};
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 "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
}
sub cmd_service {
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}) {
my $result = api_request('/services', 'GET', undef, $api_key);
my $result = api_request('/services', 'GET', undef, $public_key, $secret_key);
my $services = $result->{services} || [];
if (@$services == 0) {
print "No services\n";
@ -281,45 +301,45 @@ sub cmd_service {
}
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 "\n";
return;
}
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} // '';
return;
}
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} // '';
return;
}
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";
return;
}
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";
return;
}
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";
return;
}
if ($options->{execute}) {
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 STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
return;
@ -328,7 +348,7 @@ sub cmd_service {
if ($options->{dump_bootstrap}) {
print STDERR "Fetching bootstrap script from $options->{dump_bootstrap}...\n";
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}) {
my $bootstrap = $result->{stdout};
@ -379,7 +399,7 @@ sub cmd_service {
$payload->{network} = $options->{network} if $options->{network};
$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 "Name: ", ($result->{name} // 'N/A'), "\n";
print "URL: $result->{url}\n" if $result->{url};
@ -405,15 +425,24 @@ sub open_browser {
}
sub validate_key {
my ($api_key, $should_extend) = @_;
my ($public_key, $secret_key, $should_extend) = @_;
# Call /keys/validate endpoint
my $url = "$PORTAL_BASE/keys/validate";
my $ua = LWP::UserAgent->new(timeout => 30);
my $request = HTTP::Request->new('POST' => $url);
$request->header('Authorization' => "Bearer $api_key");
$request->header('Authorization' => "Bearer $public_key");
$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 $result = decode_json($response->content);
@ -455,8 +484,8 @@ sub validate_key {
sub cmd_key {
my ($options) = @_;
my $api_key = get_api_key($options->{api_key});
validate_key($api_key, $options->{extend});
my ($public_key, $secret_key) = get_api_key($options->{api_key});
validate_key($public_key, $secret_key, $options->{extend});
}
sub main {

125
un.pro
View file

@ -79,15 +79,32 @@ read_file_content(Filename, Content) :-
read_string(Stream, _, Content),
close(Stream).
% Get API key from environment
get_api_key(ApiKey) :-
( getenv('UNSANDBOX_API_KEY', ApiKey),
ApiKey \= ''
% Get API keys from environment (HMAC or legacy)
get_public_key(PublicKey) :-
( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
PublicKey \= ''
-> 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)
).
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_file(Filename) :-
% Check file exists
@ -105,97 +122,108 @@ execute_file(Filename) :-
halt(1)
),
% Get API key
get_api_key(ApiKey),
% Get API keys
get_public_key(PublicKey),
get_secret_key(SecretKey),
% Build and execute curl command
% Build and execute curl command with HMAC
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',
[ApiKey, Language, Filename]),
'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',
[Language, Filename, SecretKey, PublicKey]),
shell(Cmd, 0).
% Session list
session_list :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[ApiKey]),
'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"',
[SecretKey, PublicKey]),
shell(Cmd, 0).
% Session kill
session_kill(SessionId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[SessionId, ApiKey, SessionId]),
'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, SecretKey, SessionId, PublicKey, SessionId]),
shell(Cmd, 0).
% Service list
service_list :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[ApiKey]),
'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"',
[SecretKey, PublicKey]),
shell(Cmd, 0).
% Service info
service_info(ServiceId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" | jq .',
[ServiceId, ApiKey]),
'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, SecretKey, ServiceId, PublicKey]),
shell(Cmd, 0).
% Service logs
service_logs(ServiceId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd),
'curl -s -X GET https://api.unsandbox.com/services/~w/logs -H "Authorization: Bearer ~w" | jq -r ".logs"',
[ServiceId, ApiKey]),
'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, SecretKey, ServiceId, PublicKey]),
shell(Cmd, 0).
% Service sleep
service_sleep(ServiceId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[ServiceId, ApiKey, ServiceId]),
'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, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0).
% Service wake
service_wake(ServiceId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[ServiceId, ApiKey, ServiceId]),
'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, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0).
% Service destroy
service_destroy(ServiceId) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
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"',
[ServiceId, ApiKey, ServiceId]),
'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, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0).
% Service dump bootstrap
service_dump_bootstrap(ServiceId, DumpFile) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
( DumpFile = ''
-> % No file specified, print to stdout
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',
[ServiceId, ServiceId, ApiKey])
'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, SecretKey, ServiceId, PublicKey])
; % File specified, save to file
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',
[ServiceId, ServiceId, ApiKey, DumpFile, DumpFile, DumpFile])
'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, SecretKey, ServiceId, PublicKey, DumpFile, DumpFile, DumpFile])
),
shell(Cmd, 0).
% Service create
service_create(Name, Ports, Bootstrap, ServiceType) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
% Build JSON payload
( Ports \= ''
-> format(atom(PortsJson), ',"ports":[~w]', [Ports])
@ -210,25 +238,26 @@ service_create(Name, Ports, Bootstrap, ServiceType) :-
; ServiceTypeJson = ''
),
format(atom(Json), '{"name":"~w"~w~w~w}', [Name, PortsJson, BootstrapJson, ServiceTypeJson]),
% Execute curl command
% Execute curl command with HMAC
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"',
[ApiKey, Json]),
'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"',
[Json, SecretKey, PublicKey]),
shell(Cmd, 0).
% Key validate
validate_key(Extend) :-
get_api_key(ApiKey),
get_public_key(PublicKey),
get_secret_key(SecretKey),
portal_base(PortalBase),
( Extend = true
-> % Build command for --extend mode
format(atom(Cmd),
'RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
[PortalBase, ApiKey, PortalBase])
'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',
[SecretKey, PortalBase, PublicKey, PortalBase])
; % Build command for normal validation
format(atom(Cmd),
'curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
[PortalBase, ApiKey])
'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',
[SecretKey, PortalBase, PublicKey])
),
shell(Cmd, 0).

37
un.ps1
View file

@ -59,24 +59,47 @@ $EXT_MAP = @{
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
}
function Get-ApiKey {
$key = $env:UNSANDBOX_API_KEY
if (-not $key) {
Write-Error "Error: UNSANDBOX_API_KEY not set"
function Get-ApiKeys {
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
$secretKey = $env:UNSANDBOX_SECRET_KEY
# 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
}
return $key
return @($publicKey, $secretKey)
}
function Invoke-Api {
param($Endpoint, $Method = "GET", $Body = $null, $BaseUrl = $null)
$apiKey = Get-ApiKey
$publicKey, $secretKey = Get-ApiKeys
$headers = @{
"Authorization" = "Bearer $apiKey"
"Authorization" = "Bearer $publicKey"
"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 }
$uri = "$base$Endpoint"

129
un.py
View file

@ -59,6 +59,9 @@ import argparse
import urllib.request
import urllib.error
import webbrowser
import hmac
import hashlib
import time
API_BASE = "https://api.unsandbox.com"
PORTAL_BASE = "https://unsandbox.com"
@ -86,16 +89,28 @@ EXT_MAP = {
}
def get_api_key(args_key=None):
"""Get API key from args or environment"""
key = args_key or os.environ.get("UNSANDBOX_API_KEY")
if not key:
print(f"{RED}Error: UNSANDBOX_API_KEY not set{RESET}", file=sys.stderr)
sys.exit(1)
return key
def get_api_keys(args_key=None):
"""Get API keys from args or environment. Returns (public_key, secret_key)."""
# Try new split key format first
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_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"""
ext = os.path.splitext(filename)[1].lower()
lang = EXT_MAP.get(ext)
@ -114,22 +129,52 @@ def detect_language(filename):
if 'php' in first_line: return 'php'
except:
pass
print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr)
sys.exit(1)
if exit_on_error:
print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr)
sys.exit(1)
return None
return lang
def api_request(endpoint, method="GET", data=None, api_key=None):
"""Make API request and return response"""
def read_file(filepath):
"""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}"
# 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 = {
"Authorization": f"Bearer {api_key}",
"Authorization": f"Bearer {public_key}",
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json"
}
req = urllib.request.Request(url, method=method, headers=headers)
if data:
req.data = json.dumps(data).encode('utf-8')
req.data = body.encode('utf-8')
try:
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):
"""Execute source code"""
api_key = get_api_key(args.api_key)
public_key, secret_key = get_api_keys(args.api_key)
# Read source file
try:
@ -199,7 +244,7 @@ def cmd_execute(args):
payload["vcpu"] = args.vcpu
# 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
if result.get("stdout"):
@ -225,10 +270,10 @@ def cmd_execute(args):
def cmd_session(args):
"""Manage interactive sessions"""
api_key = get_api_key(args.api_key)
public_key, secret_key = get_api_keys(args.api_key)
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", [])
if not sessions:
print("No active sessions")
@ -239,7 +284,7 @@ def cmd_session(args):
return
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}")
return
@ -264,16 +309,30 @@ def cmd_session(args):
payload["audit"] = True
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"{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"""
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 = {
"Authorization": f"Bearer {api_key}",
"Authorization": f"Bearer {public_key}",
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json"
}
@ -332,16 +391,16 @@ def validate_key(api_key, extend=False):
def cmd_key(args):
"""Validate API key"""
api_key = get_api_key(args.key)
validate_key(api_key, extend=args.extend)
public_key, secret_key = get_api_keys(args.key)
validate_key(public_key, secret_key, extend=args.extend)
def cmd_service(args):
"""Manage persistent services"""
api_key = get_api_key(args.api_key)
public_key, secret_key = get_api_keys(args.api_key)
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", [])
if not services:
print("No services")
@ -354,38 +413,38 @@ def cmd_service(args):
return
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))
return
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", ""))
return
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", ""))
return
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}")
return
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}")
return
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}")
return
if args.execute:
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"):
print(f"{BLUE}{result['stdout']}{RESET}", end='')
if result.get("stderr"):
@ -395,7 +454,7 @@ def cmd_service(args):
if args.dump_bootstrap:
print(f"Fetching bootstrap script from {args.dump_bootstrap}...", file=sys.stderr)
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"):
bootstrap = result["stdout"]
@ -438,7 +497,7 @@ def cmd_service(args):
if 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"Name: {result.get('name', 'N/A')}")
if result.get('url'):

112
un.r
View file

@ -38,6 +38,7 @@
library(httr)
library(jsonlite)
library(digest)
# Extension to language mapping
ext_map <- list(
@ -75,28 +76,53 @@ detect_language <- function(filename) {
return(lang)
}
get_api_key <- function(args_key = NULL) {
key <- if (!is.null(args_key)) args_key else Sys.getenv("UNSANDBOX_API_KEY")
if (key == "") {
cat(sprintf("%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET), file = stderr())
get_api_keys <- function(args_key = NULL) {
public_key <- Sys.getenv("UNSANDBOX_PUBLIC_KEY")
secret_key <- Sys.getenv("UNSANDBOX_SECRET_KEY")
# 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)
}
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)
headers <- add_headers(
`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({
if (method == "GET") {
response <- GET(url, headers, timeout(300))
} else if (method == "POST") {
body <- if (!is.null(data)) toJSON(data, auto_unbox = TRUE) else ""
response <- POST(url, headers, body = body, encode = "raw", timeout(300))
response <- POST(url, headers, body = body_content, encode = "raw", timeout(300))
} else if (method == "DELETE") {
response <- DELETE(url, headers, timeout(300))
} else {
@ -112,7 +138,9 @@ api_request <- function(endpoint, api_key, method = "GET", data = NULL) {
}
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
if (!file.exists(filename)) {
@ -175,7 +203,7 @@ cmd_execute <- function(args) {
}
# 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
if (!is.null(result$stdout) && result$stdout != "") {
@ -204,10 +232,12 @@ cmd_execute <- 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) {
result <- api_request("/sessions", api_key)
result <- api_request("/sessions", public_key, secret_key)
sessions <- if (!is.null(result$sessions)) result$sessions else list()
if (length(sessions) == 0) {
cat("No active sessions\n")
@ -225,7 +255,7 @@ cmd_session <- function(args) {
}
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))
return()
}
@ -235,16 +265,31 @@ cmd_session <- 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) {
# First validate to get public_key
url <- paste0(PORTAL_BASE, "/keys/validate")
headers <- add_headers(
`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({
response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
@ -268,9 +313,22 @@ cmd_key <- function(args) {
url <- paste0(PORTAL_BASE, "/keys/validate")
headers <- add_headers(
`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({
response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
@ -302,10 +360,12 @@ cmd_key <- 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) {
result <- api_request("/services", api_key)
result <- api_request("/services", public_key, secret_key)
services <- if (!is.null(result$services)) result$services else list()
if (length(services) == 0) {
cat("No services\n")
@ -325,31 +385,31 @@ cmd_service <- function(args) {
}
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")
return()
}
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")
return()
}
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))
return()
}
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))
return()
}
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))
return()
}
@ -357,7 +417,7 @@ cmd_service <- function(args) {
if (!is.null(args$dump_bootstrap)) {
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
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 != "") {
bootstrap <- result$stdout
@ -415,7 +475,7 @@ cmd_service <- function(args) {
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("Name: %s\n", if (!is.null(result$name)) result$name else "N/A"))
if (!is.null(result$url)) {

80
un.raku
View file

@ -40,6 +40,7 @@
# Full-featured CLI matching un.c/un.py capabilities
use JSON::Fast;
use Digest::SHA;
constant $API_BASE = "https://api.unsandbox.com";
constant $PORTAL_BASE = "https://unsandbox.com";
@ -66,13 +67,21 @@ my %EXT_MAP = (
m => 'objc'
);
sub get-api-key() {
my $key = %*ENV<UNSANDBOX_API_KEY>;
unless $key {
note "{$RED}Error: UNSANDBOX_API_KEY not set{$RESET}";
sub get-api-keys() {
my $public-key = %*ENV<UNSANDBOX_PUBLIC_KEY> // '';
my $secret-key = %*ENV<UNSANDBOX_SECRET_KEY> // '';
# 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;
}
return $key;
return ($public-key, $secret-key);
}
sub detect-language(Str $filename --> Str) {
@ -98,9 +107,10 @@ sub detect-language(Str $filename --> Str) {
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 @args = 'curl', '-s';
my $body = '';
if $method eq '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: '-H', 'Content-Type: application/json';
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;
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) {
my $api-key = get-api-key();
my ($public-key, $secret-key) = get-api-keys();
my $source-file = '';
my %env-vars;
my @input-files;
@ -218,7 +239,7 @@ sub cmd-execute(@args) {
%payload<vcpu> = $vcpu if $vcpu > 0;
# Execute
my %result = api-request('/execute', 'POST', %payload, :$api-key);
my %result = api-request('/execute', 'POST', %payload, :$public-key, :$secret-key);
# Print output
if %result<stdout> {
@ -245,7 +266,7 @@ sub cmd-execute(@args) {
}
sub cmd-session(@args) {
my $api-key = get-api-key();
my ($public-key, $secret-key) = get-api-keys();
my $list-mode = False;
my $kill-id = '';
my $shell = '';
@ -280,7 +301,7 @@ sub cmd-session(@args) {
}
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;
unless @sessions {
say "No active sessions";
@ -295,7 +316,7 @@ sub cmd-session(@args) {
}
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}";
return;
}
@ -306,13 +327,13 @@ sub cmd-session(@args) {
%payload<vcpu> = $vcpu if $vcpu > 0;
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 "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}";
}
sub cmd-service(@args) {
my $api-key = get-api-key();
my ($public-key, $secret-key) = get-api-keys();
my $list-mode = False;
my $info-id = '';
my $logs-id = '';
@ -392,7 +413,7 @@ sub cmd-service(@args) {
}
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;
unless @services {
say "No services";
@ -409,31 +430,31 @@ sub cmd-service(@args) {
}
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);
return;
}
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>;
return;
}
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}";
return;
}
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}";
return;
}
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}";
return;
}
@ -441,7 +462,7 @@ sub cmd-service(@args) {
if $dump-bootstrap-id {
note "Fetching bootstrap script from $dump-bootstrap-id...";
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 '' {
my $bootstrap = %result<stdout>;
@ -485,7 +506,7 @@ sub cmd-service(@args) {
%payload<network> = $network if $network;
%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 "Name: {%result<name>}";
say "URL: {%result<url>}" if %result<url>;
@ -497,13 +518,22 @@ sub cmd-service(@args) {
}
sub validate-key(Bool $extend) {
my $api-key = get-api-key();
my ($public-key, $secret-key) = get-api-keys();
# Build curl command
my @args = 'curl', '-s', '-X', 'POST';
@args.append: "$PORTAL_BASE/keys/validate";
@args.append: '-H', 'Content-Type: application/json';
@args.append: '-H', "Authorization: Bearer $api-key";
@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 $body = $proc.out.slurp;

90
un.rb
View file

@ -80,13 +80,23 @@ EXT_MAP = {
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
}.freeze
def get_api_key(args_key = nil)
key = args_key || ENV['UNSANDBOX_API_KEY']
unless key
warn "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
exit 1
def get_api_keys(args_key = nil)
public_key = ENV['UNSANDBOX_PUBLIC_KEY']
secret_key = ENV['UNSANDBOX_SECRET_KEY']
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
key
{ public_key: public_key, secret_key: secret_key }
end
def detect_language(filename)
@ -112,12 +122,19 @@ def detect_language(filename)
lang
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}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
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
when 'GET' then Net::HTTP::Get.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}"
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.body = JSON.generate(data) if data
request.body = body if data
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
@ -142,7 +161,7 @@ rescue => e
end
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])
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[: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']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
@ -203,10 +222,10 @@ def cmd_execute(options)
end
def cmd_session(options)
api_key = get_api_key(options[:api_key])
keys = get_api_keys(options[:api_key])
if options[:list]
result = api_request('/sessions', api_key: api_key)
result = api_request('/sessions', keys: keys)
sessions = result['sessions'] || []
if sessions.empty?
puts 'No active sessions'
@ -222,7 +241,7 @@ def cmd_session(options)
end
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}"
return
end
@ -241,19 +260,28 @@ def cmd_session(options)
payload[:audit] = true if options[:audit]
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 "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
end
def validate_key(api_key)
def validate_key(keys)
require 'openssl'
uri = URI("#{PORTAL_BASE}/keys/validate")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
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['Authorization'] = "Bearer #{api_key}"
request['Authorization'] = "Bearer #{keys[:public_key]}"
request['X-Timestamp'] = timestamp
request['X-Signature'] = signature
request['Content-Type'] = 'application/json'
response = http.request(request)
@ -308,10 +336,10 @@ def open_browser(url)
end
def cmd_key(options)
api_key = get_api_key(options[:api_key])
keys = get_api_keys(options[:api_key])
if options[:extend]
result = validate_key(api_key)
result = validate_key(keys)
public_key = result['public_key']
if public_key
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
@ -322,15 +350,15 @@ def cmd_key(options)
exit 1
end
else
validate_key(api_key)
validate_key(keys)
end
end
def cmd_service(options)
api_key = get_api_key(options[:api_key])
keys = get_api_keys(options[:api_key])
if options[:list]
result = api_request('/services', api_key: api_key)
result = api_request('/services', keys: keys)
services = result['services'] || []
if services.empty?
puts 'No services'
@ -348,44 +376,44 @@ def cmd_service(options)
end
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)
return
end
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'] || ''
return
end
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'] || ''
return
end
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}"
return
end
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}"
return
end
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}"
return
end
if options[:execute]
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']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
return
@ -394,7 +422,7 @@ def cmd_service(options)
if options[:dump_bootstrap]
warn "Fetching bootstrap script from #{options[:dump_bootstrap]}..."
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']
bootstrap = result['stdout']
@ -434,7 +462,7 @@ def cmd_service(options)
payload[:network] = options[:network] if options[:network]
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 "Name: #{result['name'] || 'N/A'}"
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::process::{self, Command};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
const API_BASE: &str = "https://api.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 {
if let Some(k) = key_arg {
return k.to_string();
fn get_api_keys(key_arg: Option<&str>) -> (String, String) {
let public_key = env::var("UNSANDBOX_PUBLIC_KEY").ok();
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);
process::exit(1);
})
(public_key.unwrap(), secret_key.unwrap())
}
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 {
@ -163,8 +188,18 @@ fn extract_json_int(json: &str, key: &str) -> i32 {
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 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");
cmd.arg("-s")
.arg("-X")
@ -173,7 +208,11 @@ fn api_request(endpoint: &str, method: &str, body: Option<&str>, api_key: &str)
.arg("-H")
.arg("Content-Type: application/json")
.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 {
cmd.arg("-d").arg(b);
@ -200,7 +239,8 @@ fn cmd_execute(
output_dir: Option<&str>,
network: Option<&str>,
vcpu: Option<i32>,
api_key: &str,
public_key: &str,
secret_key: &str,
) {
let code = fs::read_to_string(source_file).unwrap_or_else(|e| {
eprintln!("{}Error reading file: {}{}", RED, e, RESET);
@ -265,7 +305,7 @@ fn cmd_execute(
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
let stdout_str = extract_json_string(&result, "stdout");
@ -295,16 +335,17 @@ fn cmd_session(
vcpu: Option<i32>,
tmux: bool,
screen: bool,
api_key: &str,
public_key: &str,
secret_key: &str,
) {
if list {
let result = api_request("/sessions", "GET", None, api_key);
let result = api_request("/sessions", "GET", None, public_key, secret_key);
println!("{}", result);
return;
}
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);
return;
}
@ -330,7 +371,7 @@ fn cmd_session(
json.push('}');
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");
println!("{}Session created: {}{}", GREEN, id, RESET);
}
@ -354,46 +395,47 @@ fn cmd_service(
dump_file: Option<&str>,
network: Option<&str>,
vcpu: Option<i32>,
api_key: &str,
public_key: &str,
secret_key: &str,
) {
if list {
let result = api_request("/services", "GET", None, api_key);
let result = api_request("/services", "GET", None, public_key, secret_key);
println!("{}", result);
return;
}
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);
return;
}
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"));
return;
}
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"));
return;
}
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);
return;
}
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);
return;
}
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);
return;
}
@ -401,7 +443,7 @@ fn cmd_service(
if let Some(id) = execute {
let cmd = command.unwrap_or("");
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 stderr_str = extract_json_string(&result, "stderr");
if !stdout_str.is_empty() {
@ -416,7 +458,7 @@ fn cmd_service(
if let Some(id) = dump_bootstrap {
eprintln!("Fetching bootstrap script from {}...", id);
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");
if !bootstrap.is_empty() {
@ -495,7 +537,7 @@ fn cmd_service(
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");
println!("{}Service created: {}{}", GREEN, id, RESET);
return;
@ -505,8 +547,8 @@ fn cmd_service(
process::exit(1);
}
fn cmd_key(extend: bool, api_key: &str) {
let result = api_request("/keys/validate", "POST", Some("{}"), api_key);
fn cmd_key(extend: bool, public_key: &str, secret_key: &str) {
let result = api_request("/keys/validate", "POST", Some("{}"), public_key, secret_key);
let status = extract_json_string(&result, "status");
let public_key = extract_json_string(&result, "public_key");
@ -647,7 +689,7 @@ fn main() {
}
}
"session" => {
let key = get_api_key(api_key.as_deref());
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_session(
args.contains(&"--list".to_string()),
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,
args.contains(&"--tmux".to_string()),
args.contains(&"--screen".to_string()),
&key,
&public_key,
&secret_key,
);
return;
}
"service" => {
let key = get_api_key(api_key.as_deref());
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
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 == "--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()),
network.as_deref(),
vcpu,
&key,
&public_key,
&secret_key,
);
return;
}
"key" => {
let key = get_api_key(api_key.as_deref());
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
cmd_key(
args.contains(&"--extend".to_string()),
&key,
&public_key,
&secret_key,
);
return;
}
@ -704,7 +749,7 @@ fn main() {
// Execute mode
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(
&file,
envs,
@ -713,7 +758,8 @@ fn main() {
output_dir.as_deref(),
network.as_deref(),
vcpu,
&key,
&public_key,
&secret_key,
);
} else {
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)
(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"
endpoint api-key tmp-file))
(keys (get-api-keys))
(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))
(output (let loop ((lines '()))
(let ((line (read-line port)))
@ -114,8 +120,12 @@
output))
(define (curl-get api-key endpoint)
(let* ((cmd (format #f "curl -s https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'"
endpoint api-key))
(let* ((keys (get-api-keys))
(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))
(output (let loop ((lines '()))
(let ((line (read-line port)))
@ -126,8 +136,12 @@
output))
(define (curl-delete api-key endpoint)
(let* ((cmd (format #f "curl -s -X DELETE https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'"
endpoint api-key))
(let* ((keys (get-api-keys))
(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))
(output (let loop ((lines '()))
(let ((line (read-line port)))
@ -139,8 +153,14 @@
(define (curl-post-portal api-key endpoint json-data)
(let* ((tmp-file (write-temp-file json-data))
(cmd (format #f "curl -s -X POST ~a~a -H 'Content-Type: application/json' -H 'Authorization: Bearer ~a' -d @~a"
portal-base endpoint api-key tmp-file))
(keys (get-api-keys))
(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))
(output (let loop ((lines '()))
(let ((line (read-line port)))
@ -151,11 +171,42 @@
(delete-file tmp-file)
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)
(or (getenv "UNSANDBOX_API_KEY")
(begin
(display "Error: UNSANDBOX_API_KEY not set\n" (current-error-port))
(exit 1))))
(car (get-api-keys)))
(define (hmac-sha256 secret message)
"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)
"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 method="${2:-GET}"
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
echo -e "${RED}Error: UNSANDBOX_API_KEY not set${RESET}" >&2
# Fallback to old UNSANDBOX_API_KEY for backwards compat
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
fi
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
local response
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $api_key" \
-H "Content-Type: application/json" \
-d "$data" 2>&1)
if [[ -n "$signature" ]]; then
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $public_key" \
-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
local response
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $api_key" \
-H "Content-Type: application/json" 2>&1)
if [[ -n "$signature" ]]; then
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
-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
local http_code=$(echo "$response" | tail -n1)
@ -172,7 +205,7 @@ cmd_execute() {
local output_dir="."
local network=""
local vcpu=""
local api_key="${UNSANDBOX_API_KEY}"
local api_key="${UNSANDBOX_API_KEY:-}"
# Parse arguments
while [[ $# -gt 0 ]]; do
@ -300,7 +333,7 @@ cmd_session() {
local screen=false
local network=""
local vcpu=""
local api_key="${UNSANDBOX_API_KEY}"
local api_key="${UNSANDBOX_API_KEY:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
@ -409,7 +442,7 @@ cmd_service() {
local command=""
local network=""
local vcpu=""
local api_key="${UNSANDBOX_API_KEY}"
local api_key="${UNSANDBOX_API_KEY:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
@ -743,7 +776,7 @@ validate_key() {
}
cmd_key() {
local api_key="${UNSANDBOX_API_KEY}"
local api_key="${UNSANDBOX_API_KEY:-}"
local extend=false
# 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
#
# This is free public domain software for the public good of a permacomputer hosted
@ -34,8 +35,6 @@
# https://www.foxhop.net
# https://www.unturf.com/software
#!/usr/bin/env tclsh
# unsandbox CLI - TCL implementation
# Full-featured CLI matching un.c/un.py capabilities
@ -43,6 +42,7 @@ package require http
package require json
package require tls
package require base64
package require sha256
# Register https support
::http::register https 443 ::tls::socket
@ -72,12 +72,28 @@ array set EXT_MAP {
.tcl tcl .raku raku .m objc
}
proc get_api_key {} {
if {[info exists ::env(UNSANDBOX_API_KEY)]} {
return $::env(UNSANDBOX_API_KEY)
proc get_api_keys {} {
set public_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}"
exit 1
if {[info exists ::env(UNSANDBOX_SECRET_KEY)]} {
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} {
@ -103,16 +119,29 @@ proc detect_language {filename} {
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 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"} {
set token [::http::geturl $url -headers $headers -timeout 300000]
} elseif {$method eq "DELETE"} {
set token [::http::geturl $url -method DELETE -headers $headers -timeout 300000]
} else {
set json_data [::json::write object {*}$data]
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} {
set api_key [get_api_key]
lassign [get_api_keys] public_key secret_key
set source_file ""
set env_vars [dict create]
set input_files [list]
@ -236,7 +265,7 @@ proc cmd_execute {args} {
}
# Execute
set result [api_request "/execute" "POST" $payload $api_key]
set result [api_request "/execute" "POST" $payload $public_key $secret_key]
# Print output
if {[dict exists $result stdout]} {
@ -275,7 +304,7 @@ proc cmd_execute {args} {
}
proc cmd_session {args} {
set api_key [get_api_key]
lassign [get_api_keys] public_key secret_key
set list_mode 0
set kill_id ""
set shell ""
@ -309,7 +338,7 @@ proc cmd_session {args} {
}
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]
if {[llength $sessions] == 0} {
puts "No active sessions"
@ -327,7 +356,7 @@ proc cmd_session {args} {
}
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}"
return
}
@ -347,13 +376,13 @@ proc cmd_session {args} {
}
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 "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}"
}
proc cmd_key {args} {
set api_key [get_api_key]
lassign [get_api_keys] public_key secret_key
set extend_mode 0
# Parse arguments
@ -368,7 +397,16 @@ proc cmd_key {args} {
# POST to /keys/validate with Bearer auth
set url "${::PORTAL_BASE}/keys/validate"
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"]
set 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 status [::http::status $token]
@ -438,7 +476,7 @@ proc cmd_key {args} {
}
proc cmd_service {args} {
set api_key [get_api_key]
lassign [get_api_keys] public_key secret_key
set list_mode 0
set info_id ""
set logs_id ""
@ -517,7 +555,7 @@ proc cmd_service {args} {
}
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]
if {[llength $services] == 0} {
puts "No services"
@ -538,31 +576,31 @@ proc cmd_service {args} {
}
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]]
return
}
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]
return
}
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}"
return
}
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}"
return
}
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}"
return
}
@ -570,7 +608,7 @@ proc cmd_service {args} {
if {$dump_bootstrap_id ne ""} {
puts stderr "Fetching bootstrap script from $dump_bootstrap_id..."
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 ""} {
set bootstrap [dict get $result stdout]
@ -628,7 +666,7 @@ proc cmd_service {args} {
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 "Name: [dict get $result name]"
if {[dict exists $result url]} {

96
un.ts
View file

@ -54,6 +54,7 @@
import * as fs from 'fs';
import * as https from 'https';
import * as path from 'path';
import * as crypto from 'crypto';
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
@ -112,13 +113,28 @@ interface Args {
extend: boolean;
}
function getApiKey(argsKey: string | null): string {
const key = argsKey || process.env.UNSANDBOX_API_KEY;
if (!key) {
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
process.exit(1);
interface ApiKeys {
publicKey: string;
secretKey: string;
}
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 {
@ -143,15 +159,22 @@ function detectLanguage(filename: string): string {
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) => {
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 = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 300000
@ -180,21 +203,28 @@ function apiRequest(endpoint: string, method: string = "GET", data: any = null,
});
if (data) {
req.write(JSON.stringify(data));
req.write(body);
}
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) => {
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 = {
hostname: url.hostname,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Authorization': `Bearer ${keys.publicKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
},
timeout: 30000
@ -218,14 +248,14 @@ function portalRequest(endpoint: string, method: string = "GET", data: any = nul
});
if (data) {
req.write(JSON.stringify(data));
req.write(body);
}
req.end();
});
}
async function cmdExecute(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey);
const keys = getApiKeys(args.apiKey);
let code: string;
try {
@ -267,7 +297,7 @@ async function cmdExecute(args: Args): Promise<void> {
if (args.network) payload.network = args.network;
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.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> {
const apiKey = getApiKey(args.apiKey);
const keys = getApiKeys(args.apiKey);
if (args.list) {
const result = await apiRequest("/sessions", "GET", null, apiKey);
const result = await apiRequest("/sessions", "GET", null, keys);
const sessions = result.sessions || [];
if (sessions.length === 0) {
console.log("No active sessions");
@ -306,7 +336,7 @@ async function cmdSession(args: Args): Promise<void> {
}
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}`);
return;
}
@ -325,16 +355,16 @@ async function cmdSession(args: Args): Promise<void> {
if (args.audit) payload.audit = true;
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(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
}
async function cmdService(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey);
const keys = getApiKeys(args.apiKey);
if (args.list) {
const result = await apiRequest("/services", "GET", null, apiKey);
const result = await apiRequest("/services", "GET", null, keys);
const services = result.services || [];
if (services.length === 0) {
console.log("No services");
@ -350,44 +380,44 @@ async function cmdService(args: Args): Promise<void> {
}
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));
return;
}
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 || "");
return;
}
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 || "");
return;
}
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}`);
return;
}
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}`);
return;
}
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}`);
return;
}
if (args.execute) {
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.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
return;
@ -396,7 +426,7 @@ async function cmdService(args: Args): Promise<void> {
if (args.dumpBootstrap) {
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
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) {
const bootstrap = result.stdout;
@ -436,7 +466,7 @@ async function cmdService(args: Args): Promise<void> {
if (args.network) payload.network = args.network;
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(`Name: ${result.name || 'N/A'}`);
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 {
const result = await portalRequest("/keys/validate", "POST", {}, apiKey);
const result = await portalRequest("/keys/validate", "POST", {}, keys);
// Handle --extend flag first
if (shouldExtend) {
@ -513,8 +543,8 @@ async function validateKey(apiKey: string, shouldExtend: boolean): Promise<void>
}
async function cmdKey(args: Args): Promise<void> {
const apiKey = getApiKey(args.apiKey);
await validateKey(apiKey, args.extend);
const keys = getApiKeys(args.apiKey);
await validateKey(keys, args.extend);
}
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) {
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)
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 += '}'
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))
}
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 {
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))
return
}
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)
println('${green}Session terminated: ${kill}${reset}')
return
@ -264,51 +272,54 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
json += '}'
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))
}
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 {
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))
return
}
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))
return
}
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))
return
}
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))
return
}
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)
println('${green}Service sleeping: ${sleep}${reset}')
return
}
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)
println('${green}Service waking: ${wake}${reset}')
return
}
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)
println('${green}Service destroyed: ${destroy}${reset}')
return
@ -316,7 +327,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
if execute != '' {
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)
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 != '' {
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)
bootstrap_script := extract_json_string(result, 'stdout')
@ -379,7 +391,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
json += '}'
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))
return
}
@ -388,8 +400,33 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
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() {
mut api_key := os.getenv('UNSANDBOX_API_KEY')
mut api_key := get_public_key()
if os.args.len < 2 {
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 process = std.process;
const mem = std.mem;
const time = std.time;
const API_BASE = "https://api.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 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
@ -70,10 +117,16 @@ pub fn main() !u8 {
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, "");
};
defer allocator.free(api_key);
defer allocator.free(secret_key);
// Handle session command
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) {
i += 1;
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) {
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);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} 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);
_ = std.c.system(cmd.ptr);
std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k});
} else {
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);
std.debug.print("\x1b[33mCreating session...\x1b[0m\n", .{});
_ = 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) {
i += 1;
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) {
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);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} 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);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (execute) |exec_id| {
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);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (dump_bootstrap) |bootstrap_id| {
std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id});
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);
_ = std.c.system(cmd.ptr);
@ -231,7 +319,9 @@ pub fn main() !u8 {
try writer.writeAll("}");
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);
std.debug.print("\x1b[33mCreating service...\x1b[0m\n", .{});
_ = std.c.system(cmd.ptr);
@ -247,13 +337,19 @@ pub fn main() !u8 {
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--extend")) {
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) {
// First validate to get the public_key
const json_file = "/tmp/unsandbox_key_validate.json";
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
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);
_ = 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")
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| {
const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
public_key = json_content[value_start..end_idx];
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 });
defer allocator.free(url);
std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{});
@ -290,7 +386,9 @@ pub fn main() !u8 {
} else {
// Regular validation
const json_file = "/tmp/unsandbox_key_validate.json";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
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);
_ = std.c.system(cmd.ptr);
@ -319,7 +417,7 @@ pub fn main() !u8 {
}
// Extract other fields
var public_key: ?[]const u8 = null;
var pub_key: ?[]const u8 = null;
var tier: ?[]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| {
const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
public_key = json_content[value_start..end_idx];
pub_key = json_content[value_start..end_idx];
}
}
@ -351,12 +449,12 @@ pub fn main() !u8 {
if (status) |s| {
if (mem.eql(u8, s, "valid")) {
std.debug.print("\x1b[32mValid\x1b[0m\n", .{});
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp});
} else if (mem.eql(u8, s, "expired")) {
std.debug.print("\x1b[31mExpired\x1b[0m\n", .{});
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expired: {s}\n", .{exp});
std.debug.print("\x1b[33mTo renew: Visit {s}/keys/extend\x1b[0m\n", .{PORTAL_BASE});
@ -430,8 +528,14 @@ pub fn main() !u8 {
}
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
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);
const result = std.c.system(cmd.ptr);

View file

@ -63,13 +63,28 @@ const EXT_MAP: Record<string, string> = {
m: "objc",
};
function getApiKey(): string {
const key = Deno.env.get("UNSANDBOX_API_KEY");
if (!key) {
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
Deno.exit(1);
interface ApiKeys {
publicKey: string;
secretKey: string;
}
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 {
@ -92,13 +107,40 @@ async function apiRequest(
endpoint: string,
method: string,
data?: unknown,
apiKey?: string,
keys?: ApiKeys,
baseUrl?: string,
): Promise<any> {
const base = baseUrl || API_BASE;
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> = {
Authorization: `Bearer ${apiKey}`,
Authorization: `Bearer ${keys!.publicKey}`,
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json",
};
@ -108,7 +150,7 @@ async function apiRequest(
};
if (data && method !== "GET") {
options.body = JSON.stringify(data);
options.body = body;
}
const response = await fetch(url, options);
@ -124,7 +166,7 @@ async function apiRequest(
}
async function cmdExecute(args: string[]) {
const apiKey = getApiKey();
const keys = getApiKeys();
let sourceFile = "";
const envVars: Record<string, string> = {};
const inputFiles: string[] = [];
@ -228,7 +270,7 @@ async function cmdExecute(args: string[]) {
}
// Execute
const result = await apiRequest("/execute", "POST", payload, apiKey);
const result = await apiRequest("/execute", "POST", payload, keys);
// Print output
if (result.stdout) {
@ -255,7 +297,7 @@ async function cmdExecute(args: string[]) {
}
async function cmdSession(args: string[]) {
const apiKey = getApiKey();
const keys = getApiKeys();
let listMode = false;
let killId = "";
let shell = "";
@ -293,7 +335,7 @@ async function cmdSession(args: string[]) {
}
if (listMode) {
const result = await apiRequest("/sessions", "GET", undefined, apiKey);
const result = await apiRequest("/sessions", "GET", undefined, keys);
const sessions = result.sessions || [];
if (sessions.length === 0) {
console.log("No active sessions");
@ -311,7 +353,7 @@ async function cmdSession(args: string[]) {
}
if (killId) {
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, apiKey);
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Session terminated: ${killId}${RESET}`);
return;
}
@ -324,7 +366,7 @@ async function cmdSession(args: string[]) {
if (vcpu > 0) payload.vcpu = vcpu;
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(
`${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[]) {
const apiKey = getApiKey();
const keys = getApiKeys();
let extend = false;
// Parse arguments
@ -343,7 +385,7 @@ async function cmdKey(args: string[]) {
}
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
if (extend) {
@ -395,7 +437,7 @@ async function cmdKey(args: string[]) {
}
async function cmdService(args: string[]) {
const apiKey = getApiKey();
const keys = getApiKeys();
let listMode = false;
let infoId = "";
let logsId = "";
@ -475,7 +517,7 @@ async function cmdService(args: string[]) {
}
if (listMode) {
const result = await apiRequest("/services", "GET", undefined, apiKey);
const result = await apiRequest("/services", "GET", undefined, keys);
const services = result.services || [];
if (services.length === 0) {
console.log("No services");
@ -495,31 +537,31 @@ async function cmdService(args: string[]) {
}
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));
return;
}
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 || "");
return;
}
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}`);
return;
}
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}`);
return;
}
if (destroyId) {
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, apiKey);
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, keys);
console.log(`${GREEN}Service destroyed: ${destroyId}${RESET}`);
return;
}
@ -553,7 +595,7 @@ async function cmdService(args: string[]) {
if (network) payload.network = network;
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(`Name: ${result.name}`);
if (result.url) {

View file

@ -48,6 +48,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#define API_BASE "https://api.unsandbox.com"
#define PORTAL_BASE "https://unsandbox.com"
@ -56,6 +57,7 @@
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define RESET "\033[0m"
#define MAX_CMD_LEN 16384
const char* detect_language(const char *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);
if (!language) {
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);
// 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),
"curl -s -X POST '%s/execute' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' "
"%s "
"-d @- << 'EOF'\n%s\nEOF",
API_BASE, api_key, json_body);
API_BASE, auth_headers, json_body);
FILE *curl = popen(cmd, "r");
if (!curl) {
@ -262,22 +314,27 @@ void cmd_execute(const char *source_file, char **envs, int env_count, char **fil
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) {
char cmd[4096];
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[8192];
char auth_headers[1024];
if (list) {
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/sessions", "", public_key, secret_key);
snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/sessions' -H 'Authorization: Bearer %s'",
API_BASE, api_key);
"curl -s -X GET '%s/sessions' %s",
API_BASE, auth_headers);
system(cmd);
printf("\n");
return;
}
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),
"curl -s -X DELETE '%s/sessions/%s' -H 'Authorization: Bearer %s'",
API_BASE, kill, api_key);
"curl -s -X DELETE '%s/sessions/%s' %s",
API_BASE, kill, auth_headers);
system(cmd);
printf("%sSession terminated: %s%s\n", GREEN, kill, RESET);
return;
@ -305,22 +362,25 @@ void cmd_session(int list, const char *kill, const char *shell, const char *netw
strcat(json, "}");
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),
"curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' -d '%s'",
API_BASE, api_key, json);
"%s -d '%s'",
API_BASE, auth_headers, json);
system(cmd);
printf("\n%sSession created%s\n", GREEN, RESET);
}
void cmd_key(int extend, const char *api_key) {
char cmd[4096];
void cmd_key(int extend, const char *public_key, const char *secret_key) {
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),
"curl -s -X POST '%s/keys/validate' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s'",
PORTAL_BASE, api_key);
"%s",
PORTAL_BASE, auth_headers);
FILE *curl = popen(cmd, "r");
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) {
char cmd[8192];
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[16384];
char auth_headers[1024];
if (list) {
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/services", "", public_key, secret_key);
snprintf(cmd, sizeof(cmd),
"curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'",
API_BASE, api_key);
"curl -s -X GET '%s/services' %s",
API_BASE, auth_headers);
system(cmd);
printf("\n");
return;
}
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),
"curl -s -X GET '%s/services/%s' -H 'Authorization: Bearer %s'",
API_BASE, info, api_key);
"curl -s -X GET '%s/services/%s' %s",
API_BASE, info, auth_headers);
system(cmd);
printf("\n");
return;
}
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),
"curl -s -X GET '%s/services/%s/logs' -H 'Authorization: Bearer %s'",
API_BASE, logs, api_key);
"curl -s -X GET '%s/services/%s/logs' %s",
API_BASE, logs, auth_headers);
system(cmd);
return;
}
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),
"curl -s -X GET '%s/services/%s/logs?lines=9000' -H 'Authorization: Bearer %s'",
API_BASE, tail, api_key);
"curl -s -X GET '%s/services/%s/logs?lines=9000' %s",
API_BASE, tail, auth_headers);
system(cmd);
return;
}
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),
"curl -s -X POST '%s/services/%s/sleep' -H 'Authorization: Bearer %s'",
API_BASE, sleep_svc, api_key);
"curl -s -X POST '%s/services/%s/sleep' %s",
API_BASE, sleep_svc, auth_headers);
system(cmd);
printf("%sService sleeping: %s%s\n", GREEN, sleep_svc, RESET);
return;
}
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),
"curl -s -X POST '%s/services/%s/wake' -H 'Authorization: Bearer %s'",
API_BASE, wake, api_key);
"curl -s -X POST '%s/services/%s/wake' %s",
API_BASE, wake, auth_headers);
system(cmd);
printf("%sService waking: %s%s\n", GREEN, wake, RESET);
return;
}
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),
"curl -s -X DELETE '%s/services/%s' -H 'Authorization: Bearer %s'",
API_BASE, destroy, api_key);
"curl -s -X DELETE '%s/services/%s' %s",
API_BASE, destroy, auth_headers);
system(cmd);
printf("%sService destroyed: %s%s\n", GREEN, destroy, RESET);
return;
@ -551,10 +631,11 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
strcat(json, "}");
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),
"curl -s -X POST '%s/services' -H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' -d '%s'",
API_BASE, api_key, json);
"%s -d '%s'",
API_BASE, auth_headers, json);
system(cmd);
printf("\n%sService created%s\n", GREEN, RESET);
return;
@ -565,10 +646,15 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
}
int main(int argc, char *argv[]) {
const char *api_key = getenv("UNSANDBOX_API_KEY");
if (!api_key && argc > 1 && strcmp(argv[1], "session") != 0 && strcmp(argv[1], "service") != 0) {
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET);
return 1;
const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
// 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) {
@ -584,9 +670,9 @@ int main(int argc, char *argv[]) {
int extend = 0;
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "--extend") == 0) extend = 1;
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i];
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;
}
@ -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], "--tmux") == 0) tmux = 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;
}
@ -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], "-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], "-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;
}
@ -677,7 +763,7 @@ 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], "-k") == 0 && i + 1 < argc) {
api_key = argv[++i];
public_key = argv[++i];
} else if (!source_file && argv[i][0] != '-') {
source_file = argv[i];
}
@ -688,6 +774,6 @@ int main(int argc, char *argv[]) {
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;
}