feat: add HTTP 428 sudo OTP handling to all 42 SDK implementations

When destructive operations (service/snapshot/image destroy/unlock) receive
HTTP 428 Precondition Required, the SDK now:
1. Extracts challenge_id from the JSON response
2. Prompts: 'Confirmation required. Check your email for a one-time code.'
3. Reads OTP from stdin
4. Retries with X-Sudo-OTP and X-Sudo-Challenge headers

Languages updated: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart,
.NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java,
JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP,
PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl,
TypeScript, V, Zig
This commit is contained in:
russell@unturf.com 2026-02-05 16:23:19 -05:00
parent 1b06666f11
commit a5155aed4f
42 changed files with 5163 additions and 321 deletions

View file

@ -128,6 +128,111 @@ function Invoke-Api {
}
}
# Internal API request function that returns status code info for sudo handling
function Invoke-ApiInternal {
param($Endpoint, $Method = "GET", $Body = $null, $SudoOtp = $null, $SudoChallengeId = $null)
$publicKey, $secretKey = Get-ApiKeys
$headers = @{
"Authorization" = "Bearer $publicKey"
"Content-Type" = "application/json"
}
# Add HMAC signature if secret key exists
if ($secretKey) {
$timestamp = [int][double]::Parse((Get-Date -UFormat %s))
$bodyContent = if ($Body) { $Body } else { "" }
$sigInput = "${timestamp}:${Method}:${Endpoint}:${bodyContent}"
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($secretKey)
$hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sigInput))
$signature = [System.BitConverter]::ToString($hash).Replace("-", "").ToLower()
$headers["X-Timestamp"] = $timestamp.ToString()
$headers["X-Signature"] = $signature
}
# Add sudo OTP headers if provided
if ($SudoOtp) {
$headers["X-Sudo-OTP"] = $SudoOtp
}
if ($SudoChallengeId) {
$headers["X-Sudo-Challenge"] = $SudoChallengeId
}
$uri = "$API_BASE$Endpoint"
try {
if ($Body) {
$response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers -Body $Body
} else {
$response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers
}
return @{ Success = $true; Response = $response; StatusCode = 200 }
} catch {
$statusCode = 0
$responseBody = ""
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
$responseBody = $reader.ReadToEnd()
}
return @{ Success = $false; StatusCode = $statusCode; ResponseBody = $responseBody; Error = $_.Exception.Message }
}
}
# Handle 428 sudo OTP challenge - prompts user for OTP and retries the request
function Invoke-SudoChallenge {
param($ResponseBody, $Endpoint, $Method, $Body)
# Extract challenge_id from response
$challengeId = $null
try {
$parsed = $ResponseBody | ConvertFrom-Json
$challengeId = $parsed.challenge_id
} catch {}
Write-Host "`e[33mConfirmation required. Check your email for a one-time code.`e[0m" -ForegroundColor Yellow
$otp = Read-Host "Enter OTP"
if (-not $otp) {
Write-Error "Operation cancelled"
exit 1
}
$otp = $otp.Trim()
# Retry the request with sudo headers
return Invoke-ApiInternal -Endpoint $Endpoint -Method $Method -Body $Body -SudoOtp $otp -SudoChallengeId $challengeId
}
# Wrapper for destructive operations that may require 428 sudo OTP
function Invoke-ApiWithSudo {
param($Endpoint, $Method = "GET", $Body = $null)
$result = Invoke-ApiInternal -Endpoint $Endpoint -Method $Method -Body $Body
if (-not $result.Success -and $result.StatusCode -eq 428) {
$retryResult = Invoke-SudoChallenge -ResponseBody $result.ResponseBody -Endpoint $Endpoint -Method $Method -Body $Body
if (-not $retryResult.Success) {
Write-Error "Error: $($retryResult.Error)"
exit 1
}
return $retryResult.Response
}
if (-not $result.Success) {
Write-Error "Error: $($result.Error)"
exit 1
}
return $result.Response
}
function Invoke-ApiText {
param($Endpoint, $Method, $Body, $BaseUrl = $null)
@ -583,7 +688,7 @@ function Invoke-Image {
}
if ($deleteId) {
Invoke-Api -Endpoint "/images/$deleteId" -Method "DELETE"
Invoke-ApiWithSudo -Endpoint "/images/$deleteId" -Method "DELETE"
Write-Host "`e[32mImage deleted: $deleteId`e[0m"
return
}
@ -595,7 +700,7 @@ function Invoke-Image {
}
if ($unlockId) {
Invoke-Api -Endpoint "/images/$unlockId/unlock" -Method "POST" -Body "{}"
Invoke-ApiWithSudo -Endpoint "/images/$unlockId/unlock" -Method "POST" -Body "{}"
Write-Host "`e[32mImage unlocked: $unlockId`e[0m"
return
}
@ -731,7 +836,7 @@ function Invoke-Service {
if ($Args -contains "--destroy") {
$idx = [array]::IndexOf($Args, "--destroy")
$serviceId = $Args[$idx + 1]
Invoke-Api -Endpoint "/services/$serviceId" -Method "DELETE"
Invoke-ApiWithSudo -Endpoint "/services/$serviceId" -Method "DELETE"
Write-Host "`e[32mService destroyed: $serviceId`e[0m"
return
}