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:
parent
1b06666f11
commit
a5155aed4f
42 changed files with 5163 additions and 321 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue