chore: bump version to 4.2.13

This commit is contained in:
russell@unturf.com 2026-01-23 08:57:51 -05:00
parent 8434928878
commit f7ad5d4d25
13 changed files with 291 additions and 125 deletions

View file

@ -1 +1 @@
4.2.12
4.2.13

View file

@ -6013,7 +6013,7 @@ void print_usage(const char *prog) {
* ============================================================================ */
const char *unsandbox_version(void) {
return "4.2.12";
return "4.2.13";
}
const char *unsandbox_detect_language(const char *filename) {

View file

@ -72,7 +72,7 @@
* </ol>
*
* @author Permacomputer Project
* @version 4.2.12
* @version 4.2.13
*/
import javax.crypto.Mac

View file

@ -1,6 +1,6 @@
{
"name": "un-async",
"version": "4.2.12",
"version": "4.2.13",
"description": "Unsandbox async JavaScript SDK - Execute code in 50+ languages",
"main": "src/un_async.js",
"type": "module",

View file

@ -0,0 +1,13 @@
{
"name": "un-sync",
"version": "4.2.13",
"description": "unsandbox.com JavaScript SDK (Isomorphic - Node.js + Browser)",
"type": "module",
"main": "src/un.js",
"scripts": {
"test": "echo 'No tests configured yet'"
},
"keywords": ["unsandbox", "code-execution", "sandbox", "api"],
"author": "unsandbox.com",
"license": "Unlicense"
}

View file

@ -2,9 +2,10 @@
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*
* unsandbox.com JavaScript SDK (Synchronous/Async)
* Isomorphic: Works in Node.js (CLI + SDK) and Browser environments
*
* Library Usage:
* const {
* Library Usage (ES Modules):
* import {
* // Code execution
* executeCode, executeAsync, getJob, waitForJob, cancelJob, listJobs,
* getLanguages, detectLanguage,
@ -26,18 +27,19 @@
* transferImage, spawnFromImage, cloneImage,
* // Key validation
* validateKeys,
* } = require('./un.js');
* } from './un.js';
*
* // Execute code asynchronously (returns Promise)
* const result = await executeCode('python', 'print("hello")', publicKey, secretKey);
* const jobId = await executeAsync('javascript', 'console.log("hello")', publicKey, secretKey);
* const result = await waitForJob(jobId, publicKey, secretKey);
*
* Authentication Priority (4-tier):
* Authentication Priority (5-tier):
* 1. Function arguments (publicKey, secretKey)
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
* 3. ~/.unsandbox/accounts.csv (if in Node.js)
* 4. ./accounts.csv (if in Node.js)
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) [Node.js]
* 3. localStorage (unsandboxPublicKey, unsandboxSecretKey) [Browser]
* 4. ~/.unsandbox/accounts.csv [Node.js]
* 5. ./accounts.csv [Node.js]
*
* Request Authentication (HMAC-SHA256):
* Authorization: Bearer <publicKey>
@ -48,12 +50,32 @@
* - Cached in ~/.unsandbox/languages.json (Node.js only)
* - TTL: 1 hour
* - Updated on successful API calls
*
* Browser Usage:
* - Import as ES module: <script type="module">
* - Configure credentials via localStorage:
* localStorage.setItem('useUnsandbox', 'true');
* localStorage.setItem('unsandboxPublicKey', 'unsb-pk-...');
* localStorage.setItem('unsandboxSecretKey', 'unsb-sk-...');
* - Or pass credentials directly to functions
* - Uses Web Crypto API for HMAC-SHA256 signing
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const https = require('https');
// Environment detection for isomorphic support (Node.js + Browser)
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const IS_NODE = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
// Conditional imports for Node.js (null in browser)
let crypto = null;
let fs = null;
let path = null;
if (IS_NODE) {
// Dynamic imports for Node.js
crypto = await import('crypto').then(m => m.default || m);
fs = await import('fs').then(m => m.default || m);
path = await import('path').then(m => m.default || m);
}
const API_BASE = 'https://api.unsandbox.com';
const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
@ -67,9 +89,10 @@ class CredentialsError extends Error {
}
/**
* Get ~/.unsandbox directory path, creating if necessary.
* Get ~/.unsandbox directory path, creating if necessary. [Node.js only]
*/
function getUnsandboxDir() {
if (!IS_NODE) return null;
const home = process.env.HOME || process.env.USERPROFILE;
if (!home) {
throw new Error('Could not determine home directory');
@ -82,9 +105,10 @@ function getUnsandboxDir() {
}
/**
* Load credentials from CSV file (public_key,secret_key per line).
* Load credentials from CSV file (public_key,secret_key per line). [Node.js only]
*/
function loadCredentialsFromCsv(csvPath, accountIndex = 0) {
if (!IS_NODE || !fs) return null;
if (!fs.existsSync(csvPath)) {
return null;
}
@ -113,13 +137,37 @@ function loadCredentialsFromCsv(csvPath, accountIndex = 0) {
}
/**
* Resolve credentials from 4-tier priority system.
* Load credentials from localStorage. [Browser only]
*/
function loadCredentialsFromStorage() {
if (!IS_BROWSER) return null;
try {
const useUnsandbox = localStorage.getItem('useUnsandbox') === 'true';
if (!useUnsandbox) return null;
const publicKey = localStorage.getItem('unsandboxPublicKey');
const secretKey = localStorage.getItem('unsandboxSecretKey');
if (publicKey && secretKey) {
return [publicKey, secretKey];
}
} catch (e) {
// localStorage not available
}
return null;
}
/**
* Resolve credentials from 5-tier priority system.
*
* Priority:
* 1. Function arguments
* 2. Environment variables
* 3. ~/.unsandbox/accounts.csv
* 4. ./accounts.csv
* 2. Environment variables (Node.js)
* 3. localStorage (Browser)
* 4. ~/.unsandbox/accounts.csv (Node.js)
* 5. ./accounts.csv (Node.js)
*/
function resolveCredentials(publicKey, secretKey, accountIndex) {
// Tier 1: Function arguments
@ -127,143 +175,165 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
return [publicKey, secretKey];
}
// Tier 2: Environment variables
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
const envSk = process.env.UNSANDBOX_SECRET_KEY;
if (envPk && envSk) {
return [envPk, envSk];
// Tier 2: Environment variables (Node.js only)
if (IS_NODE) {
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
const envSk = process.env.UNSANDBOX_SECRET_KEY;
if (envPk && envSk) {
return [envPk, envSk];
}
}
// Determine account index
if (accountIndex === undefined) {
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
// Tier 3: localStorage (Browser only)
if (IS_BROWSER) {
const storageCreds = loadCredentialsFromStorage();
if (storageCreds) {
return storageCreds;
}
}
// Tier 3: ~/.unsandbox/accounts.csv
try {
const unsandboxDir = getUnsandboxDir();
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
// Tier 4 & 5: File-based credentials (Node.js only)
if (IS_NODE && fs && path) {
// Determine account index
if (accountIndex === undefined) {
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
}
// Tier 4: ~/.unsandbox/accounts.csv
try {
const unsandboxDir = getUnsandboxDir();
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
if (creds) {
return creds;
}
} catch (e) {
// Continue to next tier
}
// Tier 5: ./accounts.csv
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
if (creds) {
return creds;
}
} catch (e) {
// Continue to next tier
}
// Tier 4: ./accounts.csv
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
if (creds) {
return creds;
}
const envHint = IS_NODE
? ' 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n' +
' 3. ~/.unsandbox/accounts.csv\n' +
' 4. ./accounts.csv'
: ' 2. localStorage (enable "Use unsandbox.com API keys" in settings)';
throw new CredentialsError(
'No credentials found. Please provide via:\n' +
' 1. Function arguments (publicKey, secretKey)\n' +
' 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n' +
' 3. ~/.unsandbox/accounts.csv\n' +
' 4. ./accounts.csv'
envHint
);
}
/**
* Sign a request using HMAC-SHA256.
* Isomorphic: uses Node.js crypto or Web Crypto API.
*
* Message format: "timestamp:METHOD:path:body"
* Returns: 64-character hex string
*/
function signRequest(secretKey, timestamp, method, path, body) {
async function signRequest(secretKey, timestamp, method, urlPath, body) {
const bodyStr = body || '';
const message = `${timestamp}:${method}:${path}:${bodyStr}`;
return crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
const message = `${timestamp}:${method}:${urlPath}:${bodyStr}`;
if (IS_NODE && crypto) {
// Node.js: synchronous HMAC
return crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
} else {
// Browser: Web Crypto API (async)
const encoder = new TextEncoder();
const keyData = encoder.encode(secretKey);
const messageData = encoder.encode(message);
const cryptoKey = await window.crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, messageData);
const hashArray = Array.from(new Uint8Array(signature));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
}
/**
* Make an authenticated HTTP request to the API.
* Sleep for a specified number of milliseconds.
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Make an authenticated HTTP request to the API using native fetch.
*
* Returns: Promise<Object> (parsed JSON response)
* Throws: Error on network errors or non-JSON response
*/
function makeRequest(method, path, publicKey, secretKey, data) {
return new Promise((resolve, reject) => {
const url = new URL(API_BASE + path);
const timestamp = Math.floor(Date.now() / 1000);
const body = data ? JSON.stringify(data) : '';
async function makeRequest(method, urlPath, publicKey, secretKey, data) {
const url = `${API_BASE}${urlPath}`;
const timestamp = Math.floor(Date.now() / 1000);
const body = data ? JSON.stringify(data) : '';
const signature = signRequest(secretKey, timestamp, method, path, body || null);
const signature = await signRequest(secretKey, timestamp, method, urlPath, body || null);
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp.toString(),
'X-Signature': signature,
'Content-Type': 'application/json',
'User-Agent': 'un-js/2.0',
},
timeout: 120000, // 120 seconds
};
const headers = {
'Authorization': `Bearer ${publicKey}`,
'X-Timestamp': timestamp.toString(),
'X-Signature': signature,
'Content-Type': 'application/json',
'User-Agent': 'un-js/2.0',
};
// Set Content-Length for methods with body
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
options.headers['Content-Length'] = Buffer.byteLength(body);
}
const options = {
method,
headers,
signal: AbortSignal.timeout(120000), // 120 seconds timeout
};
const req = https.request(options, (res) => {
let data = '';
// Add body for methods that support it
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
options.body = body;
}
res.on('data', (chunk) => {
data += chunk;
});
const response = await fetch(url, options);
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
return;
}
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Failed to parse response: ${e.message}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
// Write body for methods that support it
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
req.write(body);
}
req.end();
});
return response.json();
}
/**
* Get path to languages cache file.
* Get path to languages cache file. [Node.js only]
*/
function getLanguagesCachePath() {
return path.join(getUnsandboxDir(), 'languages.json');
if (!IS_NODE) return null;
const dir = getUnsandboxDir();
if (!dir) return null;
return path.join(dir, 'languages.json');
}
/**
* Load languages from cache if valid (< 1 hour old).
* Load languages from cache if valid (< 1 hour old). [Node.js only]
*/
function loadLanguagesCache() {
if (!IS_NODE || !fs) return null;
try {
const cachePath = getLanguagesCachePath();
if (!fs.existsSync(cachePath)) {
if (!cachePath || !fs.existsSync(cachePath)) {
return null;
}
@ -281,11 +351,13 @@ function loadLanguagesCache() {
}
/**
* Save languages to cache.
* Save languages to cache. [Node.js only]
*/
function saveLanguagesCache(languages) {
if (!IS_NODE || !fs) return;
try {
const cachePath = getLanguagesCachePath();
if (!cachePath) return;
const data = {
languages,
timestamp: Math.floor(Date.now() / 1000),
@ -1343,8 +1415,80 @@ async function image(prompt, options = {}) {
return makeRequest('POST', '/image', pk, sk, payload);
}
// Export all functions
module.exports = {
// ES Module exports
export {
// Code execution
executeCode,
executeAsync,
getJob,
waitForJob,
cancelJob,
listJobs,
getLanguages,
detectLanguage,
// Session management
listSessions,
getSession,
createSession,
deleteSession,
freezeSession,
unfreezeSession,
boostSession,
unboostSession,
shellSession,
// Service management
listServices,
createService,
getService,
updateService,
deleteService,
freezeService,
unfreezeService,
lockService,
unlockService,
setUnfreezeOnDemand,
getServiceLogs,
getServiceEnv,
setServiceEnv,
deleteServiceEnv,
exportServiceEnv,
redeployService,
executeInService,
// Snapshot management
sessionSnapshot,
serviceSnapshot,
listSnapshots,
restoreSnapshot,
deleteSnapshot,
lockSnapshot,
unlockSnapshot,
cloneSnapshot,
// Images API (LXD container images)
imagePublish,
listImages,
getImage,
deleteImage,
lockImage,
unlockImage,
setImageVisibility,
grantImageAccess,
revokeImageAccess,
listImageTrusted,
transferImage,
spawnFromImage,
cloneImage,
// Key validation
validateKeys,
// Image generation
image,
// Errors
CredentialsError,
// CLI
cliMain,
};
// Default export for convenience
export default {
// Code execution
executeCode,
executeAsync,
@ -2482,10 +2626,19 @@ async function cliMain() {
}
}
// CLI entry point
if (require.main === module) {
cliMain().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
// CLI entry point - detect if running as main module (ESM) [Node.js only]
// In ESM, we use import.meta.url to check if this is the main module
if (IS_NODE) {
const isMain = process.argv[1] && (
process.argv[1].endsWith('/un.js') ||
process.argv[1].endsWith('\\un.js') ||
import.meta.url === `file://${process.argv[1]}`
);
if (isMain) {
cliMain().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
}
}

View file

@ -15,7 +15,7 @@ local ltn12 = require("ltn12")
local Un = {}
Un.API_BASE = "https://api.unsandbox.com"
Un.VERSION = "4.2.12"
Un.VERSION = "4.2.13"
-- Credential loading
function Un.load_accounts_csv(path)

View file

@ -47,7 +47,7 @@ use Digest::HMAC_SHA256 qw(hmac_sha256_hex);
use File::HomeDir;
use Time::HiRes qw(time sleep);
our $VERSION = "4.2.12";
our $VERSION = "4.2.13";
our $API_BASE = 'https://api.unsandbox.com';
# Credential system

View file

@ -7,7 +7,7 @@ from setuptools import setup, find_packages
setup(
name="unsandbox-async",
version="4.2.12",
version="4.2.13",
description="Asynchronous Python SDK for unsandbox.com code execution",
long_description=open("README.md").read() if False else "Async Python SDK for unsandbox code execution",
author="unsandbox.com",

View file

@ -7,7 +7,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setup(
name="unsandbox",
version="4.2.12",
version="4.2.13",
author="Unsandbox",
description="Synchronous Python SDK for unsandbox.com code execution",
long_description=long_description,

View file

@ -27,7 +27,7 @@ from .un import (
CredentialsError,
)
__version__ = "4.2.12"
__version__ = "4.2.13"
__all__ = [
"execute_code",
"execute_async",

View file

@ -7,7 +7,7 @@
[package]
name = "un-async"
version = "4.2.12"
version = "4.2.13"
edition = "2021"
authors = ["unsandbox.com"]
description = "Asynchronous Rust SDK for unsandbox.com secure code execution API"

View file

@ -7,7 +7,7 @@
[package]
name = "un-sync"
version = "4.2.12"
version = "4.2.13"
edition = "2021"
authors = ["unsandbox.com"]
description = "Synchronous Rust SDK for unsandbox.com secure code execution API"