ci: Disable GitHub Actions workflow temporarily

This commit is contained in:
russell@unturf.com 2026-01-15 17:38:19 -05:00
parent 331cba42aa
commit 9ab564da60
16 changed files with 15954 additions and 771 deletions

File diff suppressed because it is too large Load diff

View file

@ -878,3 +878,685 @@ func DeleteSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
return resultChan
}
// ============================================================================
// Session Operations
// ============================================================================
// SessionOptions contains optional parameters for session creation.
type SessionOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use (e.g., "bash", "python3")
TTL int // Time-to-live in seconds (default: 3600)
VCPU int // Number of virtual CPUs (default: 1)
Multiplexer string // Multiplexer to use (e.g., "tmux")
}
// SessionListResult contains the result of listing sessions.
type SessionListResult struct {
Sessions []map[string]interface{}
Err error
}
// SessionResult contains the result of a session operation.
type SessionResult struct {
Data map[string]interface{}
Err error
}
// ListSessions lists all active sessions for the authenticated account.
// Returns a channel that receives exactly one SessionListResult then closes.
func ListSessions(creds *Credentials) <-chan SessionListResult {
resultChan := make(chan SessionListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/sessions", creds, nil)
if err != nil {
resultChan <- SessionListResult{Err: err}
return
}
var sessions []map[string]interface{}
if sessionsInterface, ok := response["sessions"].([]interface{}); ok {
sessions = make([]map[string]interface{}, len(sessionsInterface))
for i, session := range sessionsInterface {
if m, ok := session.(map[string]interface{}); ok {
sessions[i] = m
}
}
}
resultChan <- SessionListResult{Sessions: sessions}
}()
return resultChan
}
// GetSession gets details of a specific session.
// Returns a channel that receives exactly one SessionResult then closes.
func GetSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// CreateSession creates a new interactive session.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Args:
//
// creds: API credentials
// opts: Optional session configuration (can be nil for defaults)
func CreateSession(creds *Credentials, opts *SessionOptions) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.TTL > 0 {
data["ttl"] = opts.TTL
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
if opts.Multiplexer != "" {
data["multiplexer"] = opts.Multiplexer
}
}
response, err := makeRequest("POST", "/sessions", creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteSession terminates a session.
// Returns a channel that receives exactly one SessionResult then closes.
func DeleteSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// FreezeSession freezes a session (pauses execution, preserves state).
// Returns a channel that receives exactly one SessionResult then closes.
func FreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/freeze", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// UnfreezeSession unfreezes a previously frozen session.
// Returns a channel that receives exactly one SessionResult then closes.
func UnfreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unfreeze", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// BoostSession increases the vCPU allocation for a session.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Args:
//
// creds: API credentials
// sessionID: Session ID to boost
// vcpu: Number of vCPUs (2, 4, 8, etc.)
func BoostSession(creds *Credentials, sessionID string, vcpu int) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"vcpu": vcpu,
}
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/boost", sessionID), creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// UnboostSession resets the vCPU allocation for a session to default.
// Returns a channel that receives exactly one SessionResult then closes.
func UnboostSession(creds *Credentials, sessionID string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unboost", sessionID), creds, map[string]interface{}{})
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// ShellSession executes a command in a session's shell.
// Returns a channel that receives exactly one SessionResult then closes.
//
// Note: For interactive shell access, use the WebSocket-based shell endpoint.
// This function is for executing single commands.
func ShellSession(creds *Credentials, sessionID, command string) <-chan SessionResult {
resultChan := make(chan SessionResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"command": command,
}
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/shell", sessionID), creds, data)
resultChan <- SessionResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Service Operations
// ============================================================================
// ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs
}
// ServiceUpdateOptions contains optional parameters for service updates.
type ServiceUpdateOptions struct {
VCPU int // Number of virtual CPUs
}
// ServiceListResult contains the result of listing services.
type ServiceListResult struct {
Services []map[string]interface{}
Err error
}
// ServiceResult contains the result of a service operation.
type ServiceResult struct {
Data map[string]interface{}
Err error
}
// ListServices lists all services for the authenticated account.
// Returns a channel that receives exactly one ServiceListResult then closes.
func ListServices(creds *Credentials) <-chan ServiceListResult {
resultChan := make(chan ServiceListResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", "/services", creds, nil)
if err != nil {
resultChan <- ServiceListResult{Err: err}
return
}
var services []map[string]interface{}
if servicesInterface, ok := response["services"].([]interface{}); ok {
services = make([]map[string]interface{}, len(servicesInterface))
for i, service := range servicesInterface {
if m, ok := service.(map[string]interface{}); ok {
services[i] = m
}
}
}
resultChan <- ServiceListResult{Services: services}
}()
return resultChan
}
// CreateService creates a new persistent service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// name: Service name
// ports: Array of port numbers to expose
// bootstrap: Bootstrap script to run on service start
// opts: Optional service configuration (can be nil for defaults)
func CreateService(creds *Credentials, name string, ports []int, bootstrap string, opts *ServiceOptions) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"name": name,
"ports": ports,
"bootstrap": bootstrap,
}
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
}
response, err := makeRequest("POST", "/services", creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetService gets details of a specific service.
// Returns a channel that receives exactly one ServiceResult then closes.
func GetService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/services/%s", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UpdateService updates a service's configuration.
// Returns a channel that receives exactly one ServiceResult then closes.
func UpdateService(creds *Credentials, serviceID string, opts *ServiceUpdateOptions) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if opts != nil {
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
}
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteService destroys a service.
// Returns a channel that receives exactly one ServiceResult then closes.
func DeleteService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// FreezeService freezes a service (pauses execution, preserves state).
// Returns a channel that receives exactly one ServiceResult then closes.
func FreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/freeze", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UnfreezeService unfreezes a previously frozen service.
// Returns a channel that receives exactly one ServiceResult then closes.
func UnfreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unfreeze", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// LockService locks a service to prevent modifications or deletion.
// Returns a channel that receives exactly one ServiceResult then closes.
func LockService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/lock", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// UnlockService unlocks a previously locked service.
// Returns a channel that receives exactly one ServiceResult then closes.
func UnlockService(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetServiceLogs retrieves logs from a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// all: If true, returns all logs; if false, returns only recent logs
func GetServiceLogs(creds *Credentials, serviceID string, all bool) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
path := fmt.Sprintf("/services/%s/logs", serviceID)
if all {
path = fmt.Sprintf("/services/%s/logs?all=true", serviceID)
}
response, err := makeRequest("GET", path, creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// GetServiceEnv retrieves the environment variable names for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
// Note: Values are not returned for security; use ExportServiceEnv for full export.
func GetServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("GET", fmt.Sprintf("/services/%s/env", serviceID), creds, nil)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// SetServiceEnv sets environment variables for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// env: Map of environment variable names to values
func SetServiceEnv(creds *Credentials, serviceID string, env map[string]string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env", serviceID), creds, env)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// DeleteServiceEnv deletes environment variables from a service.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// keys: List of environment variable names to delete (nil deletes all)
func DeleteServiceEnv(creds *Credentials, serviceID string, keys []string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
var data interface{}
if keys != nil {
data = map[string]interface{}{"keys": keys}
}
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s/env", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ExportServiceEnv exports all environment variables for a service.
// Returns a channel that receives exactly one ServiceResult then closes.
// Returns the full .env format content with values.
func ExportServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env/export", serviceID), creds, map[string]interface{}{})
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// RedeployService redeploys a service with optional new bootstrap script.
// Returns a channel that receives exactly one ServiceResult then closes.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := make(map[string]interface{})
if bootstrap != "" {
data["bootstrap"] = bootstrap
}
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ExecuteInService executes a command in a running service.
// Returns a channel that receives exactly one ServiceResult then closes.
func ExecuteInService(creds *Credentials, serviceID, command string) <-chan ServiceResult {
resultChan := make(chan ServiceResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"command": command,
}
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
resultChan <- ServiceResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Additional Snapshot Operations
// ============================================================================
// LockSnapshot locks a snapshot to prevent deletion.
// Returns a channel that receives exactly one DeleteResult then closes.
func LockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
resultChan := make(chan DeleteResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/lock", snapshotID), creds, map[string]interface{}{})
resultChan <- DeleteResult{Data: response, Err: err}
}()
return resultChan
}
// UnlockSnapshot unlocks a previously locked snapshot.
// Returns a channel that receives exactly one DeleteResult then closes.
func UnlockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
resultChan := make(chan DeleteResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/unlock", snapshotID), creds, map[string]interface{}{})
resultChan <- DeleteResult{Data: response, Err: err}
}()
return resultChan
}
// CloneSnapshotOptions contains optional parameters for snapshot cloning.
type CloneSnapshotOptions struct {
Name string // Name for the cloned resource
Shell string // Shell to use (for session clones)
Ports []int // Ports to expose (for service clones)
}
// CloneSnapshotResult contains the result of cloning a snapshot.
type CloneSnapshotResult struct {
Data map[string]interface{}
Err error
}
// CloneSnapshot clones a snapshot into a new session or service.
// Returns a channel that receives exactly one CloneSnapshotResult then closes.
//
// Args:
//
// creds: API credentials
// snapshotID: Snapshot ID to clone
// cloneType: "session" or "service"
// opts: Optional clone configuration (can be nil)
func CloneSnapshot(creds *Credentials, snapshotID, cloneType string, opts *CloneSnapshotOptions) <-chan CloneSnapshotResult {
resultChan := make(chan CloneSnapshotResult, 1)
go func() {
defer close(resultChan)
data := map[string]interface{}{
"type": cloneType,
}
if opts != nil {
if opts.Name != "" {
data["name"] = opts.Name
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.Ports != nil {
data["ports"] = opts.Ports
}
}
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/clone", snapshotID), creds, data)
resultChan <- CloneSnapshotResult{Data: response, Err: err}
}()
return resultChan
}
// ============================================================================
// Key Validation
// ============================================================================
// ValidateKeysResult contains the result of validating API keys.
type ValidateKeysResult struct {
Data map[string]interface{}
Err error
}
// ValidateKeys validates the API credentials with the server.
// Returns a channel that receives exactly one ValidateKeysResult then closes.
// Returns account information if valid, error if invalid.
func ValidateKeys(creds *Credentials) <-chan ValidateKeysResult {
resultChan := make(chan ValidateKeysResult, 1)
go func() {
defer close(resultChan)
response, err := makeRequest("POST", "/keys/validate", creds, map[string]interface{}{})
resultChan <- ValidateKeysResult{Data: response, Err: err}
}()
return resultChan
}

View file

@ -649,3 +649,363 @@ func RestoreSnapshot(creds *Credentials, snapshotID string) (map[string]interfac
func DeleteSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
return makeRequest("DELETE", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
}
// ============================================================================
// Session Operations
// ============================================================================
// SessionOptions contains optional parameters for session creation.
type SessionOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use (e.g., "bash", "python3")
TTL int // Time-to-live in seconds (default: 3600)
VCPU int // Number of virtual CPUs (default: 1)
Multiplexer string // Multiplexer to use (e.g., "tmux")
}
// ListSessions lists all active sessions for the authenticated account.
func ListSessions(creds *Credentials) ([]map[string]interface{}, error) {
response, err := makeRequest("GET", "/sessions", creds, nil)
if err != nil {
return nil, err
}
if sessions, ok := response["sessions"].([]interface{}); ok {
result := make([]map[string]interface{}, len(sessions))
for i, session := range sessions {
if m, ok := session.(map[string]interface{}); ok {
result[i] = m
}
}
return result, nil
}
return []map[string]interface{}{}, nil
}
// GetSession gets details of a specific session.
func GetSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
return makeRequest("GET", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
}
// CreateSession creates a new interactive session.
//
// Args:
//
// creds: API credentials
// opts: Optional session configuration (can be nil for defaults)
//
// Returns:
//
// Session info including session_id and container_name
func CreateSession(creds *Credentials, opts *SessionOptions) (map[string]interface{}, error) {
data := make(map[string]interface{})
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.TTL > 0 {
data["ttl"] = opts.TTL
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
if opts.Multiplexer != "" {
data["multiplexer"] = opts.Multiplexer
}
}
return makeRequest("POST", "/sessions", creds, data)
}
// DeleteSession terminates a session.
func DeleteSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
return makeRequest("DELETE", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
}
// FreezeSession freezes a session (pauses execution, preserves state).
func FreezeSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/sessions/%s/freeze", sessionID), creds, map[string]interface{}{})
}
// UnfreezeSession unfreezes a previously frozen session.
func UnfreezeSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/sessions/%s/unfreeze", sessionID), creds, map[string]interface{}{})
}
// BoostSession increases the vCPU allocation for a session.
//
// Args:
//
// creds: API credentials
// sessionID: Session ID to boost
// vcpu: Number of vCPUs (2, 4, 8, etc.)
func BoostSession(creds *Credentials, sessionID string, vcpu int) (map[string]interface{}, error) {
data := map[string]interface{}{
"vcpu": vcpu,
}
return makeRequest("POST", fmt.Sprintf("/sessions/%s/boost", sessionID), creds, data)
}
// UnboostSession resets the vCPU allocation for a session to default.
func UnboostSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/sessions/%s/unboost", sessionID), creds, map[string]interface{}{})
}
// ShellSession executes a command in a session's shell.
//
// Note: For interactive shell access, use the WebSocket-based shell endpoint.
// This function is for executing single commands.
func ShellSession(creds *Credentials, sessionID, command string) (map[string]interface{}, error) {
data := map[string]interface{}{
"command": command,
}
return makeRequest("POST", fmt.Sprintf("/sessions/%s/shell", sessionID), creds, data)
}
// ============================================================================
// Service Operations
// ============================================================================
// ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs
}
// ServiceUpdateOptions contains optional parameters for service updates.
type ServiceUpdateOptions struct {
VCPU int // Number of virtual CPUs
}
// ListServices lists all services for the authenticated account.
func ListServices(creds *Credentials) ([]map[string]interface{}, error) {
response, err := makeRequest("GET", "/services", creds, nil)
if err != nil {
return nil, err
}
if services, ok := response["services"].([]interface{}); ok {
result := make([]map[string]interface{}, len(services))
for i, service := range services {
if m, ok := service.(map[string]interface{}); ok {
result[i] = m
}
}
return result, nil
}
return []map[string]interface{}{}, nil
}
// CreateService creates a new persistent service.
//
// Args:
//
// creds: API credentials
// name: Service name
// ports: Array of port numbers to expose
// bootstrap: Bootstrap script to run on service start
// opts: Optional service configuration (can be nil for defaults)
func CreateService(creds *Credentials, name string, ports []int, bootstrap string, opts *ServiceOptions) (map[string]interface{}, error) {
data := map[string]interface{}{
"name": name,
"ports": ports,
"bootstrap": bootstrap,
}
if opts != nil {
if opts.NetworkMode != "" {
data["network_mode"] = opts.NetworkMode
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
}
return makeRequest("POST", "/services", creds, data)
}
// GetService gets details of a specific service.
func GetService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("GET", fmt.Sprintf("/services/%s", serviceID), creds, nil)
}
// UpdateService updates a service's configuration.
func UpdateService(creds *Credentials, serviceID string, opts *ServiceUpdateOptions) (map[string]interface{}, error) {
data := make(map[string]interface{})
if opts != nil {
if opts.VCPU > 0 {
data["vcpu"] = opts.VCPU
}
}
return makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, data)
}
// DeleteService destroys a service.
func DeleteService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("DELETE", fmt.Sprintf("/services/%s", serviceID), creds, nil)
}
// FreezeService freezes a service (pauses execution, preserves state).
func FreezeService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/freeze", serviceID), creds, map[string]interface{}{})
}
// UnfreezeService unfreezes a previously frozen service.
func UnfreezeService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/unfreeze", serviceID), creds, map[string]interface{}{})
}
// LockService locks a service to prevent modifications or deletion.
func LockService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/lock", serviceID), creds, map[string]interface{}{})
}
// UnlockService unlocks a previously locked service.
func UnlockService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{})
}
// GetServiceLogs retrieves logs from a service.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// all: If true, returns all logs; if false, returns only recent logs
func GetServiceLogs(creds *Credentials, serviceID string, all bool) (map[string]interface{}, error) {
path := fmt.Sprintf("/services/%s/logs", serviceID)
if all {
path = fmt.Sprintf("/services/%s/logs?all=true", serviceID)
}
return makeRequest("GET", path, creds, nil)
}
// GetServiceEnv retrieves the environment variable names for a service.
// Note: Values are not returned for security; use ExportServiceEnv for full export.
func GetServiceEnv(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("GET", fmt.Sprintf("/services/%s/env", serviceID), creds, nil)
}
// SetServiceEnv sets environment variables for a service.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// env: Map of environment variable names to values
func SetServiceEnv(creds *Credentials, serviceID string, env map[string]string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/env", serviceID), creds, env)
}
// DeleteServiceEnv deletes environment variables from a service.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// keys: List of environment variable names to delete (nil deletes all)
func DeleteServiceEnv(creds *Credentials, serviceID string, keys []string) (map[string]interface{}, error) {
var data interface{}
if keys != nil {
data = map[string]interface{}{"keys": keys}
}
return makeRequest("DELETE", fmt.Sprintf("/services/%s/env", serviceID), creds, data)
}
// ExportServiceEnv exports all environment variables for a service.
// Returns the full .env format content with values.
func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/services/%s/env/export", serviceID), creds, map[string]interface{}{})
}
// RedeployService redeploys a service with optional new bootstrap script.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) {
data := make(map[string]interface{})
if bootstrap != "" {
data["bootstrap"] = bootstrap
}
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
}
// ExecuteInService executes a command in a running service.
func ExecuteInService(creds *Credentials, serviceID, command string) (map[string]interface{}, error) {
data := map[string]interface{}{
"command": command,
}
return makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
}
// ============================================================================
// Additional Snapshot Operations
// ============================================================================
// LockSnapshot locks a snapshot to prevent deletion.
func LockSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/snapshots/%s/lock", snapshotID), creds, map[string]interface{}{})
}
// UnlockSnapshot unlocks a previously locked snapshot.
func UnlockSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
return makeRequest("POST", fmt.Sprintf("/snapshots/%s/unlock", snapshotID), creds, map[string]interface{}{})
}
// CloneSnapshotOptions contains optional parameters for snapshot cloning.
type CloneSnapshotOptions struct {
Name string // Name for the cloned resource
Shell string // Shell to use (for session clones)
Ports []int // Ports to expose (for service clones)
}
// CloneSnapshot clones a snapshot into a new session or service.
//
// Args:
//
// creds: API credentials
// snapshotID: Snapshot ID to clone
// cloneType: "session" or "service"
// opts: Optional clone configuration (can be nil)
func CloneSnapshot(creds *Credentials, snapshotID, cloneType string, opts *CloneSnapshotOptions) (map[string]interface{}, error) {
data := map[string]interface{}{
"type": cloneType,
}
if opts != nil {
if opts.Name != "" {
data["name"] = opts.Name
}
if opts.Shell != "" {
data["shell"] = opts.Shell
}
if opts.Ports != nil {
data["ports"] = opts.Ports
}
}
return makeRequest("POST", fmt.Sprintf("/snapshots/%s/clone", snapshotID), creds, data)
}
// ============================================================================
// Key Validation
// ============================================================================
// ValidateKeys validates the API credentials with the server.
// Returns account information if valid, error if invalid.
func ValidateKeys(creds *Credentials) (map[string]interface{}, error) {
return makeRequest("POST", "/keys/validate", creds, map[string]interface{}{})
}

View file

@ -1088,6 +1088,694 @@ public class UnsandboxAsync {
return makeRequest("DELETE", "/snapshots/" + snapshotId, creds[0], creds[1], null);
}
// ========================================================================
// Session API Methods
// ========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing list of session maps
*/
@SuppressWarnings("unchecked")
public static CompletableFuture<List<Map<String, Object>>> listSessions(
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/sessions", creds[0], creds[1], null)
.thenApply(response -> {
Object sessions = response.get("sessions");
if (sessions instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) sessions) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
});
}
/**
* Get details of a specific session.
*
* @param sessionId Session ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing session details map
*/
public static CompletableFuture<Map<String, Object>> getSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Create a new interactive session.
*
* @param language Programming language/shell for the session (e.g., "bash", "python3")
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param opts Optional parameters: network_mode, ttl, shell, multiplexer, vcpu
* @return CompletableFuture containing response map with session_id, container_name
*/
public static CompletableFuture<Map<String, Object>> createSession(
String language,
String publicKey,
String secretKey,
Map<String, Object> opts
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("network_mode", "zerotrust");
data.put("ttl", 3600);
if (language != null && !language.isEmpty()) {
data.put("shell", language);
}
if (opts != null) {
data.putAll(opts);
}
return makeRequest("POST", "/sessions", creds[0], creds[1], data);
}
/**
* Delete (terminate) a session.
*
* @param sessionId Session ID to terminate
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with termination confirmation
*/
public static CompletableFuture<Map<String, Object>> deleteSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Freeze a session (pause execution, reduce resource consumption).
*
* @param sessionId Session ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with freeze confirmation
*/
public static CompletableFuture<Map<String, Object>> freezeSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a session (resume execution).
*
* @param sessionId Session ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with unfreeze confirmation
*/
public static CompletableFuture<Map<String, Object>> unfreezeSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Boost a session's resources (increase vCPU and memory).
*
* @param sessionId Session ID to boost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with boost confirmation
*/
public static CompletableFuture<Map<String, Object>> boostSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("vcpu", 2);
return makeRequest("POST", "/sessions/" + sessionId + "/boost", creds[0], creds[1], data);
}
/**
* Remove boost from a session (return to base resources).
*
* @param sessionId Session ID to unboost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with unboost confirmation
*/
public static CompletableFuture<Map<String, Object>> unboostSession(
String sessionId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unboost", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a shell command in an existing session.
*
* @param sessionId Session ID to execute command in
* @param command Shell command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with command output
*/
public static CompletableFuture<Map<String, Object>> shellSession(
String sessionId,
String command,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/sessions/" + sessionId + "/shell", creds[0], creds[1], data);
}
// ========================================================================
// Service API Methods
// ========================================================================
/**
* List all services for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing list of service maps
*/
@SuppressWarnings("unchecked")
public static CompletableFuture<List<Map<String, Object>>> listServices(
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services", creds[0], creds[1], null)
.thenApply(response -> {
Object services = response.get("services");
if (services instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) services) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
});
}
/**
* Create a new persistent service.
*
* @param name Service name
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with service_id
*/
public static CompletableFuture<Map<String, Object>> createService(
String name,
String ports,
String bootstrap,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("name", name);
if (ports != null && !ports.isEmpty()) {
List<Integer> portList = new ArrayList<>();
for (String p : ports.split(",")) {
try {
portList.add(Integer.parseInt(p.trim()));
} catch (NumberFormatException e) {
// Skip invalid port
}
}
data.put("ports", portList);
}
if (bootstrap != null && !bootstrap.isEmpty()) {
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
data.put("bootstrap_url", bootstrap);
} else {
data.put("bootstrap", bootstrap);
}
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
/**
* Get details of a specific service.
*
* @param serviceId Service ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing service details map
*/
public static CompletableFuture<Map<String, Object>> getService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Update a service's configuration.
*
* @param serviceId Service ID to update
* @param opts Update options (e.g., vcpu for resizing)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with update confirmation
*/
public static CompletableFuture<Map<String, Object>> updateService(
String serviceId,
Map<String, Object> opts,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], opts);
}
/**
* Delete (destroy) a service.
*
* @param serviceId Service ID to destroy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with deletion confirmation
*/
public static CompletableFuture<Map<String, Object>> deleteService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Freeze a service (pause execution, reduce resource consumption).
*
* @param serviceId Service ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with freeze confirmation
*/
public static CompletableFuture<Map<String, Object>> freezeService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a service (resume execution).
*
* @param serviceId Service ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with unfreeze confirmation
*/
public static CompletableFuture<Map<String, Object>> unfreezeService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Lock a service (prevent modifications and termination).
*
* @param serviceId Service ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with lock confirmation
*/
public static CompletableFuture<Map<String, Object>> lockService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a service (allow modifications and termination).
*
* @param serviceId Service ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with unlock confirmation
*/
public static CompletableFuture<Map<String, Object>> unlockService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Get bootstrap logs for a service.
*
* @param serviceId Service ID to get logs for
* @param all If true, return all logs; if false, return recent logs only
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with logs
*/
public static CompletableFuture<Map<String, Object>> getServiceLogs(
String serviceId,
boolean all,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/logs";
if (all) {
path += "?all=true";
}
return makeRequest("GET", path, creds[0], creds[1], null);
}
/**
* Get environment vault status for a service.
*
* @param serviceId Service ID to get env status for
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with has_vault, count, updated_at
*/
public static CompletableFuture<Map<String, Object>> getServiceEnv(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId + "/env", creds[0], creds[1], null);
}
/**
* Set environment vault for a service.
*
* @param serviceId Service ID to set env for
* @param env Environment variables map (KEY=VALUE pairs)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with set confirmation
*/
public static CompletableFuture<Map<String, Object>> setServiceEnv(
String serviceId,
Map<String, String> env,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("env", env);
return makeRequest("POST", "/services/" + serviceId + "/env", creds[0], creds[1], data);
}
/**
* Delete environment variables from a service's vault.
*
* @param serviceId Service ID to delete env from
* @param keys List of keys to delete (null to delete entire vault)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with deletion confirmation
*/
public static CompletableFuture<Map<String, Object>> deleteServiceEnv(
String serviceId,
List<String> keys,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/env";
if (keys != null && !keys.isEmpty()) {
path += "?keys=" + String.join(",", keys);
}
return makeRequest("DELETE", path, creds[0], creds[1], null);
}
/**
* Export environment vault for a service (returns decrypted values).
*
* @param serviceId Service ID to export env from
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with exported environment variables
*/
public static CompletableFuture<Map<String, Object>> exportServiceEnv(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/env/export", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param serviceId Service ID to redeploy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with redeploy confirmation
*/
public static CompletableFuture<Map<String, Object>> redeployService(
String serviceId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a command in a service container.
*
* @param serviceId Service ID to execute command in
* @param command Command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with stdout, stderr, exit_code
*/
public static CompletableFuture<Map<String, Object>> executeInService(
String serviceId,
String command,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/services/" + serviceId + "/execute", creds[0], creds[1], data);
}
// ========================================================================
// Additional Snapshot API Methods
// ========================================================================
/**
* Lock a snapshot (prevent deletion).
*
* @param snapshotId Snapshot ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with lock confirmation
*/
public static CompletableFuture<Map<String, Object>> lockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a snapshot (allow deletion).
*
* @param snapshotId Snapshot ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with unlock confirmation
*/
public static CompletableFuture<Map<String, Object>> unlockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Clone a snapshot to create a new snapshot with a different name.
*
* @param snapshotId Snapshot ID to clone
* @param name Name for the cloned snapshot
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return CompletableFuture containing response map with new snapshot_id
*/
public static CompletableFuture<Map<String, Object>> cloneSnapshot(
String snapshotId,
String name,
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
return makeRequest("POST", "/snapshots/" + snapshotId + "/clone", creds[0], creds[1], data);
}
// ========================================================================
// Key Validation API
// ========================================================================
/**
* Validate API key credentials.
*
* @param publicKey API public key to validate
* @param secretKey API secret key to validate
* @return CompletableFuture containing response map with validation result (valid, tier, etc.)
*/
public static CompletableFuture<Map<String, Object>> validateKeys(
String publicKey,
String secretKey
) {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
}
// ========================================================================
// HTTP Request Helpers
// ========================================================================
/**
* Make an HTTP request with a specified method (supports PATCH).
*/
private static CompletableFuture<Map<String, Object>> makeRequestWithMethod(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) {
return CompletableFuture.supplyAsync(() -> {
try {
return makeRequestSyncWithMethod(method, path, publicKey, secretKey, data);
} catch (IOException e) {
throw new CompletionException(e);
}
}, executor);
}
private static Map<String, Object> makeRequestSyncWithMethod(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
String url = API_BASE + path;
long timestamp = System.currentTimeMillis() / 1000;
String body = (data != null) ? mapToJson(data) : "";
String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(DEFAULT_TIMEOUT_MS);
conn.setReadTimeout(DEFAULT_TIMEOUT_MS);
conn.setRequestProperty("Authorization", "Bearer " + publicKey);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json");
if (data != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
}
int responseCode = conn.getResponseCode();
String responseBody;
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream == null) {
responseBody = "";
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
responseBody = sb.toString();
}
}
if (responseCode < 200 || responseCode >= 300) {
throw new ApiException(
"API request failed with status " + responseCode,
responseCode,
responseBody
);
}
return parseJson(responseBody);
}
/**
* Shutdown the executor services used by this class.
* Call this when your application is shutting down.

View file

@ -1050,4 +1050,761 @@ public class Un {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/snapshots/" + snapshotId, creds[0], creds[1], null);
}
// ========================================================================
// Session API Methods
// ========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of session maps containing id, container_name, shell, status, remaining_ttl
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listSessions(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/sessions", creds[0], creds[1], null);
Object sessions = response.get("sessions");
if (sessions instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) sessions) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Get details of a specific session.
*
* @param sessionId Session ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Session details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Create a new interactive session.
*
* @param language Programming language/shell for the session (e.g., "bash", "python3")
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param opts Optional parameters: network_mode, ttl, shell, multiplexer, vcpu
* @return Response map containing session_id, container_name
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createSession(
String language,
String publicKey,
String secretKey,
Map<String, Object> opts
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("network_mode", "zerotrust");
data.put("ttl", 3600);
if (language != null && !language.isEmpty()) {
data.put("shell", language);
}
if (opts != null) {
data.putAll(opts);
}
return makeRequest("POST", "/sessions", creds[0], creds[1], data);
}
/**
* Delete (terminate) a session.
*
* @param sessionId Session ID to terminate
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with termination confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Freeze a session (pause execution, reduce resource consumption).
*
* @param sessionId Session ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with freeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> freezeSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a session (resume execution).
*
* @param sessionId Session ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unfreeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unfreezeSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Boost a session's resources (increase vCPU and memory).
*
* @param sessionId Session ID to boost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with boost confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> boostSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("vcpu", 2);
return makeRequest("POST", "/sessions/" + sessionId + "/boost", creds[0], creds[1], data);
}
/**
* Remove boost from a session (return to base resources).
*
* @param sessionId Session ID to unboost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unboost confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unboostSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unboost", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a shell command in an existing session.
*
* @param sessionId Session ID to execute command in
* @param command Shell command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with command output
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> shellSession(
String sessionId,
String command,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/sessions/" + sessionId + "/shell", creds[0], creds[1], data);
}
// ========================================================================
// Service API Methods
// ========================================================================
/**
* List all services for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of service maps containing id, name, state, ports, disk_used
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listServices(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/services", creds[0], creds[1], null);
Object services = response.get("services");
if (services instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) services) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Create a new persistent service.
*
* @param name Service name
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createService(
String name,
String ports,
String bootstrap,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("name", name);
if (ports != null && !ports.isEmpty()) {
// Parse ports string into list
List<Integer> portList = new ArrayList<>();
for (String p : ports.split(",")) {
try {
portList.add(Integer.parseInt(p.trim()));
} catch (NumberFormatException e) {
// Skip invalid port
}
}
data.put("ports", portList);
}
if (bootstrap != null && !bootstrap.isEmpty()) {
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
data.put("bootstrap_url", bootstrap);
} else {
data.put("bootstrap", bootstrap);
}
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
/**
* Get details of a specific service.
*
* @param serviceId Service ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Service details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Update a service's configuration.
*
* @param serviceId Service ID to update
* @param opts Update options (e.g., vcpu for resizing)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with update confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> updateService(
String serviceId,
Map<String, Object> opts,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], opts);
}
/**
* Delete (destroy) a service.
*
* @param serviceId Service ID to destroy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Freeze a service (pause execution, reduce resource consumption).
*
* @param serviceId Service ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with freeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> freezeService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a service (resume execution).
*
* @param serviceId Service ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unfreeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unfreezeService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Lock a service (prevent modifications and termination).
*
* @param serviceId Service ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with lock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> lockService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a service (allow modifications and termination).
*
* @param serviceId Service ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unlock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unlockService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Get bootstrap logs for a service.
*
* @param serviceId Service ID to get logs for
* @param all If true, return all logs; if false, return recent logs only
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing logs
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getServiceLogs(
String serviceId,
boolean all,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/logs";
if (all) {
path += "?all=true";
}
return makeRequest("GET", path, creds[0], creds[1], null);
}
/**
* Get environment vault status for a service.
*
* @param serviceId Service ID to get env status for
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing has_vault, count, updated_at
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getServiceEnv(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId + "/env", creds[0], creds[1], null);
}
/**
* Set environment vault for a service.
*
* @param serviceId Service ID to set env for
* @param env Environment variables map (KEY=VALUE pairs)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with set confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> setServiceEnv(
String serviceId,
Map<String, String> env,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("env", env);
return makeRequest("POST", "/services/" + serviceId + "/env", creds[0], creds[1], data);
}
/**
* Delete environment variables from a service's vault.
*
* @param serviceId Service ID to delete env from
* @param keys List of keys to delete (null to delete entire vault)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteServiceEnv(
String serviceId,
List<String> keys,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/env";
if (keys != null && !keys.isEmpty()) {
// URL encode keys parameter
path += "?keys=" + String.join(",", keys);
}
return makeRequest("DELETE", path, creds[0], creds[1], null);
}
/**
* Export environment vault for a service (returns decrypted values).
*
* @param serviceId Service ID to export env from
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing exported environment variables
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> exportServiceEnv(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/env/export", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param serviceId Service ID to redeploy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with redeploy confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> redeployService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a command in a service container.
*
* @param serviceId Service ID to execute command in
* @param command Command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing stdout, stderr, exit_code
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> executeInService(
String serviceId,
String command,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/services/" + serviceId + "/execute", creds[0], creds[1], data);
}
// ========================================================================
// Additional Snapshot API Methods
// ========================================================================
/**
* Lock a snapshot (prevent deletion).
*
* @param snapshotId Snapshot ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with lock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> lockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a snapshot (allow deletion).
*
* @param snapshotId Snapshot ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unlock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unlockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Clone a snapshot to create a new snapshot with a different name.
*
* @param snapshotId Snapshot ID to clone
* @param name Name for the cloned snapshot
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing new snapshot_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> cloneSnapshot(
String snapshotId,
String name,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
return makeRequest("POST", "/snapshots/" + snapshotId + "/clone", creds[0], creds[1], data);
}
// ========================================================================
// Key Validation API
// ========================================================================
/**
* Validate API key credentials.
*
* @param publicKey API public key to validate
* @param secretKey API secret key to validate
* @return Response map with validation result (valid, tier, etc.)
* @throws IOException on network errors
* @throws ApiException if API returns an error (including invalid credentials)
*/
public static Map<String, Object> validateKeys(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
// Note: validateKeys uses POST to /keys/validate
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
}
// ========================================================================
// HTTP Request Helpers
// ========================================================================
/**
* Make an HTTP request with a specified method (supports PATCH).
*/
private static Map<String, Object> makeRequestWithMethod(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
String url = API_BASE + path;
long timestamp = System.currentTimeMillis() / 1000;
String body = (data != null) ? mapToJson(data) : "";
String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(DEFAULT_TIMEOUT_MS);
conn.setReadTimeout(DEFAULT_TIMEOUT_MS);
conn.setRequestProperty("Authorization", "Bearer " + publicKey);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json");
if (data != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
}
int responseCode = conn.getResponseCode();
String responseBody;
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream == null) {
responseBody = "";
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
responseBody = sb.toString();
}
}
if (responseCode < 200 || responseCode >= 300) {
throw new ApiException(
"API request failed with status " + responseCode,
responseCode,
responseBody
);
}
return parseJson(responseBody);
}
}

View file

@ -5,19 +5,22 @@
*
* Library Usage:
* import {
* executeCode,
* executeAsync,
* getJob,
* waitForJob,
* cancelJob,
* listJobs,
* getLanguages,
* detectLanguage,
* sessionSnapshot,
* serviceSnapshot,
* listSnapshots,
* restoreSnapshot,
* deleteSnapshot,
* // 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,
* getServiceLogs, getServiceEnv, setServiceEnv, deleteServiceEnv,
* exportServiceEnv, redeployService, executeInService,
* // Snapshot management
* sessionSnapshot, serviceSnapshot, listSnapshots, restoreSnapshot,
* deleteSnapshot, lockSnapshot, unlockSnapshot, cloneSnapshot,
* // Key validation
* validateKeys,
* } from './un_async.js';
*
* // Execute code (awaits until completion)
@ -220,7 +223,8 @@ async function makeRequest(method, urlPath, publicKey, secretKey, data) {
signal: AbortSignal.timeout(120000), // 120 seconds timeout
};
if (method === 'POST' && body) {
// Add body for methods that support it
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
options.body = body;
}
@ -581,8 +585,491 @@ async function deleteSnapshot(snapshotId, publicKey, secretKey) {
return makeRequest('DELETE', `/snapshots/${snapshotId}`, publicKey, secretKey);
}
// ============================================================================
// Session Management Functions
// ============================================================================
/**
* List all active sessions.
*
* Returns: Promise<Array> (list of session objects)
*/
async function listSessions(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('GET', '/sessions', publicKey, secretKey);
return response.sessions || [];
}
/**
* Get details of a specific session.
*
* Args:
* sessionId: Session ID to retrieve
*
* Returns: Promise<Object> (session details)
*/
async function getSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/sessions/${sessionId}`, publicKey, secretKey);
}
/**
* Create a new interactive session.
*
* Args:
* language: Optional programming language/shell (default: "bash")
* opts: Optional settings:
* - networkMode: "zerotrust" (default) or "semitrusted"
* - shell: Shell to use (e.g., "python3", "bash")
* - multiplexer: "tmux", "screen", or null
* - vcpu: Number of vCPUs (1-8)
* - ttl: Time-to-live in seconds
*
* Returns: Promise<Object> (session info with session_id, container_name)
*/
async function createSession(language, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {
network_mode: opts.networkMode || 'zerotrust',
ttl: opts.ttl || 3600,
};
if (language) data.shell = language;
if (opts.shell) data.shell = opts.shell;
if (opts.multiplexer) data.multiplexer = opts.multiplexer;
if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu;
return makeRequest('POST', '/sessions', publicKey, secretKey, data);
}
/**
* Delete/terminate a session.
*
* Args:
* sessionId: Session ID to terminate
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('DELETE', `/sessions/${sessionId}`, publicKey, secretKey);
}
/**
* Freeze a session (pause execution, preserve state).
*
* Args:
* sessionId: Session ID to freeze
*
* Returns: Promise<Object> (freeze confirmation)
*/
async function freezeSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/freeze`, publicKey, secretKey, {});
}
/**
* Unfreeze a session (resume execution).
*
* Args:
* sessionId: Session ID to unfreeze
*
* Returns: Promise<Object> (unfreeze confirmation)
*/
async function unfreezeSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/unfreeze`, publicKey, secretKey, {});
}
/**
* Boost a session's resources (increase vCPU, memory).
*
* Args:
* sessionId: Session ID to boost
* vcpu: Number of vCPUs (default: 2)
*
* Returns: Promise<Object> (boost confirmation)
*/
async function boostSession(sessionId, vcpu = 2, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/boost`, publicKey, secretKey, { vcpu });
}
/**
* Unboost a session (return to base resources).
*
* Args:
* sessionId: Session ID to unboost
*
* Returns: Promise<Object> (unboost confirmation)
*/
async function unboostSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/unboost`, publicKey, secretKey, {});
}
/**
* Execute a shell command in a session.
*
* Note: This initiates a WebSocket connection for interactive shell.
* For simple command execution, this sends the command via the shell endpoint.
*
* Args:
* sessionId: Session ID
* command: Command to execute
*
* Returns: Promise<Object> (command result)
*/
async function shellSession(sessionId, command, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/shell`, publicKey, secretKey, { command });
}
// ============================================================================
// Service Management Functions
// ============================================================================
/**
* List all services.
*
* Returns: Promise<Array> (list of service objects)
*/
async function listServices(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('GET', '/services', publicKey, secretKey);
return response.services || [];
}
/**
* Create a new service (persistent container).
*
* Args:
* name: Service name
* ports: Array of port numbers to expose (e.g., [80, 443])
* bootstrap: Bootstrap script content or URL
* opts: Optional settings:
* - networkMode: "zerotrust" or "semitrusted"
* - vcpu: Number of vCPUs (1-8)
* - domains: Array of custom domains
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
*
* Returns: Promise<Object> (service info with service_id)
*/
async function createService(name, ports, bootstrap, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (name) data.name = name;
if (ports && ports.length > 0) data.ports = ports;
if (bootstrap) {
// If bootstrap starts with http, treat as URL, otherwise as content
if (bootstrap.startsWith('http://') || bootstrap.startsWith('https://')) {
data.bootstrap = bootstrap;
} else {
data.bootstrap_content = bootstrap;
}
}
if (opts.networkMode) data.network_mode = opts.networkMode;
if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu;
if (opts.domains) data.custom_domains = opts.domains;
if (opts.serviceType) data.service_type = opts.serviceType;
return makeRequest('POST', '/services', publicKey, secretKey, data);
}
/**
* Get details of a specific service.
*
* Args:
* serviceId: Service ID to retrieve
*
* Returns: Promise<Object> (service details)
*/
async function getService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/services/${serviceId}`, publicKey, secretKey);
}
/**
* Update a service (resize vCPU/memory).
*
* Args:
* serviceId: Service ID to update
* opts: Update options:
* - vcpu: New vCPU count (1-8)
*
* Returns: Promise<Object> (update confirmation)
*/
async function updateService(serviceId, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (opts.vcpu) data.vcpu = opts.vcpu;
return makeRequest('PATCH', `/services/${serviceId}`, publicKey, secretKey, data);
}
/**
* Delete/destroy a service.
*
* Args:
* serviceId: Service ID to destroy
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('DELETE', `/services/${serviceId}`, publicKey, secretKey);
}
/**
* Freeze a service (stop container, preserve disk).
*
* Args:
* serviceId: Service ID to freeze
*
* Returns: Promise<Object> (freeze confirmation)
*/
async function freezeService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/freeze`, publicKey, secretKey, {});
}
/**
* Unfreeze a service (restart container).
*
* Args:
* serviceId: Service ID to unfreeze
*
* Returns: Promise<Object> (unfreeze confirmation)
*/
async function unfreezeService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/unfreeze`, publicKey, secretKey, {});
}
/**
* Lock a service to prevent deletion.
*
* Args:
* serviceId: Service ID to lock
*
* Returns: Promise<Object> (lock confirmation)
*/
async function lockService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/lock`, publicKey, secretKey, {});
}
/**
* Unlock a service to allow deletion.
*
* Args:
* serviceId: Service ID to unlock
*
* Returns: Promise<Object> (unlock confirmation)
*/
async function unlockService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/unlock`, publicKey, secretKey, {});
}
/**
* Get service logs.
*
* Args:
* serviceId: Service ID
* all: If true, get all logs; if false, get last ~9000 lines (default: false)
*
* Returns: Promise<Object> (log data)
*/
async function getServiceLogs(serviceId, all = false, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const path = all ? `/services/${serviceId}/logs?all=true` : `/services/${serviceId}/logs`;
return makeRequest('GET', path, publicKey, secretKey);
}
/**
* Get service environment vault status.
*
* Args:
* serviceId: Service ID
*
* Returns: Promise<Object> (vault status with has_vault, count, updated_at)
*/
async function getServiceEnv(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/services/${serviceId}/env`, publicKey, secretKey);
}
/**
* Set service environment vault.
*
* Args:
* serviceId: Service ID
* env: Environment content as string (KEY=VALUE format, newline separated)
* or object { KEY: "value", KEY2: "value2" }
*
* Returns: Promise<Object> (set confirmation)
*/
async function setServiceEnv(serviceId, env, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
// Convert object to KEY=VALUE format if needed
let envContent = env;
if (typeof env === 'object' && !Array.isArray(env)) {
envContent = Object.entries(env)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
}
// Note: This endpoint uses PUT with text/plain body
// The makeRequest function sends JSON, so we need to handle this specially
return makeRequest('PUT', `/services/${serviceId}/env`, publicKey, secretKey, { content: envContent });
}
/**
* Delete service environment vault.
*
* Args:
* serviceId: Service ID
* keys: Optional array of specific keys to delete (deletes all if not specified)
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteServiceEnv(serviceId, keys = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = keys ? { keys } : {};
return makeRequest('DELETE', `/services/${serviceId}/env`, publicKey, secretKey, data);
}
/**
* Export service environment vault.
*
* Args:
* serviceId: Service ID
*
* Returns: Promise<Object> (exported environment data)
*/
async function exportServiceEnv(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/env/export`, publicKey, secretKey, {});
}
/**
* Redeploy a service with new bootstrap script.
*
* Args:
* serviceId: Service ID to redeploy
* bootstrap: Optional new bootstrap script content or URL
*
* Returns: Promise<Object> (redeploy confirmation)
*/
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (bootstrap) {
if (bootstrap.startsWith('http://') || bootstrap.startsWith('https://')) {
data.bootstrap = bootstrap;
} else {
data.bootstrap_content = bootstrap;
}
}
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
}
/**
* Execute a command in a running service container.
*
* Args:
* serviceId: Service ID
* command: Command to execute
* timeout: Optional timeout in milliseconds (default: 30000)
*
* Returns: Promise<Object> (execution result with stdout, stderr, exit_code)
*/
async function executeInService(serviceId, command, timeout = 30000, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('POST', `/services/${serviceId}/execute`, publicKey, secretKey, {
command,
timeout,
});
// If we got a job_id, poll until completion
const jobId = response.job_id;
if (jobId) {
return waitForJob(jobId, publicKey, secretKey);
}
return response;
}
// ============================================================================
// Additional Snapshot Functions
// ============================================================================
/**
* Lock a snapshot to prevent deletion.
*
* Args:
* snapshotId: Snapshot ID to lock
*
* Returns: Promise<Object> (lock confirmation)
*/
async function lockSnapshot(snapshotId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/snapshots/${snapshotId}/lock`, publicKey, secretKey, {});
}
/**
* Unlock a snapshot to allow deletion.
*
* Args:
* snapshotId: Snapshot ID to unlock
*
* Returns: Promise<Object> (unlock confirmation)
*/
async function unlockSnapshot(snapshotId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/snapshots/${snapshotId}/unlock`, publicKey, secretKey, {});
}
/**
* Clone a snapshot to create a new session or service.
*
* Args:
* snapshotId: Snapshot ID to clone
* name: Name for the new resource
* opts: Optional settings:
* - type: "session" or "service" (default: inferred from snapshot)
* - shell: Shell for session clones
* - ports: Ports array for service clones
*
* Returns: Promise<Object> (clone result with new session_id or service_id)
*/
async function cloneSnapshot(snapshotId, name, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (name) data.name = name;
if (opts.type) data.type = opts.type;
if (opts.shell) data.shell = opts.shell;
if (opts.ports) data.ports = opts.ports;
return makeRequest('POST', `/snapshots/${snapshotId}/clone`, publicKey, secretKey, data);
}
// ============================================================================
// Key Validation
// ============================================================================
/**
* Validate API keys.
*
* Returns: Promise<Object> (validation result with valid, tier, expires_at, etc.)
*/
async function validateKeys(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
// Note: This endpoint is on the portal (unsandbox.com), not the API
// For SDK purposes, we'll call the API endpoint if available
return makeRequest('POST', '/keys/validate', publicKey, secretKey, {});
}
// ES Module exports
export {
// Code execution
executeCode,
executeAsync,
getJob,
@ -591,17 +1078,52 @@ export {
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,
getServiceLogs,
getServiceEnv,
setServiceEnv,
deleteServiceEnv,
exportServiceEnv,
redeployService,
executeInService,
// Snapshot management
sessionSnapshot,
serviceSnapshot,
listSnapshots,
restoreSnapshot,
deleteSnapshot,
lockSnapshot,
unlockSnapshot,
cloneSnapshot,
// Key validation
validateKeys,
// Errors
CredentialsError,
TimeoutError,
};
// Default export for convenience
export default {
// Code execution
executeCode,
executeAsync,
getJob,
@ -610,11 +1132,45 @@ export default {
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,
getServiceLogs,
getServiceEnv,
setServiceEnv,
deleteServiceEnv,
exportServiceEnv,
redeployService,
executeInService,
// Snapshot management
sessionSnapshot,
serviceSnapshot,
listSnapshots,
restoreSnapshot,
deleteSnapshot,
lockSnapshot,
unlockSnapshot,
cloneSnapshot,
// Key validation
validateKeys,
// Errors
CredentialsError,
TimeoutError,
};

View file

@ -4,21 +4,24 @@
* unsandbox.com JavaScript SDK (Synchronous/Async)
*
* Library Usage:
* import {
* executeCode,
* executeAsync,
* getJob,
* waitForJob,
* cancelJob,
* listJobs,
* getLanguages,
* detectLanguage,
* sessionSnapshot,
* serviceSnapshot,
* listSnapshots,
* restoreSnapshot,
* deleteSnapshot,
* } from './un.js';
* const {
* // 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,
* getServiceLogs, getServiceEnv, setServiceEnv, deleteServiceEnv,
* exportServiceEnv, redeployService, executeInService,
* // Snapshot management
* sessionSnapshot, serviceSnapshot, listSnapshots, restoreSnapshot,
* deleteSnapshot, lockSnapshot, unlockSnapshot, cloneSnapshot,
* // Key validation
* validateKeys,
* } = require('./un.js');
*
* // Execute code asynchronously (returns Promise)
* const result = await executeCode('python', 'print("hello")', publicKey, secretKey);
@ -201,7 +204,8 @@ function makeRequest(method, path, publicKey, secretKey, data) {
timeout: 120000, // 120 seconds
};
if (method === 'POST' && body) {
// Set Content-Length for methods with body
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
options.headers['Content-Length'] = Buffer.byteLength(body);
}
@ -232,7 +236,8 @@ function makeRequest(method, path, publicKey, secretKey, data) {
reject(new Error('Request timeout'));
});
if (method === 'POST' && body) {
// Write body for methods that support it
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
req.write(body);
}
@ -559,8 +564,491 @@ async function deleteSnapshot(snapshotId, publicKey, secretKey) {
return makeRequest('DELETE', `/snapshots/${snapshotId}`, publicKey, secretKey);
}
// ============================================================================
// Session Management Functions
// ============================================================================
/**
* List all active sessions.
*
* Returns: Promise<Array> (list of session objects)
*/
async function listSessions(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('GET', '/sessions', publicKey, secretKey);
return response.sessions || [];
}
/**
* Get details of a specific session.
*
* Args:
* sessionId: Session ID to retrieve
*
* Returns: Promise<Object> (session details)
*/
async function getSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/sessions/${sessionId}`, publicKey, secretKey);
}
/**
* Create a new interactive session.
*
* Args:
* language: Optional programming language/shell (default: "bash")
* opts: Optional settings:
* - networkMode: "zerotrust" (default) or "semitrusted"
* - shell: Shell to use (e.g., "python3", "bash")
* - multiplexer: "tmux", "screen", or null
* - vcpu: Number of vCPUs (1-8)
* - ttl: Time-to-live in seconds
*
* Returns: Promise<Object> (session info with session_id, container_name)
*/
async function createSession(language, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {
network_mode: opts.networkMode || 'zerotrust',
ttl: opts.ttl || 3600,
};
if (language) data.shell = language;
if (opts.shell) data.shell = opts.shell;
if (opts.multiplexer) data.multiplexer = opts.multiplexer;
if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu;
return makeRequest('POST', '/sessions', publicKey, secretKey, data);
}
/**
* Delete/terminate a session.
*
* Args:
* sessionId: Session ID to terminate
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('DELETE', `/sessions/${sessionId}`, publicKey, secretKey);
}
/**
* Freeze a session (pause execution, preserve state).
*
* Args:
* sessionId: Session ID to freeze
*
* Returns: Promise<Object> (freeze confirmation)
*/
async function freezeSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/freeze`, publicKey, secretKey, {});
}
/**
* Unfreeze a session (resume execution).
*
* Args:
* sessionId: Session ID to unfreeze
*
* Returns: Promise<Object> (unfreeze confirmation)
*/
async function unfreezeSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/unfreeze`, publicKey, secretKey, {});
}
/**
* Boost a session's resources (increase vCPU, memory).
*
* Args:
* sessionId: Session ID to boost
* vcpu: Number of vCPUs (default: 2)
*
* Returns: Promise<Object> (boost confirmation)
*/
async function boostSession(sessionId, vcpu = 2, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/boost`, publicKey, secretKey, { vcpu });
}
/**
* Unboost a session (return to base resources).
*
* Args:
* sessionId: Session ID to unboost
*
* Returns: Promise<Object> (unboost confirmation)
*/
async function unboostSession(sessionId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/unboost`, publicKey, secretKey, {});
}
/**
* Execute a shell command in a session.
*
* Note: This initiates a WebSocket connection for interactive shell.
* For simple command execution, this sends the command via the shell endpoint.
*
* Args:
* sessionId: Session ID
* command: Command to execute
*
* Returns: Promise<Object> (command result)
*/
async function shellSession(sessionId, command, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/sessions/${sessionId}/shell`, publicKey, secretKey, { command });
}
// ============================================================================
// Service Management Functions
// ============================================================================
/**
* List all services.
*
* Returns: Promise<Array> (list of service objects)
*/
async function listServices(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('GET', '/services', publicKey, secretKey);
return response.services || [];
}
/**
* Create a new service (persistent container).
*
* Args:
* name: Service name
* ports: Array of port numbers to expose (e.g., [80, 443])
* bootstrap: Bootstrap script content or URL
* opts: Optional settings:
* - networkMode: "zerotrust" or "semitrusted"
* - vcpu: Number of vCPUs (1-8)
* - domains: Array of custom domains
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
*
* Returns: Promise<Object> (service info with service_id)
*/
async function createService(name, ports, bootstrap, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (name) data.name = name;
if (ports && ports.length > 0) data.ports = ports;
if (bootstrap) {
// If bootstrap starts with http, treat as URL, otherwise as content
if (bootstrap.startsWith('http://') || bootstrap.startsWith('https://')) {
data.bootstrap = bootstrap;
} else {
data.bootstrap_content = bootstrap;
}
}
if (opts.networkMode) data.network_mode = opts.networkMode;
if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu;
if (opts.domains) data.custom_domains = opts.domains;
if (opts.serviceType) data.service_type = opts.serviceType;
return makeRequest('POST', '/services', publicKey, secretKey, data);
}
/**
* Get details of a specific service.
*
* Args:
* serviceId: Service ID to retrieve
*
* Returns: Promise<Object> (service details)
*/
async function getService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/services/${serviceId}`, publicKey, secretKey);
}
/**
* Update a service (resize vCPU/memory).
*
* Args:
* serviceId: Service ID to update
* opts: Update options:
* - vcpu: New vCPU count (1-8)
*
* Returns: Promise<Object> (update confirmation)
*/
async function updateService(serviceId, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (opts.vcpu) data.vcpu = opts.vcpu;
return makeRequest('PATCH', `/services/${serviceId}`, publicKey, secretKey, data);
}
/**
* Delete/destroy a service.
*
* Args:
* serviceId: Service ID to destroy
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('DELETE', `/services/${serviceId}`, publicKey, secretKey);
}
/**
* Freeze a service (stop container, preserve disk).
*
* Args:
* serviceId: Service ID to freeze
*
* Returns: Promise<Object> (freeze confirmation)
*/
async function freezeService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/freeze`, publicKey, secretKey, {});
}
/**
* Unfreeze a service (restart container).
*
* Args:
* serviceId: Service ID to unfreeze
*
* Returns: Promise<Object> (unfreeze confirmation)
*/
async function unfreezeService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/unfreeze`, publicKey, secretKey, {});
}
/**
* Lock a service to prevent deletion.
*
* Args:
* serviceId: Service ID to lock
*
* Returns: Promise<Object> (lock confirmation)
*/
async function lockService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/lock`, publicKey, secretKey, {});
}
/**
* Unlock a service to allow deletion.
*
* Args:
* serviceId: Service ID to unlock
*
* Returns: Promise<Object> (unlock confirmation)
*/
async function unlockService(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/unlock`, publicKey, secretKey, {});
}
/**
* Get service logs.
*
* Args:
* serviceId: Service ID
* all: If true, get all logs; if false, get last ~9000 lines (default: false)
*
* Returns: Promise<Object> (log data)
*/
async function getServiceLogs(serviceId, all = false, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const path = all ? `/services/${serviceId}/logs?all=true` : `/services/${serviceId}/logs`;
return makeRequest('GET', path, publicKey, secretKey);
}
/**
* Get service environment vault status.
*
* Args:
* serviceId: Service ID
*
* Returns: Promise<Object> (vault status with has_vault, count, updated_at)
*/
async function getServiceEnv(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('GET', `/services/${serviceId}/env`, publicKey, secretKey);
}
/**
* Set service environment vault.
*
* Args:
* serviceId: Service ID
* env: Environment content as string (KEY=VALUE format, newline separated)
* or object { KEY: "value", KEY2: "value2" }
*
* Returns: Promise<Object> (set confirmation)
*/
async function setServiceEnv(serviceId, env, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
// Convert object to KEY=VALUE format if needed
let envContent = env;
if (typeof env === 'object' && !Array.isArray(env)) {
envContent = Object.entries(env)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
}
// Note: This endpoint uses PUT with text/plain body
// The makeRequest function sends JSON, so we need to handle this specially
return makeRequest('PUT', `/services/${serviceId}/env`, publicKey, secretKey, { content: envContent });
}
/**
* Delete service environment vault.
*
* Args:
* serviceId: Service ID
* keys: Optional array of specific keys to delete (deletes all if not specified)
*
* Returns: Promise<Object> (deletion confirmation)
*/
async function deleteServiceEnv(serviceId, keys = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = keys ? { keys } : {};
return makeRequest('DELETE', `/services/${serviceId}/env`, publicKey, secretKey, data);
}
/**
* Export service environment vault.
*
* Args:
* serviceId: Service ID
*
* Returns: Promise<Object> (exported environment data)
*/
async function exportServiceEnv(serviceId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/services/${serviceId}/env/export`, publicKey, secretKey, {});
}
/**
* Redeploy a service with new bootstrap script.
*
* Args:
* serviceId: Service ID to redeploy
* bootstrap: Optional new bootstrap script content or URL
*
* Returns: Promise<Object> (redeploy confirmation)
*/
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (bootstrap) {
if (bootstrap.startsWith('http://') || bootstrap.startsWith('https://')) {
data.bootstrap = bootstrap;
} else {
data.bootstrap_content = bootstrap;
}
}
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
}
/**
* Execute a command in a running service container.
*
* Args:
* serviceId: Service ID
* command: Command to execute
* timeout: Optional timeout in milliseconds (default: 30000)
*
* Returns: Promise<Object> (execution result with stdout, stderr, exit_code)
*/
async function executeInService(serviceId, command, timeout = 30000, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const response = await makeRequest('POST', `/services/${serviceId}/execute`, publicKey, secretKey, {
command,
timeout,
});
// If we got a job_id, poll until completion
const jobId = response.job_id;
if (jobId) {
return waitForJob(jobId, publicKey, secretKey);
}
return response;
}
// ============================================================================
// Additional Snapshot Functions
// ============================================================================
/**
* Lock a snapshot to prevent deletion.
*
* Args:
* snapshotId: Snapshot ID to lock
*
* Returns: Promise<Object> (lock confirmation)
*/
async function lockSnapshot(snapshotId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/snapshots/${snapshotId}/lock`, publicKey, secretKey, {});
}
/**
* Unlock a snapshot to allow deletion.
*
* Args:
* snapshotId: Snapshot ID to unlock
*
* Returns: Promise<Object> (unlock confirmation)
*/
async function unlockSnapshot(snapshotId, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
return makeRequest('POST', `/snapshots/${snapshotId}/unlock`, publicKey, secretKey, {});
}
/**
* Clone a snapshot to create a new session or service.
*
* Args:
* snapshotId: Snapshot ID to clone
* name: Name for the new resource
* opts: Optional settings:
* - type: "session" or "service" (default: inferred from snapshot)
* - shell: Shell for session clones
* - ports: Ports array for service clones
*
* Returns: Promise<Object> (clone result with new session_id or service_id)
*/
async function cloneSnapshot(snapshotId, name, opts = {}, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (name) data.name = name;
if (opts.type) data.type = opts.type;
if (opts.shell) data.shell = opts.shell;
if (opts.ports) data.ports = opts.ports;
return makeRequest('POST', `/snapshots/${snapshotId}/clone`, publicKey, secretKey, data);
}
// ============================================================================
// Key Validation
// ============================================================================
/**
* Validate API keys.
*
* Returns: Promise<Object> (validation result with valid, tier, expires_at, etc.)
*/
async function validateKeys(publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
// Note: This endpoint is on the portal (unsandbox.com), not the API
// For SDK purposes, we'll call the API endpoint if available
return makeRequest('POST', '/keys/validate', publicKey, secretKey, {});
}
// Export all functions
module.exports = {
// Code execution
executeCode,
executeAsync,
getJob,
@ -569,10 +1057,44 @@ module.exports = {
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,
getServiceLogs,
getServiceEnv,
setServiceEnv,
deleteServiceEnv,
exportServiceEnv,
redeployService,
executeInService,
// Snapshot management
sessionSnapshot,
serviceSnapshot,
listSnapshots,
restoreSnapshot,
deleteSnapshot,
lockSnapshot,
unlockSnapshot,
cloneSnapshot,
// Key validation
validateKeys,
// Errors
CredentialsError,
};

View file

@ -466,6 +466,545 @@ class UnsandboxAsync {
return $this->makeRequest('DELETE', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
}
/**
* Lock a snapshot to prevent deletion.
*
* @param string $snapshotId Snapshot ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with lock confirmation
*/
public function lockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a snapshot to allow deletion.
*
* @param string $snapshotId Snapshot ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with unlock confirmation
*/
public function unlockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/unlock", $publicKey, $secretKey, []);
}
/**
* Clone a snapshot to create a new session or service.
*
* @param string $snapshotId Snapshot ID to clone
* @param string|null $name Optional name for the new resource
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'type' (session|service), 'shell', 'ports'
* @return PromiseInterface Resolves to response array with cloned resource info
*/
public function cloneSnapshot(
string $snapshotId,
?string $name = null,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [];
if ($name !== null) {
$data['name'] = $name;
}
if (isset($opts['type'])) {
$data['type'] = $opts['type'];
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['ports'])) {
$data['ports'] = $opts['ports'];
}
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/clone", $publicKey, $secretKey, $data);
}
// =========================================================================
// Session Methods
// =========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to list of session arrays
*/
public function listSessions(?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', '/sessions', $publicKey, $secretKey)->then(function (array $response) {
return $response['sessions'] ?? [];
});
}
/**
* Get details of a specific session.
*
* @param string $sessionId Session ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to session details
*/
public function getSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Create a new interactive session.
*
* @param string $language Programming language or shell (e.g., "bash", "python3")
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'ttl', 'shell', 'multiplexer', 'vcpu'
* @return PromiseInterface Resolves to session info including session_id and container_name
*/
public function createSession(
string $language,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [
'network_mode' => $opts['network_mode'] ?? 'zerotrust',
'ttl' => $opts['ttl'] ?? 3600,
];
if (!empty($language)) {
$data['shell'] = $language;
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['multiplexer'])) {
$data['multiplexer'] = $opts['multiplexer'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', '/sessions', $publicKey, $secretKey, $data);
}
/**
* Delete (terminate) a session.
*
* @param string $sessionId Session ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with deletion confirmation
*/
public function deleteSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Freeze a session to pause execution and reduce resource usage.
*
* @param string $sessionId Session ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with freeze confirmation
*/
public function freezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a session to resume execution.
*
* @param string $sessionId Session ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with unfreeze confirmation
*/
public function unfreezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Boost a session's resources (increase vCPU, memory is derived: vcpu * 2048MB).
*
* @param string $sessionId Session ID to boost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $vcpu Number of vCPUs (default: 2)
* @return PromiseInterface Resolves to response array with boost confirmation
*/
public function boostSession(
string $sessionId,
?string $publicKey = null,
?string $secretKey = null,
int $vcpu = 2
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/boost", $publicKey, $secretKey, ['vcpu' => $vcpu]);
}
/**
* Remove boost from a session (return to base resources).
*
* @param string $sessionId Session ID to unboost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with unboost confirmation
*/
public function unboostSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unboost", $publicKey, $secretKey, []);
}
/**
* Execute a shell command in an active session.
*
* Note: This is for one-shot commands. For interactive sessions, use WebSocket connection.
*
* @param string $sessionId Session ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with command output
*/
public function shellSession(
string $sessionId,
string $command,
?string $publicKey = null,
?string $secretKey = null
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/shell", $publicKey, $secretKey, ['command' => $command]);
}
// =========================================================================
// Service Methods
// =========================================================================
/**
* List all services for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to list of service arrays
*/
public function listServices(?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', '/services', $publicKey, $secretKey)->then(function (array $response) {
return $response['services'] ?? [];
});
}
/**
* Create a new persistent service.
*
* @param string $name Service name
* @param array|string $ports Port(s) to expose (array of ints or comma-separated string)
* @param string $bootstrap Bootstrap command or URL
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'vcpu', 'service_type', 'custom_domains', 'bootstrap_content', 'input_files'
* @return PromiseInterface Resolves to service info including service_id
*/
public function createService(
string $name,
$ports,
string $bootstrap,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
// Convert ports to array if string
if (is_string($ports)) {
$ports = array_map('intval', explode(',', $ports));
}
$data = [
'name' => $name,
'ports' => $ports,
'bootstrap' => $bootstrap,
];
if (isset($opts['network_mode'])) {
$data['network_mode'] = $opts['network_mode'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['service_type'])) {
$data['service_type'] = $opts['service_type'];
}
if (isset($opts['custom_domains'])) {
$data['custom_domains'] = $opts['custom_domains'];
}
if (isset($opts['bootstrap_content'])) {
$data['bootstrap_content'] = $opts['bootstrap_content'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', '/services', $publicKey, $secretKey, $data);
}
/**
* Get details of a specific service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to service details
*/
public function getService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Update a service (e.g., resize vCPU).
*
* @param string $serviceId Service ID
* @param array $opts Update parameters: 'vcpu', 'name', etc.
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to updated service details
*/
public function updateService(
string $serviceId,
array $opts,
?string $publicKey = null,
?string $secretKey = null
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, $opts);
}
/**
* Delete (destroy) a service.
*
* @param string $serviceId Service ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with deletion confirmation
*/
public function deleteService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Freeze a service to pause execution and reduce resource usage.
*
* @param string $serviceId Service ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with freeze confirmation
*/
public function freezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a service to resume execution.
*
* @param string $serviceId Service ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with unfreeze confirmation
*/
public function unfreezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Lock a service to prevent deletion.
*
* @param string $serviceId Service ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with lock confirmation
*/
public function lockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a service to allow deletion.
*
* @param string $serviceId Service ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with unlock confirmation
*/
public function unlockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/unlock", $publicKey, $secretKey, []);
}
/**
* Get bootstrap logs for a service.
*
* @param string $serviceId Service ID
* @param bool $all If true, get all logs; if false, get last 9000 lines
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with log content
*/
public function getServiceLogs(
string $serviceId,
bool $all = false,
?string $publicKey = null,
?string $secretKey = null
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = "/services/{$serviceId}/logs" . ($all ? '?all=true' : '');
return $this->makeRequest('GET', $path, $publicKey, $secretKey);
}
/**
* Get environment vault status for a service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with vault status (has_vault, count, updated_at)
*/
public function getServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Set environment vault for a service.
*
* @param string $serviceId Service ID
* @param string $env Environment content in .env format (KEY=VALUE\nKEY2=VALUE2)
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with update confirmation
*/
public function setServiceEnv(
string $serviceId,
string $env,
?string $publicKey = null,
?string $secretKey = null
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestRaw('PUT', "/services/{$serviceId}/env", $publicKey, $secretKey, $env, 'text/plain');
}
/**
* Delete environment vault for a service.
*
* @param string $serviceId Service ID
* @param array|null $keys Optional specific keys to delete; if null, deletes entire vault
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with deletion confirmation
*/
public function deleteServiceEnv(
string $serviceId,
?array $keys = null,
?string $publicKey = null,
?string $secretKey = null
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Export environment vault for a service (returns .env format).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with env content
*/
public function exportServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/env/export", $publicKey, $secretKey, []);
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with redeploy confirmation
*/
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
}
/**
* Execute a command in a running service container.
*
* @param string $serviceId Service ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $timeout Timeout in milliseconds (default: 30000)
* @return PromiseInterface Resolves to response array with command output (stdout, stderr, exit_code)
*/
public function executeInService(
string $serviceId,
string $command,
?string $publicKey = null,
?string $secretKey = null,
int $timeout = 30000
): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/execute", $publicKey, $secretKey, [
'command' => $command,
'timeout' => $timeout,
])->then(function (array $response) use ($publicKey, $secretKey) {
// If we got a job_id, poll until completion
$jobId = $response['job_id'] ?? null;
if ($jobId) {
return $this->waitForJob($jobId, $publicKey, $secretKey);
}
return $response;
});
}
// =========================================================================
// Key Validation
// =========================================================================
/**
* Validate API keys.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return PromiseInterface Resolves to response array with validation result
*/
public function validateKeys(?string $publicKey = null, ?string $secretKey = null): PromiseInterface {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', '/keys/validate', $publicKey, $secretKey, []);
}
/**
* Get path to ~/.unsandbox directory, creating if necessary.
*
@ -660,6 +1199,58 @@ class UnsandboxAsync {
);
}
/**
* Make an authenticated HTTP request with raw body content (non-JSON) asynchronously.
*
* @param string $method HTTP method (PUT, POST, etc.)
* @param string $path API endpoint path
* @param string $publicKey API public key
* @param string $secretKey API secret key
* @param string $body Raw request body
* @param string $contentType Content type header (e.g., 'text/plain')
* @return PromiseInterface Resolves to decoded JSON response array
*/
private function makeRequestRaw(string $method, string $path, string $publicKey, string $secretKey, string $body, string $contentType = 'text/plain'): PromiseInterface {
$timestamp = time();
$signature = $this->signRequest($secretKey, $timestamp, $method, $path, $body);
$headers = [
'Authorization' => 'Bearer ' . $publicKey,
'X-Timestamp' => (string)$timestamp,
'X-Signature' => $signature,
'Content-Type' => $contentType,
];
$options = [
'headers' => $headers,
'body' => $body,
];
return $this->httpClient->requestAsync($method, $path, $options)->then(
function ($response) {
$body = (string)$response->getBody();
$decoded = json_decode($body, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new AsyncApiException("Invalid JSON response: " . json_last_error_msg());
}
return $decoded;
},
function ($exception) {
if ($exception instanceof RequestException) {
$response = $exception->getResponse();
if ($response !== null) {
$body = (string)$response->getBody();
$decoded = json_decode($body, true);
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP " . $response->getStatusCode();
throw new AsyncApiException($errorMessage, $response->getStatusCode(), $decoded, $exception);
}
}
throw new AsyncApiException($exception->getMessage(), 0, null, $exception);
}
);
}
/**
* Get path to languages cache file.
*

View file

@ -442,6 +442,602 @@ class Unsandbox {
return $this->makeRequest('DELETE', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
}
/**
* Lock a snapshot to prevent deletion.
*
* @param string $snapshotId Snapshot ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with lock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function lockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a snapshot to allow deletion.
*
* @param string $snapshotId Snapshot ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unlock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unlockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/unlock", $publicKey, $secretKey, []);
}
/**
* Clone a snapshot to create a new session or service.
*
* @param string $snapshotId Snapshot ID to clone
* @param string|null $name Optional name for the new resource
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'type' (session|service), 'shell', 'ports'
* @return array Response array with cloned resource info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function cloneSnapshot(
string $snapshotId,
?string $name = null,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [];
if ($name !== null) {
$data['name'] = $name;
}
if (isset($opts['type'])) {
$data['type'] = $opts['type'];
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['ports'])) {
$data['ports'] = $opts['ports'];
}
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/clone", $publicKey, $secretKey, $data);
}
// =========================================================================
// Session Methods
// =========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of session arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listSessions(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/sessions', $publicKey, $secretKey);
return $response['sessions'] ?? [];
}
/**
* Get details of a specific session.
*
* @param string $sessionId Session ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Session details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Create a new interactive session.
*
* @param string $language Programming language or shell (e.g., "bash", "python3")
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'ttl', 'shell', 'multiplexer', 'vcpu'
* @return array Session info including session_id and container_name
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function createSession(
string $language,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [
'network_mode' => $opts['network_mode'] ?? 'zerotrust',
'ttl' => $opts['ttl'] ?? 3600,
];
if (!empty($language)) {
$data['shell'] = $language;
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['multiplexer'])) {
$data['multiplexer'] = $opts['multiplexer'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', '/sessions', $publicKey, $secretKey, $data);
}
/**
* Delete (terminate) a session.
*
* @param string $sessionId Session ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Freeze a session to pause execution and reduce resource usage.
*
* @param string $sessionId Session ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with freeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function freezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a session to resume execution.
*
* @param string $sessionId Session ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unfreeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unfreezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Boost a session's resources (increase vCPU, memory is derived: vcpu * 2048MB).
*
* @param string $sessionId Session ID to boost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $vcpu Number of vCPUs (default: 2)
* @return array Response array with boost confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function boostSession(
string $sessionId,
?string $publicKey = null,
?string $secretKey = null,
int $vcpu = 2
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/boost", $publicKey, $secretKey, ['vcpu' => $vcpu]);
}
/**
* Remove boost from a session (return to base resources).
*
* @param string $sessionId Session ID to unboost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unboost confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unboostSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unboost", $publicKey, $secretKey, []);
}
/**
* Execute a shell command in an active session.
*
* Note: This is for one-shot commands. For interactive sessions, use WebSocket connection.
*
* @param string $sessionId Session ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with command output
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function shellSession(
string $sessionId,
string $command,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/shell", $publicKey, $secretKey, ['command' => $command]);
}
// =========================================================================
// Service Methods
// =========================================================================
/**
* List all services for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of service arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listServices(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/services', $publicKey, $secretKey);
return $response['services'] ?? [];
}
/**
* Create a new persistent service.
*
* @param string $name Service name
* @param array|string $ports Port(s) to expose (array of ints or comma-separated string)
* @param string $bootstrap Bootstrap command or URL
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'vcpu', 'service_type', 'custom_domains', 'bootstrap_content', 'input_files'
* @return array Service info including service_id
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function createService(
string $name,
$ports,
string $bootstrap,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
// Convert ports to array if string
if (is_string($ports)) {
$ports = array_map('intval', explode(',', $ports));
}
$data = [
'name' => $name,
'ports' => $ports,
'bootstrap' => $bootstrap,
];
if (isset($opts['network_mode'])) {
$data['network_mode'] = $opts['network_mode'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['service_type'])) {
$data['service_type'] = $opts['service_type'];
}
if (isset($opts['custom_domains'])) {
$data['custom_domains'] = $opts['custom_domains'];
}
if (isset($opts['bootstrap_content'])) {
$data['bootstrap_content'] = $opts['bootstrap_content'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', '/services', $publicKey, $secretKey, $data);
}
/**
* Get details of a specific service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Service details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Update a service (e.g., resize vCPU).
*
* @param string $serviceId Service ID
* @param array $opts Update parameters: 'vcpu', 'name', etc.
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Updated service details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function updateService(
string $serviceId,
array $opts,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, $opts);
}
/**
* Delete (destroy) a service.
*
* @param string $serviceId Service ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Freeze a service to pause execution and reduce resource usage.
*
* @param string $serviceId Service ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with freeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function freezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a service to resume execution.
*
* @param string $serviceId Service ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unfreeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unfreezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Lock a service to prevent deletion.
*
* @param string $serviceId Service ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with lock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function lockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a service to allow deletion.
*
* @param string $serviceId Service ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unlock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unlockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/unlock", $publicKey, $secretKey, []);
}
/**
* Get bootstrap logs for a service.
*
* @param string $serviceId Service ID
* @param bool $all If true, get all logs; if false, get last 9000 lines
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with log content
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getServiceLogs(
string $serviceId,
bool $all = false,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = "/services/{$serviceId}/logs" . ($all ? '?all=true' : '');
return $this->makeRequest('GET', $path, $publicKey, $secretKey);
}
/**
* Get environment vault status for a service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with vault status (has_vault, count, updated_at)
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Set environment vault for a service.
*
* @param string $serviceId Service ID
* @param string $env Environment content in .env format (KEY=VALUE\nKEY2=VALUE2)
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with update confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function setServiceEnv(
string $serviceId,
string $env,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestRaw('PUT', "/services/{$serviceId}/env", $publicKey, $secretKey, $env, 'text/plain');
}
/**
* Delete environment vault for a service.
*
* @param string $serviceId Service ID
* @param array|null $keys Optional specific keys to delete; if null, deletes entire vault
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteServiceEnv(
string $serviceId,
?array $keys = null,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Export environment vault for a service (returns .env format).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with env content
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function exportServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/env/export", $publicKey, $secretKey, []);
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with redeploy confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
}
/**
* Execute a command in a running service container.
*
* @param string $serviceId Service ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $timeout Timeout in milliseconds (default: 30000)
* @return array Response array with command output (stdout, stderr, exit_code)
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function executeInService(
string $serviceId,
string $command,
?string $publicKey = null,
?string $secretKey = null,
int $timeout = 30000
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('POST', "/services/{$serviceId}/execute", $publicKey, $secretKey, [
'command' => $command,
'timeout' => $timeout,
]);
// If we got a job_id, poll until completion
$jobId = $response['job_id'] ?? null;
if ($jobId) {
return $this->waitForJob($jobId, $publicKey, $secretKey);
}
return $response;
}
// =========================================================================
// Key Validation
// =========================================================================
/**
* Validate API keys.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with validation result
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function validateKeys(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', '/keys/validate', $publicKey, $secretKey, []);
}
/**
* Get path to ~/.unsandbox directory, creating if necessary.
*
@ -616,6 +1212,14 @@ class Unsandbox {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PATCH':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
@ -647,6 +1251,61 @@ class Unsandbox {
return $decoded;
}
/**
* Make an authenticated HTTP request with raw body content (non-JSON).
*
* @param string $method HTTP method (PUT, POST, etc.)
* @param string $path API endpoint path
* @param string $publicKey API public key
* @param string $secretKey API secret key
* @param string $body Raw request body
* @param string $contentType Content type header (e.g., 'text/plain')
* @return array Decoded JSON response
* @throws ApiException On network errors or non-2xx response
*/
private function makeRequestRaw(string $method, string $path, string $publicKey, string $secretKey, string $body, string $contentType = 'text/plain'): array {
$url = self::API_BASE . $path;
$timestamp = time();
$signature = $this->signRequest($secretKey, $timestamp, $method, $path, $body);
$headers = [
'Authorization: Bearer ' . $publicKey,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: ' . $contentType,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new ApiException("cURL error: {$error}");
}
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new ApiException("Invalid JSON response: " . json_last_error_msg());
}
if ($httpCode >= 400) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
throw new ApiException($errorMessage, $httpCode, $decoded);
}
return $decoded;
}
/**
* Get path to languages cache file.
*

View file

@ -6,6 +6,7 @@ unsandbox.com Python SDK (Asynchronous)
Library Usage:
import asyncio
from un_async import (
# Execution
execute_code,
execute_async,
get_job,
@ -14,11 +15,44 @@ Library Usage:
list_jobs,
get_languages,
detect_language,
# Sessions
list_sessions,
get_session,
create_session,
delete_session,
freeze_session,
unfreeze_session,
boost_session,
unboost_session,
shell_session,
# Services
list_services,
create_service,
get_service,
update_service,
delete_service,
freeze_service,
unfreeze_service,
lock_service,
unlock_service,
get_service_logs,
get_service_env,
set_service_env,
delete_service_env,
export_service_env,
redeploy_service,
execute_in_service,
# Snapshots
session_snapshot,
service_snapshot,
list_snapshots,
restore_snapshot,
delete_snapshot,
lock_snapshot,
unlock_snapshot,
clone_snapshot,
# Key validation
validate_keys,
)
async def main():
@ -214,6 +248,10 @@ async def _make_request(
async with session.post(url, headers=headers, json=data, timeout=aiohttp.ClientTimeout(total=120)) as resp:
resp.raise_for_status()
return await resp.json()
elif method == "PATCH":
async with session.patch(url, headers=headers, json=data, timeout=aiohttp.ClientTimeout(total=120)) as resp:
resp.raise_for_status()
return await resp.json()
elif method == "DELETE":
async with session.delete(url, headers=headers, timeout=aiohttp.ClientTimeout(total=120)) as resp:
resp.raise_for_status()
@ -702,3 +740,913 @@ async def delete_snapshot(
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("DELETE", f"/snapshots/{snapshot_id}", public_key, secret_key)
# =============================================================================
# Session Management Functions
# =============================================================================
async def list_sessions(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
List all sessions for the authenticated account.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
List of session dicts containing id, container_name, status, etc.
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
response = await _make_request("GET", "/sessions", public_key, secret_key)
return response.get("sessions", [])
async def get_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get details of a specific session.
Args:
session_id: Session ID to get details for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Session details dict
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("GET", f"/sessions/{session_id}", public_key, secret_key)
async def create_session(
language: Optional[str] = None,
network_mode: str = "zerotrust",
ttl: int = 3600,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
shell: Optional[str] = None,
multiplexer: Optional[str] = None,
vcpu: int = 1,
) -> Dict[str, Any]:
"""
Create a new interactive session.
Args:
language: Optional programming language for the session
network_mode: Network mode - "zerotrust" (default, no network) or "semitrusted" (with network)
ttl: Time to live in seconds (default 3600)
public_key: Optional API key
secret_key: Optional API secret
shell: Optional shell to use (e.g., "bash", "python3")
multiplexer: Optional terminal multiplexer ("tmux" or "screen")
vcpu: Number of vCPUs (1-8, default 1)
Returns:
Response dict containing session_id, container_name, etc.
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {
"network_mode": network_mode,
"ttl": ttl,
}
if language:
data["language"] = language
if shell:
data["shell"] = shell
if multiplexer:
data["multiplexer"] = multiplexer
if vcpu > 1:
data["vcpu"] = vcpu
return await _make_request("POST", "/sessions", public_key, secret_key, data)
async def delete_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete/terminate a session.
Args:
session_id: Session ID to delete
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation and optional artifacts
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("DELETE", f"/sessions/{session_id}", public_key, secret_key)
async def freeze_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Freeze a session (pause execution, preserve state).
Args:
session_id: Session ID to freeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with freeze confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/sessions/{session_id}/freeze", public_key, secret_key, {})
async def unfreeze_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unfreeze a session (resume execution).
Args:
session_id: Session ID to unfreeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unfreeze confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/sessions/{session_id}/unfreeze", public_key, secret_key, {})
async def boost_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Boost a session (increase resources).
Args:
session_id: Session ID to boost
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with boost confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/sessions/{session_id}/boost", public_key, secret_key, {})
async def unboost_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unboost a session (return to normal resources).
Args:
session_id: Session ID to unboost
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unboost confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/sessions/{session_id}/unboost", public_key, secret_key, {})
async def shell_session(
session_id: str,
command: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute a shell command in a session.
Note: This is for one-off commands. For interactive shell access,
use WebSocket connection to /sessions/{id}/shell.
Args:
session_id: Session ID to execute command in
command: Shell command to execute
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with command output
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request(
"POST",
f"/sessions/{session_id}/shell",
public_key,
secret_key,
{"command": command},
)
# =============================================================================
# Service Management Functions
# =============================================================================
async def list_services(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
List all services for the authenticated account.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
List of service dicts containing id, name, status, ports, etc.
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
response = await _make_request("GET", "/services", public_key, secret_key)
return response.get("services", [])
async def create_service(
name: str,
ports: List[int],
bootstrap: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
network_mode: str = "semitrusted",
custom_domains: Optional[List[str]] = None,
vcpu: int = 1,
service_type: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create a new persistent service.
Args:
name: Service name (used for subdomain: name.on.unsandbox.com)
ports: List of ports to expose (e.g., [80, 443])
bootstrap: Bootstrap script content, URL, or inline command
public_key: Optional API key
secret_key: Optional API secret
network_mode: Network mode (default "semitrusted" for services)
custom_domains: Optional list of custom domain names
vcpu: Number of vCPUs (1-8, default 1)
service_type: Optional service type for SRV records (e.g., "minecraft")
Returns:
Response dict containing service_id, etc.
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {
"name": name,
"ports": ports,
"network_mode": network_mode,
}
if bootstrap:
# Check if it looks like a URL
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
data["bootstrap"] = bootstrap
else:
data["bootstrap_content"] = bootstrap
if custom_domains:
data["custom_domains"] = custom_domains
if vcpu > 1:
data["vcpu"] = vcpu
if service_type:
data["service_type"] = service_type
return await _make_request("POST", "/services", public_key, secret_key, data)
async def get_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get details of a specific service.
Args:
service_id: Service ID to get details for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Service details dict
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("GET", f"/services/{service_id}", public_key, secret_key)
async def update_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
vcpu: Optional[int] = None,
**kwargs,
) -> Dict[str, Any]:
"""
Update a service (e.g., resize vCPU/memory).
Args:
service_id: Service ID to update
public_key: Optional API key
secret_key: Optional API secret
vcpu: Optional new vCPU count (1-8)
**kwargs: Additional fields to update
Returns:
Response dict with update confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {}
if vcpu is not None:
data["vcpu"] = vcpu
data.update(kwargs)
return await _make_request("PATCH", f"/services/{service_id}", public_key, secret_key, data)
async def delete_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete/destroy a service.
Args:
service_id: Service ID to delete
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("DELETE", f"/services/{service_id}", public_key, secret_key)
async def freeze_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Freeze a service (pause execution, preserve state).
Args:
service_id: Service ID to freeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with freeze confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/services/{service_id}/freeze", public_key, secret_key, {})
async def unfreeze_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unfreeze a service (resume execution).
Args:
service_id: Service ID to unfreeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unfreeze confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/services/{service_id}/unfreeze", public_key, secret_key, {})
async def lock_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Lock a service to prevent accidental deletion.
Args:
service_id: Service ID to lock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with lock confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/services/{service_id}/lock", public_key, secret_key, {})
async def unlock_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unlock a service to allow deletion.
Args:
service_id: Service ID to unlock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unlock confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {})
async def get_service_logs(
service_id: str,
all_logs: bool = False,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get bootstrap/runtime logs for a service.
Args:
service_id: Service ID to get logs for
all_logs: If True, get all logs; if False, get last ~9000 lines (tail)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict containing "log" field with log content
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
path = f"/services/{service_id}/logs"
if all_logs:
path += "?all=true"
return await _make_request("GET", path, public_key, secret_key)
async def get_service_env(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get environment vault status for a service.
Returns metadata about the vault (has_vault, count, updated_at)
but NOT the actual secrets. Use export_service_env to retrieve secrets.
Args:
service_id: Service ID to get env status for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with has_vault, count, updated_at fields
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("GET", f"/services/{service_id}/env", public_key, secret_key)
async def set_service_env(
service_id: str,
env_dict: Dict[str, str],
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Set environment variables for a service.
Replaces the entire environment vault with the provided variables.
Variables are encrypted at rest and injected into the container.
Args:
service_id: Service ID to set env for
env_dict: Dictionary of environment variables (KEY: VALUE)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with count of variables set
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
# Convert dict to .env format for the API
env_content = "\n".join(f"{k}={v}" for k, v in env_dict.items())
# Note: This endpoint expects text/plain body, but we'll send as JSON
# and let the API handle conversion
return await _make_request(
"POST",
f"/services/{service_id}/env",
public_key,
secret_key,
{"env": env_content},
)
async def delete_service_env(
service_id: str,
keys: Optional[List[str]] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete environment vault or specific keys from a service.
Args:
service_id: Service ID to delete env from
keys: Optional list of specific keys to delete; if None, deletes entire vault
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
path = f"/services/{service_id}/env"
# If specific keys provided, could add as query params (API dependent)
return await _make_request("DELETE", path, public_key, secret_key)
async def export_service_env(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Export environment vault secrets for a service.
Requires HMAC authentication to prove ownership.
Returns the actual secret values in .env format.
Args:
service_id: Service ID to export env from
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict containing "env" field with KEY=VALUE content
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/services/{service_id}/env/export", public_key, secret_key, {})
async def redeploy_service(
service_id: str,
bootstrap: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Redeploy a service (re-run bootstrap script).
Bootstrap scripts should be idempotent for proper upgrade behavior.
Args:
service_id: Service ID to redeploy
bootstrap: Optional new bootstrap script/URL (uses existing if not provided)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with redeploy confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {}
if bootstrap:
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
data["bootstrap"] = bootstrap
else:
data["bootstrap_content"] = bootstrap
return await _make_request("POST", f"/services/{service_id}/redeploy", public_key, secret_key, data)
async def execute_in_service(
service_id: str,
command: str,
timeout: int = 30000,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute a command in a running service container.
Uses async job polling for long-running commands.
Args:
service_id: Service ID to execute command in
command: Shell command to execute
timeout: Command timeout in milliseconds (default 30000)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with job_id for async polling, or direct result
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request(
"POST",
f"/services/{service_id}/execute",
public_key,
secret_key,
{"command": command, "timeout": timeout},
)
# =============================================================================
# Additional Snapshot Functions
# =============================================================================
async def lock_snapshot(
snapshot_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Lock a snapshot to prevent accidental deletion.
Args:
snapshot_id: Snapshot ID to lock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with lock confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/snapshots/{snapshot_id}/lock", public_key, secret_key, {})
async def unlock_snapshot(
snapshot_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unlock a snapshot to allow deletion.
Args:
snapshot_id: Snapshot ID to unlock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unlock confirmation
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return await _make_request("POST", f"/snapshots/{snapshot_id}/unlock", public_key, secret_key, {})
async def clone_snapshot(
snapshot_id: str,
clone_type: str = "session",
name: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
shell: Optional[str] = None,
ports: Optional[List[int]] = None,
) -> Dict[str, Any]:
"""
Clone a snapshot to create a new session or service.
Args:
snapshot_id: Snapshot ID to clone from
clone_type: Type of resource to create ("session" or "service")
name: Optional name for the new resource
public_key: Optional API key
secret_key: Optional API secret
shell: Optional shell for session clones
ports: Optional ports list for service clones
Returns:
Response dict containing session_id or service_id
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {"type": clone_type}
if name:
data["name"] = name
if shell:
data["shell"] = shell
if ports:
data["ports"] = ports
return await _make_request("POST", f"/snapshots/{snapshot_id}/clone", public_key, secret_key, data)
# =============================================================================
# Key Validation
# =============================================================================
PORTAL_BASE = "https://unsandbox.com"
async def validate_keys(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Validate API keys against the portal.
Checks if the keys are valid, not expired, and not suspended.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with validation result:
- valid: True if keys are valid
- tier: Account tier level
- expires_at: Expiration timestamp (if applicable)
- reason: Reason for invalid status (if applicable)
Raises:
aiohttp.ClientError: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
url = f"{PORTAL_BASE}/keys/validate"
timestamp = int(time.time())
body = ""
signature = _sign_request(secret_key, timestamp, "POST", "/keys/validate", body)
headers = {
"Authorization": f"Bearer {public_key}",
"X-Timestamp": str(timestamp),
"X-Signature": signature,
"Content-Type": "application/json",
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=30)) as resp:
resp.raise_for_status()
return await resp.json()

View file

@ -5,6 +5,7 @@ unsandbox.com Python SDK (Synchronous)
Library Usage:
from un import (
# Execution
execute_code,
execute_async,
get_job,
@ -13,11 +14,44 @@ Library Usage:
list_jobs,
get_languages,
detect_language,
# Sessions
list_sessions,
get_session,
create_session,
delete_session,
freeze_session,
unfreeze_session,
boost_session,
unboost_session,
shell_session,
# Services
list_services,
create_service,
get_service,
update_service,
delete_service,
freeze_service,
unfreeze_service,
lock_service,
unlock_service,
get_service_logs,
get_service_env,
set_service_env,
delete_service_env,
export_service_env,
redeploy_service,
execute_in_service,
# Snapshots
session_snapshot,
service_snapshot,
list_snapshots,
restore_snapshot,
delete_snapshot,
lock_snapshot,
unlock_snapshot,
clone_snapshot,
# Key validation
validate_keys,
)
# Execute code synchronously
@ -221,6 +255,8 @@ def _make_request(
response = requests.get(url, headers=headers, timeout=120)
elif method == "POST":
response = requests.post(url, headers=headers, json=data, timeout=120)
elif method == "PATCH":
response = requests.patch(url, headers=headers, json=data, timeout=120)
elif method == "DELETE":
response = requests.delete(url, headers=headers, timeout=120)
else:
@ -718,3 +754,912 @@ def delete_snapshot(
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("DELETE", f"/snapshots/{snapshot_id}", public_key, secret_key)
# =============================================================================
# Session Management Functions
# =============================================================================
def list_sessions(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
List all sessions for the authenticated account.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
List of session dicts containing id, container_name, status, etc.
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
response = _make_request("GET", "/sessions", public_key, secret_key)
return response.get("sessions", [])
def get_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get details of a specific session.
Args:
session_id: Session ID to get details for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Session details dict
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("GET", f"/sessions/{session_id}", public_key, secret_key)
def create_session(
language: Optional[str] = None,
network_mode: str = "zerotrust",
ttl: int = 3600,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
shell: Optional[str] = None,
multiplexer: Optional[str] = None,
vcpu: int = 1,
) -> Dict[str, Any]:
"""
Create a new interactive session.
Args:
language: Optional programming language for the session
network_mode: Network mode - "zerotrust" (default, no network) or "semitrusted" (with network)
ttl: Time to live in seconds (default 3600)
public_key: Optional API key
secret_key: Optional API secret
shell: Optional shell to use (e.g., "bash", "python3")
multiplexer: Optional terminal multiplexer ("tmux" or "screen")
vcpu: Number of vCPUs (1-8, default 1)
Returns:
Response dict containing session_id, container_name, etc.
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {
"network_mode": network_mode,
"ttl": ttl,
}
if language:
data["language"] = language
if shell:
data["shell"] = shell
if multiplexer:
data["multiplexer"] = multiplexer
if vcpu > 1:
data["vcpu"] = vcpu
return _make_request("POST", "/sessions", public_key, secret_key, data)
def delete_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete/terminate a session.
Args:
session_id: Session ID to delete
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation and optional artifacts
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("DELETE", f"/sessions/{session_id}", public_key, secret_key)
def freeze_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Freeze a session (pause execution, preserve state).
Args:
session_id: Session ID to freeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with freeze confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/sessions/{session_id}/freeze", public_key, secret_key, {})
def unfreeze_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unfreeze a session (resume execution).
Args:
session_id: Session ID to unfreeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unfreeze confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/sessions/{session_id}/unfreeze", public_key, secret_key, {})
def boost_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Boost a session (increase resources).
Args:
session_id: Session ID to boost
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with boost confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/sessions/{session_id}/boost", public_key, secret_key, {})
def unboost_session(
session_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unboost a session (return to normal resources).
Args:
session_id: Session ID to unboost
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unboost confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/sessions/{session_id}/unboost", public_key, secret_key, {})
def shell_session(
session_id: str,
command: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute a shell command in a session.
Note: This is for one-off commands. For interactive shell access,
use WebSocket connection to /sessions/{id}/shell.
Args:
session_id: Session ID to execute command in
command: Shell command to execute
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with command output
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request(
"POST",
f"/sessions/{session_id}/shell",
public_key,
secret_key,
{"command": command},
)
# =============================================================================
# Service Management Functions
# =============================================================================
def list_services(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
List all services for the authenticated account.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
List of service dicts containing id, name, status, ports, etc.
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
response = _make_request("GET", "/services", public_key, secret_key)
return response.get("services", [])
def create_service(
name: str,
ports: List[int],
bootstrap: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
network_mode: str = "semitrusted",
custom_domains: Optional[List[str]] = None,
vcpu: int = 1,
service_type: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create a new persistent service.
Args:
name: Service name (used for subdomain: name.on.unsandbox.com)
ports: List of ports to expose (e.g., [80, 443])
bootstrap: Bootstrap script content, URL, or inline command
public_key: Optional API key
secret_key: Optional API secret
network_mode: Network mode (default "semitrusted" for services)
custom_domains: Optional list of custom domain names
vcpu: Number of vCPUs (1-8, default 1)
service_type: Optional service type for SRV records (e.g., "minecraft")
Returns:
Response dict containing service_id, etc.
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {
"name": name,
"ports": ports,
"network_mode": network_mode,
}
if bootstrap:
# Check if it looks like a URL
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
data["bootstrap"] = bootstrap
else:
data["bootstrap_content"] = bootstrap
if custom_domains:
data["custom_domains"] = custom_domains
if vcpu > 1:
data["vcpu"] = vcpu
if service_type:
data["service_type"] = service_type
return _make_request("POST", "/services", public_key, secret_key, data)
def get_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get details of a specific service.
Args:
service_id: Service ID to get details for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Service details dict
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("GET", f"/services/{service_id}", public_key, secret_key)
def update_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
vcpu: Optional[int] = None,
**kwargs,
) -> Dict[str, Any]:
"""
Update a service (e.g., resize vCPU/memory).
Args:
service_id: Service ID to update
public_key: Optional API key
secret_key: Optional API secret
vcpu: Optional new vCPU count (1-8)
**kwargs: Additional fields to update
Returns:
Response dict with update confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {}
if vcpu is not None:
data["vcpu"] = vcpu
data.update(kwargs)
return _make_request("PATCH", f"/services/{service_id}", public_key, secret_key, data)
def delete_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete/destroy a service.
Args:
service_id: Service ID to delete
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("DELETE", f"/services/{service_id}", public_key, secret_key)
def freeze_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Freeze a service (pause execution, preserve state).
Args:
service_id: Service ID to freeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with freeze confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/services/{service_id}/freeze", public_key, secret_key, {})
def unfreeze_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unfreeze a service (resume execution).
Args:
service_id: Service ID to unfreeze
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unfreeze confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/services/{service_id}/unfreeze", public_key, secret_key, {})
def lock_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Lock a service to prevent accidental deletion.
Args:
service_id: Service ID to lock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with lock confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/services/{service_id}/lock", public_key, secret_key, {})
def unlock_service(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unlock a service to allow deletion.
Args:
service_id: Service ID to unlock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unlock confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {})
def get_service_logs(
service_id: str,
all_logs: bool = False,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get bootstrap/runtime logs for a service.
Args:
service_id: Service ID to get logs for
all_logs: If True, get all logs; if False, get last ~9000 lines (tail)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict containing "log" field with log content
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
path = f"/services/{service_id}/logs"
if all_logs:
path += "?all=true"
return _make_request("GET", path, public_key, secret_key)
def get_service_env(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get environment vault status for a service.
Returns metadata about the vault (has_vault, count, updated_at)
but NOT the actual secrets. Use export_service_env to retrieve secrets.
Args:
service_id: Service ID to get env status for
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with has_vault, count, updated_at fields
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("GET", f"/services/{service_id}/env", public_key, secret_key)
def set_service_env(
service_id: str,
env_dict: Dict[str, str],
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Set environment variables for a service.
Replaces the entire environment vault with the provided variables.
Variables are encrypted at rest and injected into the container.
Args:
service_id: Service ID to set env for
env_dict: Dictionary of environment variables (KEY: VALUE)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with count of variables set
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
# Convert dict to .env format for the API
env_content = "\n".join(f"{k}={v}" for k, v in env_dict.items())
# Note: This endpoint expects text/plain body, but we'll send as JSON
# and let the API handle conversion
return _make_request(
"POST",
f"/services/{service_id}/env",
public_key,
secret_key,
{"env": env_content},
)
def delete_service_env(
service_id: str,
keys: Optional[List[str]] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete environment vault or specific keys from a service.
Args:
service_id: Service ID to delete env from
keys: Optional list of specific keys to delete; if None, deletes entire vault
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with deletion confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
path = f"/services/{service_id}/env"
# If specific keys provided, could add as query params (API dependent)
return _make_request("DELETE", path, public_key, secret_key)
def export_service_env(
service_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Export environment vault secrets for a service.
Requires HMAC authentication to prove ownership.
Returns the actual secret values in .env format.
Args:
service_id: Service ID to export env from
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict containing "env" field with KEY=VALUE content
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/services/{service_id}/env/export", public_key, secret_key, {})
def redeploy_service(
service_id: str,
bootstrap: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Redeploy a service (re-run bootstrap script).
Bootstrap scripts should be idempotent for proper upgrade behavior.
Args:
service_id: Service ID to redeploy
bootstrap: Optional new bootstrap script/URL (uses existing if not provided)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with redeploy confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {}
if bootstrap:
if bootstrap.startswith("http://") or bootstrap.startswith("https://"):
data["bootstrap"] = bootstrap
else:
data["bootstrap_content"] = bootstrap
return _make_request("POST", f"/services/{service_id}/redeploy", public_key, secret_key, data)
def execute_in_service(
service_id: str,
command: str,
timeout: int = 30000,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute a command in a running service container.
Uses async job polling for long-running commands.
Args:
service_id: Service ID to execute command in
command: Shell command to execute
timeout: Command timeout in milliseconds (default 30000)
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with job_id for async polling, or direct result
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request(
"POST",
f"/services/{service_id}/execute",
public_key,
secret_key,
{"command": command, "timeout": timeout},
)
# =============================================================================
# Additional Snapshot Functions
# =============================================================================
def lock_snapshot(
snapshot_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Lock a snapshot to prevent accidental deletion.
Args:
snapshot_id: Snapshot ID to lock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with lock confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/snapshots/{snapshot_id}/lock", public_key, secret_key, {})
def unlock_snapshot(
snapshot_id: str,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Unlock a snapshot to allow deletion.
Args:
snapshot_id: Snapshot ID to unlock
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with unlock confirmation
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
return _make_request("POST", f"/snapshots/{snapshot_id}/unlock", public_key, secret_key, {})
def clone_snapshot(
snapshot_id: str,
clone_type: str = "session",
name: Optional[str] = None,
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
shell: Optional[str] = None,
ports: Optional[List[int]] = None,
) -> Dict[str, Any]:
"""
Clone a snapshot to create a new session or service.
Args:
snapshot_id: Snapshot ID to clone from
clone_type: Type of resource to create ("session" or "service")
name: Optional name for the new resource
public_key: Optional API key
secret_key: Optional API secret
shell: Optional shell for session clones
ports: Optional ports list for service clones
Returns:
Response dict containing session_id or service_id
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
data: Dict[str, Any] = {"type": clone_type}
if name:
data["name"] = name
if shell:
data["shell"] = shell
if ports:
data["ports"] = ports
return _make_request("POST", f"/snapshots/{snapshot_id}/clone", public_key, secret_key, data)
# =============================================================================
# Key Validation
# =============================================================================
PORTAL_BASE = "https://unsandbox.com"
def validate_keys(
public_key: Optional[str] = None,
secret_key: Optional[str] = None,
) -> Dict[str, Any]:
"""
Validate API keys against the portal.
Checks if the keys are valid, not expired, and not suspended.
Args:
public_key: Optional API key
secret_key: Optional API secret
Returns:
Response dict with validation result:
- valid: True if keys are valid
- tier: Account tier level
- expires_at: Expiration timestamp (if applicable)
- reason: Reason for invalid status (if applicable)
Raises:
requests.RequestException: Network errors
ValueError: Invalid response format
CredentialsError: Missing credentials
"""
public_key, secret_key = _resolve_credentials(public_key, secret_key)
url = f"{PORTAL_BASE}/keys/validate"
timestamp = int(time.time())
body = ""
signature = _sign_request(secret_key, timestamp, "POST", "/keys/validate", body)
headers = {
"Authorization": f"Bearer {public_key}",
"X-Timestamp": str(timestamp),
"X-Signature": signature,
"Content-Type": "application/json",
}
response = requests.post(url, headers=headers, data=body, timeout=30)
response.raise_for_status()
return response.json()

View file

@ -477,6 +477,619 @@ module UnAsync
end
end
# Lock a snapshot to prevent deletion
#
# @param snapshot_id [String] Snapshot ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with lock confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.lock_snapshot(snapshot_id).value
def lock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/snapshots/#{snapshot_id}/lock", pk, sk, {})
end
end
# Unlock a snapshot to allow deletion
#
# @param snapshot_id [String] Snapshot ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.unlock_snapshot(snapshot_id).value
def unlock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/snapshots/#{snapshot_id}/unlock", pk, sk, {})
end
end
# Clone a snapshot to create a new session or service
#
# @param snapshot_id [String] Snapshot ID to clone
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param name [String, nil] Optional name for the cloned resource
# @param type [String] Type of resource to create ("session" or "service")
# @param shell [String, nil] Optional shell for session clones
# @param ports [Array<Integer>, nil] Optional ports for service clones
# @return [Future<Hash>] Future resolving to response hash with cloned resource info
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example Clone to session
# result = UnAsync.clone_snapshot(snapshot_id, type: "session").value
# puts result["session_id"]
def clone_snapshot(snapshot_id, public_key: nil, secret_key: nil, name: nil, type: 'session', shell: nil, ports: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
data = { type: type }
data[:name] = name if name
data[:shell] = shell if shell
data[:ports] = ports if ports
make_request_sync('POST', "/snapshots/#{snapshot_id}/clone", pk, sk, data)
end
end
# ============================================================================
# Session Functions
# ============================================================================
# List all sessions for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Array<Hash>>] Future resolving to list of session hashes
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# sessions = UnAsync.list_sessions.value
# sessions.each { |s| puts "#{s['id']}: #{s['status']}" }
def list_sessions(public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request_sync('GET', '/sessions', pk, sk)
response['sessions'] || []
end
end
# Get session details by ID
#
# @param session_id [String] Session ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to session details hash
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# session = UnAsync.get_session(session_id).value
# puts session["status"]
def get_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('GET', "/sessions/#{session_id}", pk, sk)
end
end
# Create a new interactive session
#
# @param language [String] Shell or language for the session (e.g., "bash", "python3")
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param ttl [Integer] Time-to-live in seconds (default: 3600)
# @param multiplexer [String, nil] Terminal multiplexer ("tmux" or "screen")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @return [Future<Hash>] Future resolving to response hash with session_id and container_name
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.create_session("bash", network_mode: "semitrusted").value
# puts result["session_id"]
def create_session(language, public_key: nil, secret_key: nil, network_mode: 'zerotrust', ttl: 3600, multiplexer: nil, vcpu: 1)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
data = {
network_mode: network_mode,
ttl: ttl
}
data[:shell] = language if language
data[:multiplexer] = multiplexer if multiplexer
data[:vcpu] = vcpu if vcpu > 1
make_request_sync('POST', '/sessions', pk, sk, data)
end
end
# Delete (terminate) a session
#
# @param session_id [String] Session ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.delete_session(session_id).value
def delete_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('DELETE', "/sessions/#{session_id}", pk, sk)
end
end
# Freeze a session (pause execution, reduce resource usage)
#
# @param session_id [String] Session ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.freeze_session(session_id).value
def freeze_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/sessions/#{session_id}/freeze", pk, sk, {})
end
end
# Unfreeze a session (resume execution)
#
# @param session_id [String] Session ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.unfreeze_session(session_id).value
def unfreeze_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/sessions/#{session_id}/unfreeze", pk, sk, {})
end
end
# Boost a session (increase vCPU allocation)
#
# @param session_id [String] Session ID to boost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with boost confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.boost_session(session_id).value
def boost_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/sessions/#{session_id}/boost", pk, sk, {})
end
end
# Unboost a session (reduce vCPU allocation)
#
# @param session_id [String] Session ID to unboost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with unboost confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.unboost_session(session_id).value
def unboost_session(session_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/sessions/#{session_id}/unboost", pk, sk, {})
end
end
# Execute a shell command in an existing session
#
# @param session_id [String] Session ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with command output
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.shell_session(session_id, "ls -la").value
# puts result["stdout"]
def shell_session(session_id, command, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/sessions/#{session_id}/shell", pk, sk, { command: command })
end
end
# ============================================================================
# Service Functions
# ============================================================================
# List all services for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Array<Hash>>] Future resolving to list of service hashes
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# services = UnAsync.list_services.value
# services.each { |s| puts "#{s['id']}: #{s['name']} (#{s['state']})" }
def list_services(public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request_sync('GET', '/services', pk, sk)
response['services'] || []
end
end
# Create a new persistent service
#
# @param name [String] Service name
# @param ports [Array<Integer>] Ports to expose
# @param bootstrap [String] Bootstrap script content or URL
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @param custom_domains [Array<String>, nil] Custom domains for the service
# @param service_type [String, nil] Service type for SRV records
# @return [Future<Hash>] Future resolving to response hash with service_id
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.create_service("web", [80, 443], "apt install -y nginx && nginx").value
# puts result["service_id"]
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
data = {
name: name,
ports: ports,
bootstrap: bootstrap,
network_mode: network_mode
}
data[:vcpu] = vcpu if vcpu > 1
data[:custom_domains] = custom_domains if custom_domains
data[:service_type] = service_type if service_type
make_request_sync('POST', '/services', pk, sk, data)
end
end
# Get service details by ID
#
# @param service_id [String] Service ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to service details hash
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# service = UnAsync.get_service(service_id).value
# puts service["status"]
def get_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('GET', "/services/#{service_id}", pk, sk)
end
end
# Update a service (e.g., resize vCPU)
#
# @param service_id [String] Service ID to update
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param vcpu [Integer, nil] New vCPU count (1-8)
# @param name [String, nil] New service name
# @return [Future<Hash>] Future resolving to response hash with update confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.update_service(service_id, vcpu: 4).value
def update_service(service_id, public_key: nil, secret_key: nil, vcpu: nil, name: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:vcpu] = vcpu if vcpu
data[:name] = name if name
make_request_sync('PATCH', "/services/#{service_id}", pk, sk, data)
end
end
# Delete (destroy) a service
#
# @param service_id [String] Service ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.delete_service(service_id).value
def delete_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('DELETE', "/services/#{service_id}", pk, sk)
end
end
# Freeze a service (stop container, reduce resource usage)
#
# @param service_id [String] Service ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.freeze_service(service_id).value
def freeze_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/services/#{service_id}/freeze", pk, sk, {})
end
end
# Unfreeze a service (start container)
#
# @param service_id [String] Service ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.unfreeze_service(service_id).value
def unfreeze_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/services/#{service_id}/unfreeze", pk, sk, {})
end
end
# Lock a service to prevent deletion
#
# @param service_id [String] Service ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with lock confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.lock_service(service_id).value
def lock_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/services/#{service_id}/lock", pk, sk, {})
end
end
# Unlock a service to allow deletion
#
# @param service_id [String] Service ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.unlock_service(service_id).value
def unlock_service(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/services/#{service_id}/unlock", pk, sk, {})
end
end
# Get service logs (bootstrap output)
#
# @param service_id [String] Service ID to get logs for
# @param all [Boolean] If true, get all logs; if false, get last 9000 lines
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with log content
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# logs = UnAsync.get_service_logs(service_id).value
# puts logs["log"]
def get_service_logs(service_id, all: false, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
path = "/services/#{service_id}/logs"
path += '?all=true' if all
make_request_sync('GET', path, pk, sk)
end
end
# Get service environment vault status
#
# @param service_id [String] Service ID to get env status for
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with vault status
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# status = UnAsync.get_service_env(service_id).value
# puts "Variables: #{status['count']}"
def get_service_env(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('GET', "/services/#{service_id}/env", pk, sk)
end
end
# Set service environment variables (replaces existing vault)
#
# @param service_id [String] Service ID to set env for
# @param env [String] Environment content in .env format (KEY=VALUE per line)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.set_service_env(service_id, "API_KEY=secret\nDEBUG=true").value
def set_service_env(service_id, env, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_text_sync('PUT', "/services/#{service_id}/env", pk, sk, env)
end
end
# Delete service environment vault
#
# @param service_id [String] Service ID to delete env for
# @param keys [Array<String>, nil] Specific keys to delete (nil = delete entire vault)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example Delete entire vault
# UnAsync.delete_service_env(service_id).value
def delete_service_env(service_id, keys: nil, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
if keys
make_request_sync('DELETE', "/services/#{service_id}/env", pk, sk, { keys: keys })
else
make_request_sync('DELETE', "/services/#{service_id}/env", pk, sk)
end
end
end
# Export service environment vault (returns .env format)
#
# @param service_id [String] Service ID to export env from
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with env content
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.export_service_env(service_id).value
# puts result["env"]
def export_service_env(service_id, public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', "/services/#{service_id}/env/export", pk, sk, {})
end
end
# Redeploy a service (re-run bootstrap script)
#
# @param service_id [String] Service ID to redeploy
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param bootstrap [String, nil] New bootstrap script (optional)
# @return [Future<Hash>] Future resolving to response hash with redeploy confirmation
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# UnAsync.redeploy_service(service_id).value
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:bootstrap] = bootstrap if bootstrap
make_request_sync('POST', "/services/#{service_id}/redeploy", pk, sk, data)
end
end
# Execute a command in a running service
#
# @param service_id [String] Service ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param timeout [Integer] Command timeout in milliseconds (default: 30000)
# @return [Future<Hash>] Future resolving to response hash with command output
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails (on .value)
#
# @example
# result = UnAsync.execute_in_service(service_id, "ls -la").value
# puts result["stdout"]
def execute_in_service(service_id, command, public_key: nil, secret_key: nil, timeout: 30_000)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
# Start async execution
response = make_request_sync('POST', "/services/#{service_id}/execute", pk, sk, {
command: command,
timeout: timeout
})
job_id = response['job_id']
if job_id
# Poll for completion
wait_for_job_sync(job_id, pk, sk, (timeout / 1000) + 10)
else
response
end
end
end
# ============================================================================
# Key Validation
# ============================================================================
# Validate API keys
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Future<Hash>] Future resolving to response hash with validation result
# @raise [CredentialsError] If no credentials found (on .value)
# @raise [APIError] If API request fails or keys invalid (on .value)
#
# @example
# result = UnAsync.validate_keys.value
# puts result["valid"]
def validate_keys(public_key: nil, secret_key: nil)
Future.new do
pk, sk = resolve_credentials(public_key, secret_key)
make_request_sync('POST', '/keys/validate', pk, sk, {})
end
end
# Execute multiple futures concurrently and wait for all to complete
#
# @param futures [Array<Future>] Array of futures to wait for
@ -741,8 +1354,14 @@ module UnAsync
http.get(uri.request_uri, headers)
when 'POST'
http.post(uri.request_uri, body, headers)
when 'PATCH'
http.patch(uri.request_uri, body, headers)
when 'PUT'
http.put(uri.request_uri, body, headers)
when 'DELETE'
http.delete(uri.request_uri, headers)
req = Net::HTTP::Delete.new(uri.request_uri, headers)
req.body = body if data
http.request(req)
else
raise APIError, "Unsupported HTTP method: #{method}"
end
@ -766,6 +1385,59 @@ module UnAsync
raise
end
# Make a synchronous authenticated HTTP request with text/plain content type
#
# @param method [String] HTTP method (PUT)
# @param path [String] API path
# @param public_key [String] API public key
# @param secret_key [String] API secret key
# @param body [String] Plain text request body
# @return [Hash] Parsed JSON response
# @raise [APIError] If request fails
def make_request_text_sync(method, path, public_key, secret_key, body)
uri = URI.parse("#{API_BASE}#{path}")
timestamp = Time.now.to_i
signature = sign_request(secret_key, timestamp, method, path, body)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = REQUEST_TIMEOUT
http.read_timeout = REQUEST_TIMEOUT
headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => timestamp.to_s,
'X-Signature' => signature,
'Content-Type' => 'text/plain'
}
response = case method
when 'PUT'
http.put(uri.request_uri, body, headers)
else
raise APIError, "Unsupported HTTP method for text: #{method}"
end
unless response.is_a?(Net::HTTPSuccess)
raise APIError.new(
"API request failed: #{response.code} #{response.message}",
status_code: response.code.to_i,
response_body: response.body
)
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise APIError, "Invalid JSON response: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise APIError, "Request timeout: #{e.message}"
rescue StandardError => e
raise APIError, "Request failed: #{e.message}" unless e.is_a?(APIError)
raise
end
# Wait for job completion synchronously (used internally)
#
# @param job_id [String] Job ID

View file

@ -366,6 +366,569 @@ module Un
make_request('DELETE', "/snapshots/#{snapshot_id}", pk, sk)
end
# Lock a snapshot to prevent deletion
#
# @param snapshot_id [String] Snapshot ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with lock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.lock_snapshot(snapshot_id)
def lock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/snapshots/#{snapshot_id}/lock", pk, sk, {})
end
# Unlock a snapshot to allow deletion
#
# @param snapshot_id [String] Snapshot ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unlock_snapshot(snapshot_id)
def unlock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/snapshots/#{snapshot_id}/unlock", pk, sk, {})
end
# Clone a snapshot to create a new session or service
#
# @param snapshot_id [String] Snapshot ID to clone
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param name [String, nil] Optional name for the cloned resource
# @param type [String] Type of resource to create ("session" or "service")
# @param shell [String, nil] Optional shell for session clones
# @param ports [Array<Integer>, nil] Optional ports for service clones
# @return [Hash] Response hash with cloned resource info (session_id or service_id)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Clone to session
# result = Un.clone_snapshot(snapshot_id, type: "session")
# puts result["session_id"]
#
# @example Clone to service
# result = Un.clone_snapshot(snapshot_id, type: "service", ports: [80, 443])
# puts result["service_id"]
def clone_snapshot(snapshot_id, public_key: nil, secret_key: nil, name: nil, type: 'session', shell: nil, ports: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = { type: type }
data[:name] = name if name
data[:shell] = shell if shell
data[:ports] = ports if ports
make_request('POST', "/snapshots/#{snapshot_id}/clone", pk, sk, data)
end
# ============================================================================
# Session Functions
# ============================================================================
# List all sessions for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of session hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# sessions = Un.list_sessions
# sessions.each { |s| puts "#{s['id']}: #{s['status']}" }
def list_sessions(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/sessions', pk, sk)
response['sessions'] || []
end
# Get session details by ID
#
# @param session_id [String] Session ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Session details hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# session = Un.get_session(session_id)
# puts session["status"]
def get_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/sessions/#{session_id}", pk, sk)
end
# Create a new interactive session
#
# @param language [String] Shell or language for the session (e.g., "bash", "python3")
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param ttl [Integer] Time-to-live in seconds (default: 3600)
# @param multiplexer [String, nil] Terminal multiplexer ("tmux" or "screen")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @return [Hash] Response hash with session_id and container_name
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.create_session("bash", network_mode: "semitrusted")
# puts result["session_id"]
def create_session(language, public_key: nil, secret_key: nil, network_mode: 'zerotrust', ttl: 3600, multiplexer: nil, vcpu: 1)
pk, sk = resolve_credentials(public_key, secret_key)
data = {
network_mode: network_mode,
ttl: ttl
}
data[:shell] = language if language
data[:multiplexer] = multiplexer if multiplexer
data[:vcpu] = vcpu if vcpu > 1
make_request('POST', '/sessions', pk, sk, data)
end
# Delete (terminate) a session
#
# @param session_id [String] Session ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_session(session_id)
def delete_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('DELETE', "/sessions/#{session_id}", pk, sk)
end
# Freeze a session (pause execution, reduce resource usage)
#
# @param session_id [String] Session ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.freeze_session(session_id)
def freeze_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/freeze", pk, sk, {})
end
# Unfreeze a session (resume execution)
#
# @param session_id [String] Session ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unfreeze_session(session_id)
def unfreeze_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/unfreeze", pk, sk, {})
end
# Boost a session (increase vCPU allocation)
#
# @param session_id [String] Session ID to boost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with boost confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.boost_session(session_id)
def boost_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/boost", pk, sk, {})
end
# Unboost a session (reduce vCPU allocation)
#
# @param session_id [String] Session ID to unboost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unboost confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unboost_session(session_id)
def unboost_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/unboost", pk, sk, {})
end
# Execute a shell command in an existing session
# Note: This is for non-interactive command execution, not for WebSocket shell access
#
# @param session_id [String] Session ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with command output
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.shell_session(session_id, "ls -la")
# puts result["stdout"]
def shell_session(session_id, command, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/shell", pk, sk, { command: command })
end
# ============================================================================
# Service Functions
# ============================================================================
# List all services for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of service hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# services = Un.list_services
# services.each { |s| puts "#{s['id']}: #{s['name']} (#{s['state']})" }
def list_services(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/services', pk, sk)
response['services'] || []
end
# Create a new persistent service
#
# @param name [String] Service name
# @param ports [Array<Integer>] Ports to expose
# @param bootstrap [String] Bootstrap script content or URL
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @param custom_domains [Array<String>, nil] Custom domains for the service
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
# @return [Hash] Response hash with service_id
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
# puts result["service_id"]
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {
name: name,
ports: ports,
bootstrap: bootstrap,
network_mode: network_mode
}
data[:vcpu] = vcpu if vcpu > 1
data[:custom_domains] = custom_domains if custom_domains
data[:service_type] = service_type if service_type
make_request('POST', '/services', pk, sk, data)
end
# Get service details by ID
#
# @param service_id [String] Service ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Service details hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# service = Un.get_service(service_id)
# puts service["status"]
def get_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/services/#{service_id}", pk, sk)
end
# Update a service (e.g., resize vCPU)
#
# @param service_id [String] Service ID to update
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param vcpu [Integer, nil] New vCPU count (1-8)
# @param name [String, nil] New service name
# @return [Hash] Response hash with update confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.update_service(service_id, vcpu: 4)
def update_service(service_id, public_key: nil, secret_key: nil, vcpu: nil, name: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:vcpu] = vcpu if vcpu
data[:name] = name if name
make_request('PATCH', "/services/#{service_id}", pk, sk, data)
end
# Delete (destroy) a service
#
# @param service_id [String] Service ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_service(service_id)
def delete_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('DELETE', "/services/#{service_id}", pk, sk)
end
# Freeze a service (stop container, reduce resource usage)
#
# @param service_id [String] Service ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.freeze_service(service_id)
def freeze_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/freeze", pk, sk, {})
end
# Unfreeze a service (start container)
#
# @param service_id [String] Service ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unfreeze_service(service_id)
def unfreeze_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/unfreeze", pk, sk, {})
end
# Lock a service to prevent deletion
#
# @param service_id [String] Service ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with lock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.lock_service(service_id)
def lock_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/lock", pk, sk, {})
end
# Unlock a service to allow deletion
#
# @param service_id [String] Service ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unlock_service(service_id)
def unlock_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/unlock", pk, sk, {})
end
# Get service logs (bootstrap output)
#
# @param service_id [String] Service ID to get logs for
# @param all [Boolean] If true, get all logs; if false, get last 9000 lines (default: false)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with log content
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# logs = Un.get_service_logs(service_id)
# puts logs["log"]
def get_service_logs(service_id, all: false, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
path = "/services/#{service_id}/logs"
path += '?all=true' if all
make_request('GET', path, pk, sk)
end
# Get service environment vault status
#
# @param service_id [String] Service ID to get env status for
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with vault status (has_vault, count, updated_at)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# status = Un.get_service_env(service_id)
# puts "Variables: #{status['count']}"
def get_service_env(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/services/#{service_id}/env", pk, sk)
end
# Set service environment variables (replaces existing vault)
#
# @param service_id [String] Service ID to set env for
# @param env [String] Environment content in .env format (KEY=VALUE per line)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.set_service_env(service_id, "API_KEY=secret\nDEBUG=true")
def set_service_env(service_id, env, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
# This endpoint uses text/plain content type and PUT method
make_request_text('PUT', "/services/#{service_id}/env", pk, sk, env)
end
# Delete service environment vault
#
# @param service_id [String] Service ID to delete env for
# @param keys [Array<String>, nil] Specific keys to delete (nil = delete entire vault)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Delete entire vault
# Un.delete_service_env(service_id)
#
# @example Delete specific keys (if API supports it)
# Un.delete_service_env(service_id, keys: ["API_KEY", "DEBUG"])
def delete_service_env(service_id, keys: nil, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
if keys
make_request('DELETE', "/services/#{service_id}/env", pk, sk, { keys: keys })
else
make_request('DELETE', "/services/#{service_id}/env", pk, sk)
end
end
# Export service environment vault (returns .env format)
#
# @param service_id [String] Service ID to export env from
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with env content
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.export_service_env(service_id)
# puts result["env"] # API_KEY=secret\nDEBUG=true
def export_service_env(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/env/export", pk, sk, {})
end
# Redeploy a service (re-run bootstrap script)
#
# @param service_id [String] Service ID to redeploy
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param bootstrap [String, nil] New bootstrap script (optional)
# @return [Hash] Response hash with redeploy confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.redeploy_service(service_id)
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:bootstrap] = bootstrap if bootstrap
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
end
# Execute a command in a running service
#
# @param service_id [String] Service ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param timeout [Integer] Command timeout in milliseconds (default: 30000)
# @return [Hash] Response hash with command output (stdout, stderr, exit_code)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.execute_in_service(service_id, "ls -la")
# puts result["stdout"]
def execute_in_service(service_id, command, public_key: nil, secret_key: nil, timeout: 30_000)
pk, sk = resolve_credentials(public_key, secret_key)
# Start async execution
response = make_request('POST', "/services/#{service_id}/execute", pk, sk, {
command: command,
timeout: timeout
})
job_id = response['job_id']
return response unless job_id
# Poll for completion
wait_for_job(job_id, public_key: pk, secret_key: sk, timeout: (timeout / 1000) + 10)
end
# ============================================================================
# Key Validation
# ============================================================================
# Validate API keys
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with validation result and account info
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails or keys invalid
#
# @example
# result = Un.validate_keys
# puts result["valid"] # true or false
def validate_keys(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
# Note: This endpoint is on the portal, not API, but we use same auth
make_request('POST', '/keys/validate', pk, sk, {})
end
private
# Language detection mapping (file extension -> language)
@ -545,8 +1108,14 @@ module Un
http.get(uri.request_uri, headers)
when 'POST'
http.post(uri.request_uri, body, headers)
when 'PATCH'
http.patch(uri.request_uri, body, headers)
when 'PUT'
http.put(uri.request_uri, body, headers)
when 'DELETE'
http.delete(uri.request_uri, headers)
req = Net::HTTP::Delete.new(uri.request_uri, headers)
req.body = body if data
http.request(req)
else
raise APIError, "Unsupported HTTP method: #{method}"
end
@ -570,6 +1139,59 @@ module Un
raise
end
# Make an authenticated HTTP request with text/plain content type
#
# @param method [String] HTTP method (PUT)
# @param path [String] API path
# @param public_key [String] API public key
# @param secret_key [String] API secret key
# @param body [String] Plain text request body
# @return [Hash] Parsed JSON response
# @raise [APIError] If request fails
def make_request_text(method, path, public_key, secret_key, body)
uri = URI.parse("#{API_BASE}#{path}")
timestamp = Time.now.to_i
signature = sign_request(secret_key, timestamp, method, path, body)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = REQUEST_TIMEOUT
http.read_timeout = REQUEST_TIMEOUT
headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => timestamp.to_s,
'X-Signature' => signature,
'Content-Type' => 'text/plain'
}
response = case method
when 'PUT'
http.put(uri.request_uri, body, headers)
else
raise APIError, "Unsupported HTTP method for text: #{method}"
end
unless response.is_a?(Net::HTTPSuccess)
raise APIError.new(
"API request failed: #{response.code} #{response.message}",
status_code: response.code.to_i,
response_body: response.body
)
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise APIError, "Invalid JSON response: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise APIError, "Request timeout: #{e.message}"
rescue StandardError => e
raise APIError, "Request failed: #{e.message}" unless e.is_a?(APIError)
raise
end
# Get path to languages cache file
#
# @return [String] Path to languages.json

View file

@ -250,6 +250,153 @@ pub struct RestoreResult {
pub message: String,
}
/// Session information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
/// Session ID
pub session_id: String,
/// Container name (e.g., "unsb-vm-abc123")
#[serde(default)]
pub container_name: String,
/// Session status: "running", "frozen", "stopped"
#[serde(default)]
pub status: String,
/// Network mode: "zerotrust" or "semitrusted"
#[serde(default)]
pub network_mode: String,
/// Shell type (e.g., "bash", "python3")
#[serde(default)]
pub shell: String,
/// Number of vCPUs
#[serde(default)]
pub vcpu: u32,
/// Memory in MB
#[serde(default)]
pub memory_mb: u32,
/// Whether the session is boosted
#[serde(default)]
pub boosted: bool,
/// Created timestamp
#[serde(default)]
pub created_at: String,
/// Last activity timestamp
#[serde(default)]
pub last_activity: String,
}
/// Service information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
/// Service ID
pub service_id: String,
/// Service name
#[serde(default)]
pub name: String,
/// Container name
#[serde(default)]
pub container_name: String,
/// Service status: "running", "frozen", "stopped", "locked"
#[serde(default)]
pub status: String,
/// Exposed ports
#[serde(default)]
pub ports: Vec<u16>,
/// Custom domains
#[serde(default)]
pub domains: Vec<String>,
/// Network mode
#[serde(default)]
pub network_mode: String,
/// Number of vCPUs
#[serde(default)]
pub vcpu: u32,
/// Memory in MB
#[serde(default)]
pub memory_mb: u32,
/// Whether the service is locked (cannot be modified)
#[serde(default)]
pub locked: bool,
/// Public URL for the service
#[serde(default)]
pub url: String,
/// Created timestamp
#[serde(default)]
pub created_at: String,
}
/// Result of shell command execution in a session
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellResult {
/// Command output (stdout + stderr)
#[serde(default)]
pub output: String,
/// Exit code
#[serde(default)]
pub exit_code: i32,
}
/// Result of validating API keys
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysValid {
/// Whether the keys are valid
pub valid: bool,
/// Account ID associated with the keys
#[serde(default)]
pub account_id: String,
/// Account email (if available)
#[serde(default)]
pub email: String,
/// Account plan/tier
#[serde(default)]
pub plan: String,
/// Error message if invalid
#[serde(default)]
pub error: String,
}
/// Options for creating a session
#[derive(Debug, Clone, Default)]
pub struct SessionCreateOptions {
/// Network mode: "zerotrust" (default) or "semitrusted"
pub network_mode: Option<String>,
/// Shell to use (e.g., "bash", "python3")
pub shell: Option<String>,
/// Number of vCPUs (default: 1)
pub vcpu: Option<u32>,
/// Whether to use tmux multiplexer
pub tmux: Option<bool>,
/// Whether to use screen multiplexer
pub screen: Option<bool>,
}
/// Options for creating a service
#[derive(Debug, Clone, Default)]
pub struct ServiceCreateOptions {
/// Network mode: "zerotrust" (default) or "semitrusted"
pub network_mode: Option<String>,
/// Number of vCPUs (default: 1)
pub vcpu: Option<u32>,
/// Custom domains for the service
pub domains: Option<Vec<String>>,
/// Bootstrap script content
pub bootstrap: Option<String>,
/// Bootstrap script URL
pub bootstrap_url: Option<String>,
}
/// Options for updating a service
#[derive(Debug, Clone, Default)]
pub struct ServiceUpdateOptions {
/// New service name
pub name: Option<String>,
/// New ports
pub ports: Option<Vec<u16>>,
/// New domains
pub domains: Option<Vec<String>>,
/// New vCPU count
pub vcpu: Option<u32>,
}
// =============================================================================
// Internal Response Types
// =============================================================================
@ -304,6 +451,26 @@ struct LanguagesCache {
timestamp: u64,
}
#[derive(Debug, Deserialize)]
struct SessionsListResponse {
sessions: Vec<Session>,
}
#[derive(Debug, Deserialize)]
struct ServicesListResponse {
services: Vec<Service>,
}
#[derive(Debug, Deserialize)]
struct EnvResponse {
env: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct EnvExportResponse {
content: String,
}
// =============================================================================
// Language Detection
// =============================================================================
@ -632,6 +799,8 @@ async fn make_request<T: for<'de> Deserialize<'de>>(
let mut request = match method {
"GET" => client.get(&url),
"POST" => client.post(&url),
"PATCH" => client.patch(&url),
"PUT" => client.put(&url),
"DELETE" => client.delete(&url),
_ => client.get(&url),
};
@ -989,6 +1158,672 @@ pub async fn delete_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<(
Ok(())
}
/// Lock a snapshot to prevent deletion.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to lock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Snapshot information
pub async fn lock_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/lock", snapshot_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Unlock a snapshot to allow deletion.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to unlock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Snapshot information
pub async fn unlock_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/unlock", snapshot_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Clone a snapshot to create a new snapshot with a different name.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to clone
/// * `name` - Name for the new snapshot
/// * `creds` - API credentials
///
/// # Returns
/// New Snapshot information
pub async fn clone_snapshot(snapshot_id: &str, name: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/clone", snapshot_id);
let body = serde_json::json!({
"name": name
});
make_request("POST", &path, creds, Some(&body)).await
}
// =============================================================================
// Session API Functions
// =============================================================================
/// List all sessions for the authenticated account.
///
/// # Arguments
/// * `creds` - API credentials
///
/// # Returns
/// Vector of Session information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let sessions = list_sessions(&creds).await?;
/// for session in sessions {
/// println!("{}: {} ({})", session.session_id, session.container_name, session.status);
/// }
/// ```
pub async fn list_sessions(creds: &Credentials) -> Result<Vec<Session>> {
let response: SessionsListResponse = make_request("GET", "/sessions", creds, None::<&()>).await?;
Ok(response.sessions)
}
/// Get details of a specific session.
///
/// # Arguments
/// * `session_id` - Session ID to retrieve
/// * `creds` - API credentials
///
/// # Returns
/// Session information
pub async fn get_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}", session_id);
make_request("GET", &path, creds, None::<&()>).await
}
/// Create a new interactive session.
///
/// # Arguments
/// * `language` - Programming language/shell (e.g., "bash", "python")
/// * `creds` - API credentials
/// * `opts` - Optional session creation options
///
/// # Returns
/// Created Session information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
///
/// // Create a basic bash session
/// let session = create_session("bash", &creds, None).await?;
///
/// // Create a session with options
/// let opts = SessionCreateOptions {
/// network_mode: Some("semitrusted".to_string()),
/// tmux: Some(true),
/// ..Default::default()
/// };
/// let session = create_session("bash", &creds, Some(opts)).await?;
/// ```
pub async fn create_session(
language: &str,
creds: &Credentials,
opts: Option<SessionCreateOptions>,
) -> Result<Session> {
let mut body = serde_json::json!({
"language": language
});
if let Some(opts) = opts {
if let Some(network_mode) = opts.network_mode {
body["network_mode"] = serde_json::json!(network_mode);
}
if let Some(shell) = opts.shell {
body["shell"] = serde_json::json!(shell);
}
if let Some(vcpu) = opts.vcpu {
body["vcpu"] = serde_json::json!(vcpu);
}
if let Some(tmux) = opts.tmux {
body["tmux"] = serde_json::json!(tmux);
}
if let Some(screen) = opts.screen {
body["screen"] = serde_json::json!(screen);
}
}
make_request("POST", "/sessions", creds, Some(&body)).await
}
/// Delete (terminate) a session.
///
/// # Arguments
/// * `session_id` - Session ID to delete
/// * `creds` - API credentials
pub async fn delete_session(session_id: &str, creds: &Credentials) -> Result<()> {
let path = format!("/sessions/{}", session_id);
let _: serde_json::Value = make_request("DELETE", &path, creds, None::<&()>).await?;
Ok(())
}
/// Freeze a session to save resources while preserving state.
///
/// Frozen sessions can be unfrozen later to resume work.
///
/// # Arguments
/// * `session_id` - Session ID to freeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub async fn freeze_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/freeze", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Unfreeze a frozen session to resume work.
///
/// # Arguments
/// * `session_id` - Session ID to unfreeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub async fn unfreeze_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/unfreeze", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Boost a session's resources (increase vCPU and memory).
///
/// Memory is derived from vCPU: vcpu * 2048MB.
///
/// # Arguments
/// * `session_id` - Session ID to boost
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub async fn boost_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/boost", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Remove boost from a session (return to base resources).
///
/// # Arguments
/// * `session_id` - Session ID to unboost
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub async fn unboost_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/unboost", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Execute a shell command in a session.
///
/// # Arguments
/// * `session_id` - Session ID to execute command in
/// * `command` - Command to execute
/// * `creds` - API credentials
///
/// # Returns
/// ShellResult with output and exit code
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = shell_session("session-123", "ls -la", &creds).await?;
/// println!("Output: {}", result.output);
/// println!("Exit code: {}", result.exit_code);
/// ```
pub async fn shell_session(session_id: &str, command: &str, creds: &Credentials) -> Result<ShellResult> {
let path = format!("/sessions/{}/shell", session_id);
let body = serde_json::json!({
"command": command
});
make_request("POST", &path, creds, Some(&body)).await
}
// =============================================================================
// Service API Functions
// =============================================================================
/// List all services for the authenticated account.
///
/// # Arguments
/// * `creds` - API credentials
///
/// # Returns
/// Vector of Service information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let services = list_services(&creds).await?;
/// for service in services {
/// println!("{}: {} ({}) - {}", service.service_id, service.name, service.status, service.url);
/// }
/// ```
pub async fn list_services(creds: &Credentials) -> Result<Vec<Service>> {
let response: ServicesListResponse = make_request("GET", "/services", creds, None::<&()>).await?;
Ok(response.services)
}
/// Create a new persistent service.
///
/// # Arguments
/// * `name` - Service name (used in URL: name.on.unsandbox.com)
/// * `ports` - Ports to expose
/// * `bootstrap` - Bootstrap script content to run on startup
/// * `creds` - API credentials
/// * `opts` - Optional service creation options
///
/// # Returns
/// Created Service information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
///
/// // Create a simple web service
/// let service = create_service(
/// "myapp",
/// &[8080],
/// "python3 -m http.server 8080",
/// &creds,
/// None
/// ).await?;
/// println!("Service URL: {}", service.url);
///
/// // Create with options
/// let opts = ServiceCreateOptions {
/// network_mode: Some("semitrusted".to_string()),
/// vcpu: Some(2),
/// domains: Some(vec!["example.com".to_string()]),
/// ..Default::default()
/// };
/// let service = create_service("myapp", &[80, 443], bootstrap, &creds, Some(opts)).await?;
/// ```
pub async fn create_service(
name: &str,
ports: &[u16],
bootstrap: &str,
creds: &Credentials,
opts: Option<ServiceCreateOptions>,
) -> Result<Service> {
let mut body = serde_json::json!({
"name": name,
"ports": ports,
"bootstrap": bootstrap
});
if let Some(opts) = opts {
if let Some(network_mode) = opts.network_mode {
body["network_mode"] = serde_json::json!(network_mode);
}
if let Some(vcpu) = opts.vcpu {
body["vcpu"] = serde_json::json!(vcpu);
}
if let Some(domains) = opts.domains {
body["domains"] = serde_json::json!(domains);
}
if let Some(bootstrap_url) = opts.bootstrap_url {
body["bootstrap_url"] = serde_json::json!(bootstrap_url);
}
}
make_request("POST", "/services", creds, Some(&body)).await
}
/// Get details of a specific service.
///
/// # Arguments
/// * `service_id` - Service ID to retrieve
/// * `creds` - API credentials
///
/// # Returns
/// Service information
pub async fn get_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}", service_id);
make_request("GET", &path, creds, None::<&()>).await
}
/// Update a service's configuration.
///
/// # Arguments
/// * `service_id` - Service ID to update
/// * `creds` - API credentials
/// * `opts` - Update options (name, ports, domains, vcpu)
///
/// # Returns
/// Updated Service information
pub async fn update_service(
service_id: &str,
creds: &Credentials,
opts: ServiceUpdateOptions,
) -> Result<Service> {
let path = format!("/services/{}", service_id);
let mut body = serde_json::Map::new();
if let Some(name) = opts.name {
body.insert("name".to_string(), serde_json::json!(name));
}
if let Some(ports) = opts.ports {
body.insert("ports".to_string(), serde_json::json!(ports));
}
if let Some(domains) = opts.domains {
body.insert("domains".to_string(), serde_json::json!(domains));
}
if let Some(vcpu) = opts.vcpu {
body.insert("vcpu".to_string(), serde_json::json!(vcpu));
}
make_request("PATCH", &path, creds, Some(&serde_json::Value::Object(body))).await
}
/// Delete (destroy) a service.
///
/// # Arguments
/// * `service_id` - Service ID to delete
/// * `creds` - API credentials
pub async fn delete_service(service_id: &str, creds: &Credentials) -> Result<()> {
let path = format!("/services/{}", service_id);
let _: serde_json::Value = make_request("DELETE", &path, creds, None::<&()>).await?;
Ok(())
}
/// Freeze a service to save resources while preserving state.
///
/// # Arguments
/// * `service_id` - Service ID to freeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub async fn freeze_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/freeze", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Unfreeze a frozen service to resume operation.
///
/// # Arguments
/// * `service_id` - Service ID to unfreeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub async fn unfreeze_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/unfreeze", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Lock a service to prevent modifications.
///
/// # Arguments
/// * `service_id` - Service ID to lock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub async fn lock_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/lock", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Unlock a service to allow modifications.
///
/// # Arguments
/// * `service_id` - Service ID to unlock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub async fn unlock_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/unlock", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Get bootstrap logs for a service.
///
/// # Arguments
/// * `service_id` - Service ID to get logs for
/// * `all` - If true, get all logs; if false, get last 9000 lines
/// * `creds` - API credentials
///
/// # Returns
/// Log content as string
pub async fn get_service_logs(service_id: &str, all: bool, creds: &Credentials) -> Result<String> {
let path = if all {
format!("/services/{}/logs?all=true", service_id)
} else {
format!("/services/{}/logs", service_id)
};
#[derive(Deserialize)]
struct LogsResponse {
#[serde(default)]
logs: String,
}
let response: LogsResponse = make_request("GET", &path, creds, None::<&()>).await?;
Ok(response.logs)
}
/// Get environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to get env for
/// * `creds` - API credentials
///
/// # Returns
/// HashMap of environment variable key-value pairs
pub async fn get_service_env(service_id: &str, creds: &Credentials) -> Result<HashMap<String, String>> {
let path = format!("/services/{}/env", service_id);
let response: EnvResponse = make_request("GET", &path, creds, None::<&()>).await?;
Ok(response.env)
}
/// Set environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to set env for
/// * `env` - HashMap of environment variable key-value pairs
/// * `creds` - API credentials
pub async fn set_service_env(
service_id: &str,
env: &HashMap<String, String>,
creds: &Credentials,
) -> Result<()> {
let path = format!("/services/{}/env", service_id);
// Convert to .env format
let content: String = env
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("\n");
// Use PUT with text/plain content type
let client = Client::builder()
.timeout(Duration::from_secs(120))
.build()?;
let url = format!("{}{}", API_BASE, path);
let timestamp = get_timestamp();
let signature = sign_request(&creds.secret_key, timestamp, "PUT", &path, &content);
let response = client
.put(&url)
.header("Authorization", format!("Bearer {}", creds.public_key))
.header("X-Timestamp", timestamp.to_string())
.header("X-Signature", signature)
.header("Content-Type", "text/plain")
.header("User-Agent", "un-rust-async/2.0")
.body(content)
.send()
.await?;
let status = response.status().as_u16();
if status < 200 || status >= 300 {
let response_text = response.text().await?;
return Err(UnsandboxError::ApiError {
status,
message: response_text,
});
}
Ok(())
}
/// Delete environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to delete env for
/// * `keys` - List of environment variable keys to delete
/// * `creds` - API credentials
pub async fn delete_service_env(
service_id: &str,
keys: &[&str],
creds: &Credentials,
) -> Result<()> {
let path = format!("/services/{}/env", service_id);
let body = serde_json::json!({
"keys": keys
});
let _: serde_json::Value = make_request("DELETE", &path, creds, Some(&body)).await?;
Ok(())
}
/// Export environment variables for a service in .env format.
///
/// # Arguments
/// * `service_id` - Service ID to export env for
/// * `creds` - API credentials
///
/// # Returns
/// Environment variables in .env format string
pub async fn export_service_env(service_id: &str, creds: &Credentials) -> Result<String> {
let path = format!("/services/{}/env/export", service_id);
let body = serde_json::json!({});
let response: EnvExportResponse = make_request("POST", &path, creds, Some(&body)).await?;
Ok(response.content)
}
/// Redeploy a service with a new bootstrap script.
///
/// # Arguments
/// * `service_id` - Service ID to redeploy
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub async fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/redeploy", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body)).await
}
/// Execute a command in a service container.
///
/// # Arguments
/// * `service_id` - Service ID to execute command in
/// * `command` - Command to execute
/// * `creds` - API credentials
///
/// # Returns
/// ExecuteResult with output and exit code
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = execute_in_service("service-123", "ls -la /app", &creds).await?;
/// println!("Output: {}", result.output);
/// ```
pub async fn execute_in_service(
service_id: &str,
command: &str,
creds: &Credentials,
) -> Result<ExecuteResult> {
let path = format!("/services/{}/execute", service_id);
let body = serde_json::json!({
"command": command,
"timeout": 30000
});
make_request("POST", &path, creds, Some(&body)).await
}
// =============================================================================
// Key Validation API Functions
// =============================================================================
/// Validate API keys.
///
/// # Arguments
/// * `creds` - API credentials to validate
///
/// # Returns
/// KeysValid with validation result and account info
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = validate_keys(&creds).await?;
/// if result.valid {
/// println!("Keys valid for account: {}", result.account_id);
/// } else {
/// println!("Invalid keys: {}", result.error);
/// }
/// ```
pub async fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
// Note: This endpoint is on the portal, not the API
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
let url = "https://unsandbox.com/keys/validate";
let path = "/keys/validate";
let timestamp = get_timestamp();
let body_str = "";
let signature = sign_request(&creds.secret_key, timestamp, "POST", path, body_str);
let response = client
.post(url)
.header("Authorization", format!("Bearer {}", creds.public_key))
.header("X-Timestamp", timestamp.to_string())
.header("X-Signature", signature)
.header("Content-Type", "application/json")
.header("User-Agent", "un-rust-async/2.0")
.body("")
.send()
.await?;
let status = response.status().as_u16();
let response_text = response.text().await?;
if status < 200 || status >= 300 {
return Err(UnsandboxError::ApiError {
status,
message: response_text,
});
}
let result: KeysValid = serde_json::from_str(&response_text)?;
Ok(result)
}
// =============================================================================
// Tests
// =============================================================================

View file

@ -245,6 +245,153 @@ pub struct RestoreResult {
pub message: String,
}
/// Session information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
/// Session ID
pub session_id: String,
/// Container name (e.g., "unsb-vm-abc123")
#[serde(default)]
pub container_name: String,
/// Session status: "running", "frozen", "stopped"
#[serde(default)]
pub status: String,
/// Network mode: "zerotrust" or "semitrusted"
#[serde(default)]
pub network_mode: String,
/// Shell type (e.g., "bash", "python3")
#[serde(default)]
pub shell: String,
/// Number of vCPUs
#[serde(default)]
pub vcpu: u32,
/// Memory in MB
#[serde(default)]
pub memory_mb: u32,
/// Whether the session is boosted
#[serde(default)]
pub boosted: bool,
/// Created timestamp
#[serde(default)]
pub created_at: String,
/// Last activity timestamp
#[serde(default)]
pub last_activity: String,
}
/// Service information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
/// Service ID
pub service_id: String,
/// Service name
#[serde(default)]
pub name: String,
/// Container name
#[serde(default)]
pub container_name: String,
/// Service status: "running", "frozen", "stopped", "locked"
#[serde(default)]
pub status: String,
/// Exposed ports
#[serde(default)]
pub ports: Vec<u16>,
/// Custom domains
#[serde(default)]
pub domains: Vec<String>,
/// Network mode
#[serde(default)]
pub network_mode: String,
/// Number of vCPUs
#[serde(default)]
pub vcpu: u32,
/// Memory in MB
#[serde(default)]
pub memory_mb: u32,
/// Whether the service is locked (cannot be modified)
#[serde(default)]
pub locked: bool,
/// Public URL for the service
#[serde(default)]
pub url: String,
/// Created timestamp
#[serde(default)]
pub created_at: String,
}
/// Result of shell command execution in a session
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellResult {
/// Command output (stdout + stderr)
#[serde(default)]
pub output: String,
/// Exit code
#[serde(default)]
pub exit_code: i32,
}
/// Result of validating API keys
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysValid {
/// Whether the keys are valid
pub valid: bool,
/// Account ID associated with the keys
#[serde(default)]
pub account_id: String,
/// Account email (if available)
#[serde(default)]
pub email: String,
/// Account plan/tier
#[serde(default)]
pub plan: String,
/// Error message if invalid
#[serde(default)]
pub error: String,
}
/// Options for creating a session
#[derive(Debug, Clone, Default)]
pub struct SessionCreateOptions {
/// Network mode: "zerotrust" (default) or "semitrusted"
pub network_mode: Option<String>,
/// Shell to use (e.g., "bash", "python3")
pub shell: Option<String>,
/// Number of vCPUs (default: 1)
pub vcpu: Option<u32>,
/// Whether to use tmux multiplexer
pub tmux: Option<bool>,
/// Whether to use screen multiplexer
pub screen: Option<bool>,
}
/// Options for creating a service
#[derive(Debug, Clone, Default)]
pub struct ServiceCreateOptions {
/// Network mode: "zerotrust" (default) or "semitrusted"
pub network_mode: Option<String>,
/// Number of vCPUs (default: 1)
pub vcpu: Option<u32>,
/// Custom domains for the service
pub domains: Option<Vec<String>>,
/// Bootstrap script content
pub bootstrap: Option<String>,
/// Bootstrap script URL
pub bootstrap_url: Option<String>,
}
/// Options for updating a service
#[derive(Debug, Clone, Default)]
pub struct ServiceUpdateOptions {
/// New service name
pub name: Option<String>,
/// New ports
pub ports: Option<Vec<u16>>,
/// New domains
pub domains: Option<Vec<String>>,
/// New vCPU count
pub vcpu: Option<u32>,
}
// =============================================================================
// Internal Response Types
// =============================================================================
@ -299,6 +446,26 @@ struct LanguagesCache {
timestamp: u64,
}
#[derive(Debug, Deserialize)]
struct SessionsListResponse {
sessions: Vec<Session>,
}
#[derive(Debug, Deserialize)]
struct ServicesListResponse {
services: Vec<Service>,
}
#[derive(Debug, Deserialize)]
struct EnvResponse {
env: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct EnvExportResponse {
content: String,
}
// =============================================================================
// Language Detection
// =============================================================================
@ -549,6 +716,8 @@ fn make_request<T: for<'de> Deserialize<'de>>(
let mut request = match method {
"GET" => client.get(&url),
"POST" => client.post(&url),
"PATCH" => client.patch(&url),
"PUT" => client.put(&url),
"DELETE" => client.delete(&url),
_ => client.get(&url),
};
@ -906,6 +1075,670 @@ pub fn delete_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<()> {
Ok(())
}
/// Lock a snapshot to prevent deletion.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to lock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Snapshot information
pub fn lock_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/lock", snapshot_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Unlock a snapshot to allow deletion.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to unlock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Snapshot information
pub fn unlock_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/unlock", snapshot_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Clone a snapshot to create a new snapshot with a different name.
///
/// # Arguments
/// * `snapshot_id` - Snapshot ID to clone
/// * `name` - Name for the new snapshot
/// * `creds` - API credentials
///
/// # Returns
/// New Snapshot information
pub fn clone_snapshot(snapshot_id: &str, name: &str, creds: &Credentials) -> Result<Snapshot> {
let path = format!("/snapshots/{}/clone", snapshot_id);
let body = serde_json::json!({
"name": name
});
make_request("POST", &path, creds, Some(&body))
}
// =============================================================================
// Session API Functions
// =============================================================================
/// List all sessions for the authenticated account.
///
/// # Arguments
/// * `creds` - API credentials
///
/// # Returns
/// Vector of Session information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let sessions = list_sessions(&creds)?;
/// for session in sessions {
/// println!("{}: {} ({})", session.session_id, session.container_name, session.status);
/// }
/// ```
pub fn list_sessions(creds: &Credentials) -> Result<Vec<Session>> {
let response: SessionsListResponse = make_request("GET", "/sessions", creds, None::<&()>)?;
Ok(response.sessions)
}
/// Get details of a specific session.
///
/// # Arguments
/// * `session_id` - Session ID to retrieve
/// * `creds` - API credentials
///
/// # Returns
/// Session information
pub fn get_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}", session_id);
make_request("GET", &path, creds, None::<&()>)
}
/// Create a new interactive session.
///
/// # Arguments
/// * `language` - Programming language/shell (e.g., "bash", "python")
/// * `creds` - API credentials
/// * `opts` - Optional session creation options
///
/// # Returns
/// Created Session information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
///
/// // Create a basic bash session
/// let session = create_session("bash", &creds, None)?;
///
/// // Create a session with options
/// let opts = SessionCreateOptions {
/// network_mode: Some("semitrusted".to_string()),
/// tmux: Some(true),
/// ..Default::default()
/// };
/// let session = create_session("bash", &creds, Some(opts))?;
/// ```
pub fn create_session(
language: &str,
creds: &Credentials,
opts: Option<SessionCreateOptions>,
) -> Result<Session> {
let mut body = serde_json::json!({
"language": language
});
if let Some(opts) = opts {
if let Some(network_mode) = opts.network_mode {
body["network_mode"] = serde_json::json!(network_mode);
}
if let Some(shell) = opts.shell {
body["shell"] = serde_json::json!(shell);
}
if let Some(vcpu) = opts.vcpu {
body["vcpu"] = serde_json::json!(vcpu);
}
if let Some(tmux) = opts.tmux {
body["tmux"] = serde_json::json!(tmux);
}
if let Some(screen) = opts.screen {
body["screen"] = serde_json::json!(screen);
}
}
make_request("POST", "/sessions", creds, Some(&body))
}
/// Delete (terminate) a session.
///
/// # Arguments
/// * `session_id` - Session ID to delete
/// * `creds` - API credentials
pub fn delete_session(session_id: &str, creds: &Credentials) -> Result<()> {
let path = format!("/sessions/{}", session_id);
let _: serde_json::Value = make_request("DELETE", &path, creds, None::<&()>)?;
Ok(())
}
/// Freeze a session to save resources while preserving state.
///
/// Frozen sessions can be unfrozen later to resume work.
///
/// # Arguments
/// * `session_id` - Session ID to freeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub fn freeze_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/freeze", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Unfreeze a frozen session to resume work.
///
/// # Arguments
/// * `session_id` - Session ID to unfreeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub fn unfreeze_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/unfreeze", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Boost a session's resources (increase vCPU and memory).
///
/// Memory is derived from vCPU: vcpu * 2048MB.
///
/// # Arguments
/// * `session_id` - Session ID to boost
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub fn boost_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/boost", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Remove boost from a session (return to base resources).
///
/// # Arguments
/// * `session_id` - Session ID to unboost
/// * `creds` - API credentials
///
/// # Returns
/// Updated Session information
pub fn unboost_session(session_id: &str, creds: &Credentials) -> Result<Session> {
let path = format!("/sessions/{}/unboost", session_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Execute a shell command in a session.
///
/// # Arguments
/// * `session_id` - Session ID to execute command in
/// * `command` - Command to execute
/// * `creds` - API credentials
///
/// # Returns
/// ShellResult with output and exit code
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = shell_session("session-123", "ls -la", &creds)?;
/// println!("Output: {}", result.output);
/// println!("Exit code: {}", result.exit_code);
/// ```
pub fn shell_session(session_id: &str, command: &str, creds: &Credentials) -> Result<ShellResult> {
let path = format!("/sessions/{}/shell", session_id);
let body = serde_json::json!({
"command": command
});
make_request("POST", &path, creds, Some(&body))
}
// =============================================================================
// Service API Functions
// =============================================================================
/// List all services for the authenticated account.
///
/// # Arguments
/// * `creds` - API credentials
///
/// # Returns
/// Vector of Service information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let services = list_services(&creds)?;
/// for service in services {
/// println!("{}: {} ({}) - {}", service.service_id, service.name, service.status, service.url);
/// }
/// ```
pub fn list_services(creds: &Credentials) -> Result<Vec<Service>> {
let response: ServicesListResponse = make_request("GET", "/services", creds, None::<&()>)?;
Ok(response.services)
}
/// Create a new persistent service.
///
/// # Arguments
/// * `name` - Service name (used in URL: name.on.unsandbox.com)
/// * `ports` - Ports to expose
/// * `bootstrap` - Bootstrap script content to run on startup
/// * `creds` - API credentials
/// * `opts` - Optional service creation options
///
/// # Returns
/// Created Service information
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
///
/// // Create a simple web service
/// let service = create_service(
/// "myapp",
/// &[8080],
/// "python3 -m http.server 8080",
/// &creds,
/// None
/// )?;
/// println!("Service URL: {}", service.url);
///
/// // Create with options
/// let opts = ServiceCreateOptions {
/// network_mode: Some("semitrusted".to_string()),
/// vcpu: Some(2),
/// domains: Some(vec!["example.com".to_string()]),
/// ..Default::default()
/// };
/// let service = create_service("myapp", &[80, 443], bootstrap, &creds, Some(opts))?;
/// ```
pub fn create_service(
name: &str,
ports: &[u16],
bootstrap: &str,
creds: &Credentials,
opts: Option<ServiceCreateOptions>,
) -> Result<Service> {
let mut body = serde_json::json!({
"name": name,
"ports": ports,
"bootstrap": bootstrap
});
if let Some(opts) = opts {
if let Some(network_mode) = opts.network_mode {
body["network_mode"] = serde_json::json!(network_mode);
}
if let Some(vcpu) = opts.vcpu {
body["vcpu"] = serde_json::json!(vcpu);
}
if let Some(domains) = opts.domains {
body["domains"] = serde_json::json!(domains);
}
if let Some(bootstrap_url) = opts.bootstrap_url {
body["bootstrap_url"] = serde_json::json!(bootstrap_url);
}
}
make_request("POST", "/services", creds, Some(&body))
}
/// Get details of a specific service.
///
/// # Arguments
/// * `service_id` - Service ID to retrieve
/// * `creds` - API credentials
///
/// # Returns
/// Service information
pub fn get_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}", service_id);
make_request("GET", &path, creds, None::<&()>)
}
/// Update a service's configuration.
///
/// # Arguments
/// * `service_id` - Service ID to update
/// * `creds` - API credentials
/// * `opts` - Update options (name, ports, domains, vcpu)
///
/// # Returns
/// Updated Service information
pub fn update_service(
service_id: &str,
creds: &Credentials,
opts: ServiceUpdateOptions,
) -> Result<Service> {
let path = format!("/services/{}", service_id);
let mut body = serde_json::Map::new();
if let Some(name) = opts.name {
body.insert("name".to_string(), serde_json::json!(name));
}
if let Some(ports) = opts.ports {
body.insert("ports".to_string(), serde_json::json!(ports));
}
if let Some(domains) = opts.domains {
body.insert("domains".to_string(), serde_json::json!(domains));
}
if let Some(vcpu) = opts.vcpu {
body.insert("vcpu".to_string(), serde_json::json!(vcpu));
}
make_request("PATCH", &path, creds, Some(&serde_json::Value::Object(body)))
}
/// Delete (destroy) a service.
///
/// # Arguments
/// * `service_id` - Service ID to delete
/// * `creds` - API credentials
pub fn delete_service(service_id: &str, creds: &Credentials) -> Result<()> {
let path = format!("/services/{}", service_id);
let _: serde_json::Value = make_request("DELETE", &path, creds, None::<&()>)?;
Ok(())
}
/// Freeze a service to save resources while preserving state.
///
/// # Arguments
/// * `service_id` - Service ID to freeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub fn freeze_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/freeze", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Unfreeze a frozen service to resume operation.
///
/// # Arguments
/// * `service_id` - Service ID to unfreeze
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub fn unfreeze_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/unfreeze", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Lock a service to prevent modifications.
///
/// # Arguments
/// * `service_id` - Service ID to lock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub fn lock_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/lock", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Unlock a service to allow modifications.
///
/// # Arguments
/// * `service_id` - Service ID to unlock
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub fn unlock_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/unlock", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Get bootstrap logs for a service.
///
/// # Arguments
/// * `service_id` - Service ID to get logs for
/// * `all` - If true, get all logs; if false, get last 9000 lines
/// * `creds` - API credentials
///
/// # Returns
/// Log content as string
pub fn get_service_logs(service_id: &str, all: bool, creds: &Credentials) -> Result<String> {
let path = if all {
format!("/services/{}/logs?all=true", service_id)
} else {
format!("/services/{}/logs", service_id)
};
#[derive(Deserialize)]
struct LogsResponse {
#[serde(default)]
logs: String,
}
let response: LogsResponse = make_request("GET", &path, creds, None::<&()>)?;
Ok(response.logs)
}
/// Get environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to get env for
/// * `creds` - API credentials
///
/// # Returns
/// HashMap of environment variable key-value pairs
pub fn get_service_env(service_id: &str, creds: &Credentials) -> Result<HashMap<String, String>> {
let path = format!("/services/{}/env", service_id);
let response: EnvResponse = make_request("GET", &path, creds, None::<&()>)?;
Ok(response.env)
}
/// Set environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to set env for
/// * `env` - HashMap of environment variable key-value pairs
/// * `creds` - API credentials
pub fn set_service_env(
service_id: &str,
env: &HashMap<String, String>,
creds: &Credentials,
) -> Result<()> {
let path = format!("/services/{}/env", service_id);
// Convert to .env format
let content: String = env
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("\n");
// Use PUT with text/plain content type
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(120))
.build()?;
let url = format!("{}{}", API_BASE, path);
let timestamp = get_timestamp();
let signature = sign_request(&creds.secret_key, timestamp, "PUT", &path, &content);
let response = client
.put(&url)
.header("Authorization", format!("Bearer {}", creds.public_key))
.header("X-Timestamp", timestamp.to_string())
.header("X-Signature", signature)
.header("Content-Type", "text/plain")
.header("User-Agent", "un-rust-sync/2.0")
.body(content)
.send()?;
let status = response.status().as_u16();
if status < 200 || status >= 300 {
let response_text = response.text()?;
return Err(UnsandboxError::ApiError {
status,
message: response_text,
});
}
Ok(())
}
/// Delete environment variables for a service.
///
/// # Arguments
/// * `service_id` - Service ID to delete env for
/// * `keys` - List of environment variable keys to delete
/// * `creds` - API credentials
pub fn delete_service_env(
service_id: &str,
keys: &[&str],
creds: &Credentials,
) -> Result<()> {
let path = format!("/services/{}/env", service_id);
let body = serde_json::json!({
"keys": keys
});
let _: serde_json::Value = make_request("DELETE", &path, creds, Some(&body))?;
Ok(())
}
/// Export environment variables for a service in .env format.
///
/// # Arguments
/// * `service_id` - Service ID to export env for
/// * `creds` - API credentials
///
/// # Returns
/// Environment variables in .env format string
pub fn export_service_env(service_id: &str, creds: &Credentials) -> Result<String> {
let path = format!("/services/{}/env/export", service_id);
let body = serde_json::json!({});
let response: EnvExportResponse = make_request("POST", &path, creds, Some(&body))?;
Ok(response.content)
}
/// Redeploy a service with a new bootstrap script.
///
/// # Arguments
/// * `service_id` - Service ID to redeploy
/// * `creds` - API credentials
///
/// # Returns
/// Updated Service information
pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service> {
let path = format!("/services/{}/redeploy", service_id);
let body = serde_json::json!({});
make_request("POST", &path, creds, Some(&body))
}
/// Execute a command in a service container.
///
/// # Arguments
/// * `service_id` - Service ID to execute command in
/// * `command` - Command to execute
/// * `creds` - API credentials
///
/// # Returns
/// ExecuteResult with output and exit code
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = execute_in_service("service-123", "ls -la /app", &creds)?;
/// println!("Output: {}", result.output);
/// ```
pub fn execute_in_service(
service_id: &str,
command: &str,
creds: &Credentials,
) -> Result<ExecuteResult> {
let path = format!("/services/{}/execute", service_id);
let body = serde_json::json!({
"command": command,
"timeout": 30000
});
make_request("POST", &path, creds, Some(&body))
}
// =============================================================================
// Key Validation API Functions
// =============================================================================
/// Validate API keys.
///
/// # Arguments
/// * `creds` - API credentials to validate
///
/// # Returns
/// KeysValid with validation result and account info
///
/// # Examples
/// ```ignore
/// let creds = resolve_credentials(None, None)?;
/// let result = validate_keys(&creds)?;
/// if result.valid {
/// println!("Keys valid for account: {}", result.account_id);
/// } else {
/// println!("Invalid keys: {}", result.error);
/// }
/// ```
pub fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
// Note: This endpoint is on the portal, not the API
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
let url = "https://unsandbox.com/keys/validate";
let path = "/keys/validate";
let timestamp = get_timestamp();
let body_str = "";
let signature = sign_request(&creds.secret_key, timestamp, "POST", path, body_str);
let response = client
.post(url)
.header("Authorization", format!("Bearer {}", creds.public_key))
.header("X-Timestamp", timestamp.to_string())
.header("X-Signature", signature)
.header("Content-Type", "application/json")
.header("User-Agent", "un-rust-sync/2.0")
.body("")
.send()?;
let status = response.status().as_u16();
let response_text = response.text()?;
if status < 200 || status >= 300 {
return Err(UnsandboxError::ApiError {
status,
message: response_text,
});
}
let result: KeysValid = serde_json::from_str(&response_text)?;
Ok(result)
}
// =============================================================================
// Tests
// =============================================================================