OnetimeSecretV2.psm1
|
#Requires -Version 5.1 # OnetimeSecretV2 0.2.0 -- generated by OnetimeSecretV2.build.ps1; edit src/ instead. Set-StrictMode -Version Latest $script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = [string] (Import-PowerShellDataFile -Path (Join-Path $PSScriptRoot 'OnetimeSecretV2.psd1')).ModuleVersion $script:DefaultConnection = $null # Windows PowerShell 5.1 inherits the .NET Framework default protocol list, # which on older Windows builds still excludes TLS 1.2. if ($PSVersionTable.PSEdition -ne 'Core') { try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 } catch { Write-Verbose ('Could not enable TLS 1.2: {0}' -f $_.Exception.Message) } } #region ConvertFrom-OtsRecord.ps1 # Converters from raw API records to the typed objects the module emits. # Each output object carries a PSTypeName so OnetimeSecretV2.Format.ps1xml can # shape its default view, and keeps the untouched API record in # RawResponse for anything the typed projection does not cover. function ConvertFrom-OtsReceipt { <# .SYNOPSIS Projects a receipt record to an 'OnetimeSecret.Receipt' object. .PARAMETER Record The receipt record from the API (record, or record.receipt for create calls). .PARAMETER Details The optional details envelope that accompanies show/burn responses. .PARAMETER SecretRecord For create responses, the sibling secret record (supplies the generated value). .PARAMETER Connection Used to construct share/receipt/burn URLs when the API omits them. #> [CmdletBinding()] [OutputType('OnetimeSecret.Receipt')] param( [Parameter(Mandatory)] [AllowNull()] [object] $Record, [Parameter()] [AllowNull()] [object] $Details, [Parameter()] [AllowNull()] [object] $SecretRecord, [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter()] [AllowNull()] [string] $ShareDomain ) if ($null -eq $Record) { return $null } $receiptId = [string] (Get-OtsProperty $Record 'identifier') $secretId = [string] (Get-OtsProperty $Record 'secret_identifier') if (-not $secretId -and $null -ne $SecretRecord) { $secretId = [string] (Get-OtsProperty $SecretRecord 'identifier') } if (-not $ShareDomain) { $ShareDomain = [string] (Get-OtsProperty $Record 'share_domain') } $base = $Connection.ServerUri.AbsoluteUri.TrimEnd('/') $shareBase = $base if ($ShareDomain) { $shareBase = '{0}://{1}' -f $Connection.ServerUri.Scheme, $ShareDomain } # After a burn the server reports share_url with an empty identifier, so # only trust it when it still names the secret. $shareUrl = [string] (Get-OtsProperty $Record 'share_url') if ($secretId) { if (-not $shareUrl -or $shareUrl -notlike ('*' + $secretId)) { $shareUrl = '{0}/secret/{1}' -f $shareBase, $secretId } } else { $shareUrl = $null } $receiptUrl = [string] (Get-OtsProperty $Record 'receipt_url') if (-not $receiptUrl -and $receiptId) { $receiptUrl = '{0}/receipt/{1}' -f $base, $receiptId } $burnUrl = [string] (Get-OtsProperty $Record 'burn_url') if (-not $burnUrl -and $receiptId) { $burnUrl = '{0}/receipt/{1}/burn' -f $base, $receiptId } # The generated value is reported on the secret record at creation time and # under details.secret_value when a receipt is shown shortly afterwards. $value = $null if ($null -ne $SecretRecord) { $value = Get-OtsProperty $SecretRecord 'secret_value' } if ([string]::IsNullOrEmpty([string] $value) -and $null -ne $Details) { $value = Get-OtsProperty $Details 'secret_value' } if ([string]::IsNullOrEmpty([string] $value)) { $value = $null } $hasPassphrase = Get-OtsProperty $Record 'has_passphrase' if ($null -eq $hasPassphrase -and $null -ne $Details) { $hasPassphrase = Get-OtsProperty $Details 'has_passphrase' } $expiration = Get-OtsProperty $Record 'expiration' $expiresIn = ConvertFrom-OtsDuration (Get-OtsProperty $Record 'expiration_in_seconds') $secretTtl = ConvertFrom-OtsDuration (Get-OtsProperty $Record 'secret_ttl') $expires = ConvertFrom-OtsTimestamp $expiration if ($null -eq $expires -and $null -ne $expiresIn) { $expires = (Get-Date).Add($expiresIn) } if ($null -eq $expires -and $null -ne $secretTtl) { $created = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'created') if ($null -ne $created) { $expires = $created.Add($secretTtl) } } $viewCount = $null if ($null -ne $Details) { $viewCount = Get-OtsProperty $Details 'view_count' } [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.Receipt' ReceiptId = $receiptId SecretId = $secretId State = [string] (Get-OtsProperty $Record 'state') ShareUrl = $shareUrl ReceiptUrl = $receiptUrl BurnUrl = $burnUrl ShareDomain = $ShareDomain HasPassphrase = ConvertTo-OtsBoolean $hasPassphrase Recipients = ConvertTo-OtsStringArray (Get-OtsProperty $Record 'recipients') Memo = [string] (Get-OtsProperty $Record 'memo') Kind = [string] (Get-OtsProperty $Record 'kind') Value = $value Created = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'created') Updated = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'updated') Expires = $expires ExpiresIn = $expiresIn SecretTtl = $secretTtl ReceiptTtl = ConvertFrom-OtsDuration (Get-OtsProperty $Record 'receipt_ttl') NaturalExpiration = [string] (Get-OtsProperty $Record 'natural_expiration') IsViewed = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_viewed') IsReceived = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_received') IsBurned = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_burned') IsExpired = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_expired') IsOrphaned = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_orphaned') IsDestroyed = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_destroyed') Received = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'received') Burned = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'burned') ViewCount = $viewCount ShortId = [string] (Get-OtsProperty $Record 'shortid') SecretShortId = [string] (Get-OtsProperty $Record 'secret_shortid') ServerUri = $Connection.ServerUri RawResponse = $Record } } function ConvertFrom-OtsSecret { <# .SYNOPSIS Projects a secret record to an 'OnetimeSecret.Secret' object. #> [CmdletBinding()] [OutputType('OnetimeSecret.Secret')] param( [Parameter(Mandatory)] [AllowNull()] [object] $Record, [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter()] [switch] $AsSecureString ) if ($null -eq $Record) { return $null } $value = Get-OtsProperty $Record 'secret_value' if ([string]::IsNullOrEmpty([string] $value)) { $value = $null } elseif ($AsSecureString) { $value = ConvertTo-OtsSecureString -Value ([string] $value) } $secretId = [string] (Get-OtsProperty $Record 'identifier') [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.Secret' SecretId = $secretId State = [string] (Get-OtsProperty $Record 'state') Value = $value HasPassphrase = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'has_passphrase') DisplayType = [string] (Get-OtsProperty $Record 'display_type' 'text') Ttl = ConvertFrom-OtsDuration (Get-OtsProperty $Record 'secret_ttl') Lifespan = [string] (Get-OtsProperty $Record 'lifespan') Created = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'created') Updated = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'updated') IsViewed = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_viewed') IsReceived = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_received') ShortId = [string] (Get-OtsProperty $Record 'shortid') ShareUrl = '{0}/secret/{1}' -f $Connection.ServerUri.AbsoluteUri.TrimEnd('/'), $secretId ServerUri = $Connection.ServerUri RawResponse = $Record } } function ConvertFrom-OtsSecretStatus { <# .SYNOPSIS Projects a status record to an 'OnetimeSecret.SecretStatus' object. #> [CmdletBinding()] [OutputType('OnetimeSecret.SecretStatus')] param( [Parameter(Mandatory)] [AllowNull()] [object] $Record, [Parameter()] [AllowNull()] [object] $Details, [Parameter()] [AllowEmptyString()] [string] $RequestedId, [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection ) $state = [string] (Get-OtsProperty $Record 'state' 'unknown') $secretId = [string] (Get-OtsProperty $Record 'identifier') if (-not $secretId) { $secretId = $RequestedId } $currentExpiration = $null if ($null -ne $Details) { $currentExpiration = ConvertFrom-OtsTimestamp (Get-OtsProperty $Details 'current_expiration') } [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.SecretStatus' SecretId = $secretId State = $state Exists = ($state -ne 'unknown') HasPassphrase = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'has_passphrase') Expires = $currentExpiration Ttl = ConvertFrom-OtsDuration (Get-OtsProperty $Record 'secret_ttl') Created = ConvertFrom-OtsTimestamp (Get-OtsProperty $Record 'created') IsViewed = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_viewed') IsReceived = ConvertTo-OtsBoolean (Get-OtsProperty $Record 'is_received') ServerUri = $Connection.ServerUri RawResponse = $Record } } #endregion ConvertFrom-OtsRecord.ps1 #region ConvertFrom-OtsSecureString.ps1 function ConvertFrom-OtsSecureString { <# .SYNOPSIS Extracts the plain-text value of a SecureString. .DESCRIPTION Works on Windows PowerShell 5.1 and PowerShell 7 alike (the -AsPlainText switch of ConvertFrom-SecureString is 7.0+ only). The unmanaged copy is zeroed as soon as the managed string has been produced. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [securestring] $SecureString ) $pointer = [System.IntPtr]::Zero try { $pointer = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) } finally { if ($pointer -ne [System.IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } } } function ConvertTo-OtsSecureString { <# .SYNOPSIS Wraps a plain string in a SecureString without the -Force dance. #> [CmdletBinding()] [OutputType([securestring])] param( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [string] $Value ) $secure = New-Object -TypeName System.Security.SecureString foreach ($char in $Value.ToCharArray()) { $secure.AppendChar($char) } $secure.MakeReadOnly() return $secure } #endregion ConvertFrom-OtsSecureString.ps1 #region ConvertFrom-OtsValue.ps1 # Small coercion helpers for the loosely typed values the API returns. # Timestamps arrive as epoch numbers, epoch strings or ISO-8601 strings; # booleans arrive as true/false, "true"/"false" or null; durations arrive # as seconds in either numeric or string form. function ConvertFrom-OtsTimestamp { <# .SYNOPSIS Converts an API timestamp (epoch seconds or ISO-8601) to a local DateTime. #> [CmdletBinding()] [OutputType([datetime])] param( [Parameter(Position = 0)] [AllowNull()] [object] $Value ) if ($null -eq $Value) { return $null } $text = ([string] $Value).Trim() if ($text.Length -eq 0) { return $null } $seconds = 0.0 $isNumber = [double]::TryParse( $text, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref] $seconds) if ($isNumber) { if ($seconds -le 0) { return $null } $epoch = New-Object -TypeName System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, ([System.DateTimeKind]::Utc) return $epoch.AddSeconds($seconds).ToLocalTime() } $parsed = [datetime]::MinValue $styles = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal if ([datetime]::TryParse($text, [System.Globalization.CultureInfo]::InvariantCulture, $styles, [ref] $parsed)) { return [datetime]::SpecifyKind($parsed, [System.DateTimeKind]::Utc).ToLocalTime() } return $null } function ConvertFrom-OtsDuration { <# .SYNOPSIS Converts a duration in seconds (number or numeric string) to a TimeSpan. #> [CmdletBinding()] [OutputType([timespan])] param( [Parameter(Position = 0)] [AllowNull()] [object] $Value ) if ($null -eq $Value) { return $null } $seconds = 0.0 $text = ([string] $Value).Trim() if ($text.Length -eq 0) { return $null } if ([double]::TryParse($text, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref] $seconds)) { return [timespan]::FromSeconds($seconds) } return $null } function ConvertTo-OtsBoolean { <# .SYNOPSIS Normalises the API's mixed boolean representations to [bool]. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Position = 0)] [AllowNull()] [object] $Value ) if ($null -eq $Value) { return $false } if ($Value -is [bool]) { return $Value } $text = ([string] $Value).Trim().ToLowerInvariant() switch ($text) { '' { return $false } 'false' { return $false } '0' { return $false } 'no' { return $false } 'true' { return $true } '1' { return $true } 'yes' { return $true } } # Anything else non-empty (for example a timestamp recorded when an event # happened) counts as "it happened". return $true } function ConvertTo-OtsStringArray { <# .SYNOPSIS Normalises a scalar-or-array API value to a string array. #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Position = 0)] [AllowNull()] [object] $Value ) if ($null -eq $Value) { return @() } if ($Value -is [string]) { if ($Value.Trim().Length -eq 0) { return @() } return @($Value.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } return @($Value | ForEach-Object { [string] $_ } | Where-Object { $_ }) } #endregion ConvertFrom-OtsValue.ps1 #region ConvertTo-OtsIdentifier.ps1 function ConvertTo-OtsIdentifier { <# .SYNOPSIS Accepts a bare identifier or a share/receipt URL and returns the identifier. .DESCRIPTION People paste the link they were given at least as often as the key inside it, so every cmdlet that takes an identifier accepts "https://eu.onetimesecret.com/secret/abc123", "/secret/abc123" or plain "abc123". The identifier is the path segment after /secret/ or /receipt/ (and the legacy /private/ alias). #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [string] $Value, [Parameter()] [ValidateSet('Secret', 'Receipt', 'Any')] [string] $Kind = 'Any' ) $candidate = $Value.Trim() $segments = switch ($Kind) { 'Secret' { 'secret' } 'Receipt' { 'receipt|private|metadata' } default { 'secret|receipt|private|metadata' } } $pattern = '(?:^|/)(?:{0})/(?<id>[A-Za-z0-9_-]+)' -f $segments if ($candidate -match $pattern) { return $Matches['id'] } if ($candidate -match '^[A-Za-z0-9_-]+$') { return $candidate } throw ("'{0}' is not a valid Onetime Secret identifier or link." -f $Value) } #endregion ConvertTo-OtsIdentifier.ps1 #region Get-OtsPathPrefix.ps1 function Get-OtsPathPrefix { <# .SYNOPSIS Returns 'guest/' for anonymous connections, '' otherwise. .DESCRIPTION The v2 API mirrors the secret and receipt endpoints under /guest for unauthenticated use. Endpoints without a guest twin (listing receipts, updating a memo, status checks) do not call this helper. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection ) if ($Connection.IsAnonymous) { return 'guest/' } return '' } function Assert-OtsAuthenticated { <# .SYNOPSIS Throws when an authenticated-only operation is attempted anonymously. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter(Mandatory)] [string] $Operation ) if ($Connection.IsAnonymous) { throw ('{0} requires an authenticated connection. Run Connect-OtsAccount with -Credential.' -f $Operation) } } #endregion Get-OtsPathPrefix.ps1 #region Get-OtsProperty.ps1 function Get-OtsProperty { <# .SYNOPSIS Reads a property from an API response object without tripping strict mode. .DESCRIPTION API payloads omit fields freely. This helper returns the property value when present (including an explicit null) and $Default otherwise, for both PSCustomObject (ConvertFrom-Json output) and hashtable input. #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Position = 0)] [AllowNull()] [object] $InputObject, [Parameter(Mandatory, Position = 1)] [string] $Name, [Parameter(Position = 2)] [AllowNull()] [object] $Default = $null ) if ($null -eq $InputObject) { return $Default } if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Contains($Name)) { return $InputObject[$Name] } return $Default } $property = $InputObject.PSObject.Properties[$Name] if ($null -ne $property) { return $property.Value } return $Default } #endregion Get-OtsProperty.ps1 #region Invoke-OtsRequest.ps1 function Invoke-OtsRequest { <# .SYNOPSIS Sends one request to the Onetime Secret API and returns the parsed JSON. .DESCRIPTION The single HTTP chokepoint for the module. It builds the URL from the connection's server URI and API version, applies Basic auth and the client identification headers, serialises the body as JSON, retries idempotent GETs on 429/502/503/504 with exponential backoff (honouring Retry-After), and converts failures into rich ErrorRecords via New-OtsErrorRecord. On PowerShell 7+ HTTP failures are read from the response (-SkipHttpErrorCheck) rather than caught as exceptions: the exception Invoke-WebRequest would throw carries the whole request, Authorization header included, and PowerShell records it in $Error even when it is caught here. Windows PowerShell 5.1 has no such switch; its WebException exposes only the response, so the catch path is safe there. #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory)] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter()] [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')] [string] $Method = 'GET', # Path relative to the API root, e.g. 'secret/conceal'. [Parameter(Mandatory)] [string] $Path, [Parameter()] [AllowNull()] [System.Collections.IDictionary] $Body, [Parameter()] [ValidateRange(0, 10)] [int] $MaxRetry = 2 ) $uri = '{0}/api/{1}/{2}' -f $Connection.ServerUri.AbsoluteUri.TrimEnd('/'), $Connection.ApiVersion, $Path.TrimStart('/') $headers = @{ 'Accept' = 'application/json' 'X-Onetime-Client' = 'powershell:{0}/{1}' -f $PSVersionTable.PSVersion, $script:ModuleVersion } if (-not $Connection.IsAnonymous) { $token = $Connection.Credential.GetNetworkCredential().Password $pair = '{0}:{1}' -f $Connection.Credential.UserName, $token $headers['Authorization'] = 'Basic ' + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair)) } $request = @{ Uri = $uri Method = $Method Headers = $headers UserAgent = $Connection.UserAgent TimeoutSec = $Connection.TimeoutSec UseBasicParsing = $true ErrorAction = 'Stop' } if ($null -ne $Body) { $json = ConvertTo-Json -InputObject $Body -Depth 10 -Compress $request['ContentType'] = 'application/json; charset=utf-8' $request['Body'] = [System.Text.Encoding]::UTF8.GetBytes($json) } if ($PSVersionTable.PSVersion.Major -ge 7) { $request['SkipHttpErrorCheck'] = $true } $attempt = 0 while ($true) { $failure = $null $cause = $null try { Write-Verbose ('{0} {1}' -f $Method, $uri) $response = Invoke-WebRequest @request $statusCode = 0 if ($null -ne $response -and $null -ne $response.PSObject.Properties['StatusCode']) { try { $statusCode = [int] $response.StatusCode } catch { $statusCode = 0 } } if ($statusCode -ge 400) { $failure = ConvertFrom-OtsHttpResponse -Response $response -StatusCode $statusCode } else { $content = $response.Content if ($content -is [byte[]]) { $content = [System.Text.Encoding]::UTF8.GetString($content) } if ([string]::IsNullOrWhiteSpace($content)) { return $null } # The parser's message is not kept as the cause: Windows # PowerShell 5.1 quotes the input in it, and reveal and # receipt bodies carry secret values. try { return ($content | ConvertFrom-Json) } catch { $failure = [PSCustomObject] @{ StatusCode = $statusCode; Body = $null; InvalidJson = $true } } } } catch { $failure = ConvertFrom-OtsHttpError -ErrorRecord $_ $cause = $_.Exception.Message } $retryable = ($Method -eq 'GET') -and ($failure.StatusCode -in @(429, 502, 503, 504)) -and ($attempt -lt $MaxRetry) if ($retryable) { $attempt++ $delay = [math]::Pow(2, $attempt) if ($failure.RetryAfter -gt 0) { $delay = [math]::Max($delay, $failure.RetryAfter) } Write-Verbose ('{0} from {1}; retry {2}/{3} in {4}s' -f $failure.StatusCode, $uri, $attempt, $MaxRetry, $delay) Start-Sleep -Seconds $delay continue } $PSCmdlet.ThrowTerminatingError((New-OtsErrorRecord -Failure $failure -Method $Method -Uri $uri -Cause $cause)) } } function ConvertFrom-OtsHttpResponse { <# .SYNOPSIS Builds the failure object for a non-2xx response returned with -SkipHttpErrorCheck. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [object] $Response, [Parameter(Mandatory)] [int] $StatusCode ) $retryAfter = 0 try { $headers = $Response.Headers if ($null -ne $headers) { $value = $null if ($headers -is [System.Collections.IDictionary]) { # Hashtable (mocks) and Dictionary<string, IEnumerable<string>> # (PowerShell 7) disagree on Contains/ContainsKey; walk the keys. foreach ($key in $headers.Keys) { if ($key -eq 'Retry-After') { $value = $headers[$key]; break } } } else { $value = $headers['Retry-After'] } $value = [string] (@($value) | Select-Object -First 1) if ($value -match '^\d+$') { $retryAfter = [int] $value } } } catch { Write-Debug ('Retry-After could not be read: {0}' -f $_.Exception.Message) } $rawBody = $Response.Content if ($rawBody -is [byte[]]) { $rawBody = [System.Text.Encoding]::UTF8.GetString($rawBody) } $rawBody = [string] $rawBody $body = $null if (-not [string]::IsNullOrWhiteSpace($rawBody)) { try { $body = $rawBody | ConvertFrom-Json } catch { $body = $null } } return [PSCustomObject] @{ StatusCode = $StatusCode Body = $body RawBody = $rawBody RetryAfter = $retryAfter } } function ConvertFrom-OtsHttpError { <# .SYNOPSIS Normalises the differing HTTP error shapes of Windows PowerShell and PowerShell 7. .DESCRIPTION Windows PowerShell throws System.Net.WebException with an HttpWebResponse; PowerShell 7 throws HttpResponseException with an HttpResponseMessage and puts the body in ErrorDetails (only reachable here through mocks, since Invoke-OtsRequest passes -SkipHttpErrorCheck on 7+). Connection failures have no response at all. All three end up as the same small object. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) $statusCode = 0 $rawBody = $null $retryAfter = 0 $response = $null $exception = $ErrorRecord.Exception if ($null -ne $exception -and $null -ne $exception.PSObject.Properties['Response']) { $response = $exception.Response } if ($null -ne $response) { try { $statusCode = [int] $response.StatusCode } catch { $statusCode = 0 } try { $responseHeaders = $response.Headers if ($responseHeaders -is [System.Net.WebHeaderCollection]) { $value = $responseHeaders['Retry-After'] if ($value -match '^\d+$') { $retryAfter = [int] $value } } elseif ($null -ne $responseHeaders.PSObject.Properties['RetryAfter'] -and $null -ne $responseHeaders.RetryAfter -and $null -ne $responseHeaders.RetryAfter.Delta) { $retryAfter = [int] $responseHeaders.RetryAfter.Delta.TotalSeconds } } catch { Write-Debug ('Retry-After could not be read: {0}' -f $_.Exception.Message) } } if ($null -ne $ErrorRecord.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($ErrorRecord.ErrorDetails.Message)) { $rawBody = $ErrorRecord.ErrorDetails.Message } elseif ($null -ne $response -and $null -ne $response.PSObject.Methods['GetResponseStream']) { try { $stream = $response.GetResponseStream() if ($null -ne $stream) { $reader = New-Object -TypeName System.IO.StreamReader -ArgumentList $stream $rawBody = $reader.ReadToEnd() $reader.Dispose() } } catch { Write-Debug ('Error body could not be read: {0}' -f $_.Exception.Message) } } $body = $null if (-not [string]::IsNullOrWhiteSpace($rawBody)) { try { $body = $rawBody | ConvertFrom-Json } catch { $body = $null } } return [PSCustomObject] @{ StatusCode = $statusCode Body = $body RawBody = $rawBody RetryAfter = $retryAfter } } function New-OtsErrorRecord { <# .SYNOPSIS Builds the ErrorRecord thrown for a failed API call. .DESCRIPTION The API's error envelope is { error, error_type, field, error_id, request_id } (ADR-013). Those fields are surfaced three ways so callers can branch however they prefer: - FullyQualifiedErrorId "OnetimeSecret.Api.<error_type>" (or Http<status>; OnetimeSecret.InvalidResponse for a 2xx body that is not JSON) - ErrorCategory mapped from the HTTP status - Exception.Data StatusCode, ErrorType, Field, ErrorId, RequestId, Uri, Method, Response The transport exception is deliberately not chained as InnerException: on PowerShell 7 it carries the HttpRequestMessage, Authorization header and body included, and anything that serialises the error (logging, ConvertTo-Json, transcripts) would write the credential out. Only its message survives, as a plain inner exception. #> [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory ErrorRecord; nothing outside the process changes.')] param( [Parameter(Mandatory)] [pscustomobject] $Failure, [Parameter(Mandatory)] [string] $Method, [Parameter(Mandatory)] [string] $Uri, # Message of the underlying transport error, when there was one. [Parameter()] [AllowNull()] [AllowEmptyString()] [string] $Cause ) $status = [int] $Failure.StatusCode $body = $Failure.Body $errorType = [string] (Get-OtsProperty $body 'error_type') $field = [string] (Get-OtsProperty $body 'field') $errorId = [string] (Get-OtsProperty $body 'error_id') $requestId = [string] (Get-OtsProperty $body 'request_id') $serverMessage = [string] (Get-OtsProperty $body 'error') if ([string]::IsNullOrWhiteSpace($serverMessage)) { $serverMessage = [string] (Get-OtsProperty $body 'message') } $accountRequired = ($field -eq 'requires_account') -or ($errorType -match 'AccountRequired|RequiresAccount') if ($Failure.PSObject.Properties['InvalidJson']) { $category = [System.Management.Automation.ErrorCategory]::InvalidResult $message = 'The response from {0} (HTTP {1}) is not valid JSON.' -f $Uri, $status $fqid = 'OnetimeSecret.InvalidResponse' $recommended = 'Check that the server URI points at a Onetime Secret server and not at a proxy or login page.' } elseif ($status -eq 0) { $category = [System.Management.Automation.ErrorCategory]::ConnectionError $message = 'Could not reach {0}: {1}' -f $Uri, $Cause $fqid = 'OnetimeSecret.ConnectionError' $recommended = 'Check the server URI and your network connection.' } else { $category = switch ($status) { 400 { [System.Management.Automation.ErrorCategory]::InvalidArgument } 401 { [System.Management.Automation.ErrorCategory]::AuthenticationError } 403 { [System.Management.Automation.ErrorCategory]::PermissionDenied } 404 { [System.Management.Automation.ErrorCategory]::ObjectNotFound } 409 { [System.Management.Automation.ErrorCategory]::ResourceExists } 422 { [System.Management.Automation.ErrorCategory]::InvalidData } 429 { [System.Management.Automation.ErrorCategory]::LimitsExceeded } default { if ($status -ge 500) { [System.Management.Automation.ErrorCategory]::ResourceUnavailable } else { [System.Management.Automation.ErrorCategory]::InvalidOperation } } } if ($accountRequired) { $category = [System.Management.Automation.ErrorCategory]::AuthenticationError } if ([string]::IsNullOrWhiteSpace($serverMessage)) { $serverMessage = switch ($status) { 401 { 'Authentication required.' } 403 { 'Permission denied.' } 404 { 'Not found.' } 422 { 'The request was rejected by validation.' } 429 { 'Rate limit exceeded.' } default { 'The request failed.' } } } $label = if ($errorType) { $errorType } else { 'HTTP {0}' -f $status } $message = 'Onetime Secret API returned {0} ({1}): {2}' -f $status, $label, $serverMessage if ($field) { $message += ' [field: {0}]' -f $field } if ($requestId) { $message += ' [request_id: {0}]' -f $requestId } $fqid = if ($errorType) { 'OnetimeSecret.Api.{0}' -f $errorType } else { 'OnetimeSecret.Api.Http{0}' -f $status } $recommended = switch ($status) { 401 { 'Check the customer extid (username) and API token (password) passed to Connect-OtsAccount.' } 404 { 'The secret or receipt may have been viewed, burned or expired, or the identifier is wrong.' } 429 { 'Slow down and retry later.' } default { $null } } if ($accountRequired) { $recommended = 'This operation needs an authenticated account. Run Connect-OtsAccount with a credential.' } } if ([string]::IsNullOrWhiteSpace($Cause)) { $exception = New-Object -TypeName System.Exception -ArgumentList $message } else { $exception = New-Object -TypeName System.Exception -ArgumentList $message, (New-Object -TypeName System.Exception -ArgumentList $Cause) } $exception.Data['StatusCode'] = $status $exception.Data['ErrorType'] = $errorType $exception.Data['Field'] = $field $exception.Data['ErrorId'] = $errorId $exception.Data['RequestId'] = $requestId $exception.Data['Method'] = $Method $exception.Data['Uri'] = $Uri $exception.Data['Response'] = $body $record = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, $fqid, $category, $Uri $record.ErrorDetails = New-Object -TypeName System.Management.Automation.ErrorDetails -ArgumentList $message if ($recommended) { $record.ErrorDetails.RecommendedAction = $recommended } return $record } #endregion Invoke-OtsRequest.ps1 #region New-OtsConnectionObject.ps1 # Connection construction and validation. A connection is a plain # PSCustomObject tagged with the 'OnetimeSecret.Connection' type name so it # can be bound with [PSTypeName()] on parameters without a PowerShell class # (which would not be visible to callers without `using module`). # The apex domain serves the marketing site; the API lives on the regional # hosts, self-hosted installs and custom domains. $script:ApexHosts = @('onetimesecret.com', 'www.onetimesecret.com') $script:RegionalHosts = @( 'https://us.onetimesecret.com' 'https://eu.onetimesecret.com' 'https://ca.onetimesecret.com' 'https://uk.onetimesecret.com' 'https://nz.onetimesecret.com' ) # A customer external id ("extid") is an opaque, case-insensitive identifier # that always begins with "ur". The API also accepts the account email. $script:CustomerExtidPattern = '^ur[a-z0-9]+$' function Resolve-OtsServerUri { <# .SYNOPSIS Validates and normalises a server base URI. #> [CmdletBinding()] [OutputType([uri])] param( [Parameter(Mandatory, Position = 0)] [string] $ServerUri ) $uri = $null if (-not [uri]::TryCreate($ServerUri.Trim(), [System.UriKind]::Absolute, [ref] $uri) -or $uri.Scheme -notin @('http', 'https')) { throw ("ServerUri must be an absolute http(s) URL such as {0}; got '{1}'." -f $script:RegionalHosts[0], $ServerUri) } if ($uri.Host.ToLowerInvariant() -in $script:ApexHosts) { throw ("{0} is the Onetime Secret website, not an API host. Use your region's host ({1}), your self-hosted domain or your custom domain." -f $uri.Host, ($script:RegionalHosts -join ', ')) } # Keep any path prefix (reverse-proxied installs) but drop a trailing slash # so request paths can be appended predictably. $builder = New-Object -TypeName System.UriBuilder -ArgumentList $uri $builder.Path = $builder.Path.TrimEnd('/') $builder.Query = '' $builder.Fragment = '' return $builder.Uri } function Test-OtsUsername { <# .SYNOPSIS Returns $true when a Basic-auth username looks like an extid or an email. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [string] $Username ) if ([string]::IsNullOrWhiteSpace($Username)) { return $false } if ($Username -match $script:CustomerExtidPattern) { return $true } return ($Username -match '^[^@\s]+@[^@\s]+\.[^@\s]+$') } function Get-OtsUserAgent { [CmdletBinding()] [OutputType([string])] param() $edition = if ($PSVersionTable.ContainsKey('PSEdition')) { $PSVersionTable.PSEdition } else { 'Desktop' } $os = [System.Environment]::OSVersion.Platform return 'OnetimeSecretV2-PowerShell/{0} (PowerShell/{1}; {2}; {3})' -f $script:ModuleVersion, $PSVersionTable.PSVersion, $edition, $os } function New-OtsConnectionObject { <# .SYNOPSIS Builds a validated 'OnetimeSecret.Connection' object. #> [CmdletBinding()] [OutputType('OnetimeSecret.Connection')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory connection object; nothing outside the process changes.')] param( [Parameter(Mandatory)] [string] $ServerUri, [Parameter()] [AllowNull()] [pscredential] $Credential, [Parameter()] [ValidateRange(1, 3600)] [int] $TimeoutSec = 30, [Parameter()] [AllowEmptyString()] [string] $UserAgent ) $resolvedUri = Resolve-OtsServerUri -ServerUri $ServerUri if ($null -ne $Credential -and -not (Test-OtsUsername -Username $Credential.UserName)) { throw ("'{0}' is not a customer extid or account email. The extid is the ""ur..."" identifier shown at the bottom of the user menu when you are signed in; pass it as the credential's username with your API token as the password." -f $Credential.UserName) } if ([string]::IsNullOrWhiteSpace($UserAgent)) { $UserAgent = Get-OtsUserAgent } $username = $null if ($null -ne $Credential) { $username = $Credential.UserName } $connection = [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.Connection' ServerUri = $resolvedUri Username = $username IsAnonymous = ($null -eq $Credential) ApiVersion = 'v2' TimeoutSec = $TimeoutSec UserAgent = $UserAgent Credential = $Credential } return $connection } function Resolve-OtsConnection { <# .SYNOPSIS Picks the connection a cmdlet should use. .DESCRIPTION Precedence: an explicit -Connection argument, then the module default set by Connect-OtsAccount, then the ONETIME_BASE_URL / ONETIME_CUSTOMER_EXTID / ONETIME_API_TOKEN environment variables (which become the module default on first use). #> [CmdletBinding()] [OutputType('OnetimeSecret.Connection')] param( [Parameter(Position = 0)] [AllowNull()] [object] $Connection ) if ($null -ne $Connection) { if ($Connection.PSObject.TypeNames -notcontains 'OnetimeSecret.Connection') { throw 'Connection must be an object returned by Connect-OtsAccount -PassThru or Get-OtsConnection.' } return $Connection } if ($null -ne $script:DefaultConnection) { return $script:DefaultConnection } $baseUrl = [System.Environment]::GetEnvironmentVariable('ONETIME_BASE_URL') if (-not [string]::IsNullOrWhiteSpace($baseUrl)) { $credential = $null $extid = [System.Environment]::GetEnvironmentVariable('ONETIME_CUSTOMER_EXTID') $token = [System.Environment]::GetEnvironmentVariable('ONETIME_API_TOKEN') if (-not [string]::IsNullOrWhiteSpace($extid) -and -not [string]::IsNullOrWhiteSpace($token)) { $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $extid, (ConvertTo-OtsSecureString -Value $token) } Write-Verbose 'No explicit connection; using ONETIME_* environment variables.' $script:DefaultConnection = New-OtsConnectionObject -ServerUri $baseUrl -Credential $credential return $script:DefaultConnection } throw 'Not connected. Run Connect-OtsAccount (or set ONETIME_BASE_URL, ONETIME_CUSTOMER_EXTID and ONETIME_API_TOKEN) first.' } #endregion New-OtsConnectionObject.ps1 #region Connect-OtsAccount.ps1 function Connect-OtsAccount { <# .SYNOPSIS Establishes the connection that other OnetimeSecretV2 cmdlets use. .DESCRIPTION Validates the server URI and credential, then stores the resulting connection as the module default so subsequent cmdlets need no -Connection argument. Use -PassThru to also return the connection object, which can be passed explicitly when talking to several servers at once. Authentication is HTTP Basic: the credential's username is your customer extid (the "ur..." identifier at the bottom of the user menu when signed in) or your account email, and the password is your API token from the account's API settings page. Alternatively, set the ONETIME_BASE_URL, ONETIME_CUSTOMER_EXTID and ONETIME_API_TOKEN environment variables and skip this cmdlet entirely; the module connects on first use. Onetime Secret runs region-isolated deployments, so there is no default server. Pick your region's host (https://us.onetimesecret.com, https://eu.onetimesecret.com, https://ca.onetimesecret.com, https://uk.onetimesecret.com, https://nz.onetimesecret.com), your self-hosted instance or your custom domain. .PARAMETER ServerUri Base URL of the API host, for example https://eu.onetimesecret.com. The apex domain onetimesecret.com is the website and is rejected. .PARAMETER Credential PSCredential whose username is the customer extid (or account email) and whose password is the API token. .PARAMETER Anonymous Connect without credentials. Only the guest endpoints are available (create, reveal, burn, show); listing receipts and setting memos are not. .PARAMETER TimeoutSec Per-request timeout in seconds. Defaults to 30. .PARAMETER UserAgent Override the User-Agent header. Defaults to a string identifying this module and the PowerShell version. .PARAMETER PassThru Return the connection object in addition to setting it as the default. .EXAMPLE Connect-OtsAccount -ServerUri https://eu.onetimesecret.com -Credential (Get-Credential -UserName 'ur1abc23def' -Message 'API token') Prompts for the API token and connects as the given customer. .EXAMPLE $token = ConvertTo-SecureString $env:ONETIME_API_TOKEN -AsPlainText -Force $cred = [pscredential]::new('ur1abc23def', $token) Connect-OtsAccount https://us.onetimesecret.com -Credential $cred Connects non-interactively from an automation script. .EXAMPLE $eu = Connect-OtsAccount https://eu.onetimesecret.com -Anonymous -PassThru New-OtsSecret -Secret 'hunter2' -Connection $eu Uses an explicit anonymous connection for a single call. .OUTPUTS OnetimeSecret.Connection (only with -PassThru) .LINK https://api.onetimesecret.com/doc/api-v2/authentication #> [CmdletBinding(DefaultParameterSetName = 'Credential')] [OutputType('OnetimeSecret.Connection')] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [Alias('BaseUrl', 'Server', 'Uri')] [string] $ServerUri, [Parameter(Mandatory, ParameterSetName = 'Credential')] [System.Management.Automation.Credential()] [pscredential] $Credential, [Parameter(Mandatory, ParameterSetName = 'Anonymous')] [switch] $Anonymous, [Parameter()] [ValidateRange(1, 3600)] [int] $TimeoutSec = 30, [Parameter()] [ValidateNotNullOrEmpty()] [string] $UserAgent, [Parameter()] [switch] $PassThru ) try { $parameters = @{ ServerUri = $ServerUri TimeoutSec = $TimeoutSec } if ($PSCmdlet.ParameterSetName -eq 'Credential') { $parameters['Credential'] = $Credential } if ($PSBoundParameters.ContainsKey('UserAgent')) { $parameters['UserAgent'] = $UserAgent } $connection = New-OtsConnectionObject @parameters $script:DefaultConnection = $connection if ($connection.IsAnonymous) { Write-Verbose ('Connected anonymously to {0}' -f $connection.ServerUri) } else { Write-Verbose ('Connected to {0} as {1}' -f $connection.ServerUri, $connection.Username) } if ($PassThru) { return $connection } } catch { $PSCmdlet.ThrowTerminatingError($_) } } #endregion Connect-OtsAccount.ps1 #region Disconnect-OtsAccount.ps1 function Disconnect-OtsAccount { <# .SYNOPSIS Forgets the default connection set by Connect-OtsAccount. .DESCRIPTION Clears the module-level default connection. Connection objects you obtained with -PassThru are unaffected; there is no server-side session to end because the API is authenticated per request. .EXAMPLE Disconnect-OtsAccount .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess)] [OutputType([void])] param() if ($null -eq $script:DefaultConnection) { Write-Verbose 'No default connection to clear.' return } $target = $script:DefaultConnection.ServerUri.ToString() if ($PSCmdlet.ShouldProcess($target, 'Clear default connection')) { $script:DefaultConnection = $null } } #endregion Disconnect-OtsAccount.ps1 #region Get-OtsConnection.ps1 function Get-OtsConnection { <# .SYNOPSIS Returns the connection the module is currently using. .DESCRIPTION Returns the default connection set by Connect-OtsAccount, or one built from the ONETIME_* environment variables when nothing has been set yet. Returns nothing when neither exists. .EXAMPLE Get-OtsConnection ServerUri Username IsAnonymous --------- -------- ----------- https://eu.onetimesecret.com/ ur1abc23def False .OUTPUTS OnetimeSecret.Connection #> [CmdletBinding()] [OutputType('OnetimeSecret.Connection')] param() try { Resolve-OtsConnection } catch { Write-Verbose $_.Exception.Message } } #endregion Get-OtsConnection.ps1 #region Get-OtsReceipt.ps1 function Get-OtsReceipt { <# .SYNOPSIS Gets receipts: the creator-side record of a secret's state and links. .DESCRIPTION With -ReceiptId, returns the receipt for one or more secrets you created, including whether the secret has been viewed, burned or has expired, and the share, receipt and burn URLs. With no identifier, lists the authenticated account's receipts for secrets created in the last 30 days (requires a credential). .PARAMETER ReceiptId The receipt identifier, or the receipt link it appears in. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Get-OtsReceipt ReceiptId SecretId State Created Expires --------- -------- ----- ------- ------- rcp5f8d... sec3kd9f... new 20/10/2026 14:04:05 27/10/2026 14:04:05 rcp2ab1... secxx91c... received 19/10/2026 09:12:33 .EXAMPLE Get-OtsReceipt -ReceiptId rcp5f8d... | Select-Object State, IsReceived, Received Checks whether a specific secret has been picked up. .EXAMPLE Get-OtsReceipt | Where-Object IsReceived | Format-Table ReceiptId, Received, Recipients .OUTPUTS OnetimeSecret.Receipt .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_showreceipt .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_listreceipts #> [CmdletBinding(DefaultParameterSetName = 'Recent')] [OutputType('OnetimeSecret.Receipt')] param( [Parameter(Mandatory, ParameterSetName = 'ById', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string[]] $ReceiptId, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $prefix = Get-OtsPathPrefix -Connection $resolved } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { if ($PSCmdlet.ParameterSetName -eq 'Recent') { try { Assert-OtsAuthenticated -Connection $resolved -Operation 'Listing receipts' $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path 'receipt/recent' foreach ($record in @(Get-OtsProperty $response 'records' @())) { ConvertFrom-OtsReceipt -Record $record -Connection $resolved } } catch { $PSCmdlet.ThrowTerminatingError($_) } return } foreach ($item in $ReceiptId) { try { $identifier = ConvertTo-OtsIdentifier -Value $item -Kind Receipt $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path ('{0}receipt/{1}' -f $prefix, [uri]::EscapeDataString($identifier)) ConvertFrom-OtsReceipt -Record (Get-OtsProperty $response 'record') -Details (Get-OtsProperty $response 'details') -Connection $resolved } catch { $PSCmdlet.WriteError($_) } } } } #endregion Get-OtsReceipt.ps1 #region Get-OtsSecret.ps1 function Get-OtsSecret { <# .SYNOPSIS Gets a secret's metadata without revealing its value. .DESCRIPTION Returns state, passphrase requirement and timing for a secret, leaving the value untouched. Use Receive-OtsSecret to reveal (and thereby destroy) it. The server records this access as a preview on the secret's receipt, which its creator can see. For a lighter check that only answers "does it still exist and what state is it in", use Get-OtsSecretStatus. .PARAMETER SecretId The secret identifier, or the full share link it appears in. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Get-OtsSecret -SecretId 'https://eu.onetimesecret.com/secret/sec3kd9f...' .EXAMPLE New-OtsSecret -Secret 'hunter2' | Get-OtsSecret Reads the metadata of a secret just created, via SecretId on the pipeline. .OUTPUTS OnetimeSecret.Secret .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_showsecret #> [CmdletBinding()] [OutputType('OnetimeSecret.Secret')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string[]] $SecretId, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $prefix = Get-OtsPathPrefix -Connection $resolved } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { foreach ($item in $SecretId) { try { $identifier = ConvertTo-OtsIdentifier -Value $item -Kind Secret $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path ('{0}secret/{1}' -f $prefix, [uri]::EscapeDataString($identifier)) ConvertFrom-OtsSecret -Record (Get-OtsProperty $response 'record') -Connection $resolved } catch { $PSCmdlet.WriteError($_) } } } } #endregion Get-OtsSecret.ps1 #region Get-OtsSecretStatus.ps1 function Get-OtsSecretStatus { <# .SYNOPSIS Checks whether secrets still exist and what state they are in. .DESCRIPTION A lightweight, non-consuming check. Unknown identifiers come back with State 'unknown' and Exists $false rather than an error, which makes this the right call for polling ("has the recipient picked it up yet?"). Several identifiers are checked in a single request. .PARAMETER SecretId One or more secret identifiers or share links. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Get-OtsSecretStatus -SecretId sec3kd9f..., sec8xy2a... SecretId State Exists Expires -------- ----- ------ ------- sec3kd9f... new True 20/10/2026 15:04:05 sec8xy2a... unknown False .EXAMPLE Get-OtsReceipt | Get-OtsSecretStatus Checks every recent receipt's secret in one request. .OUTPUTS OnetimeSecret.SecretStatus .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_listsecretstatus #> [CmdletBinding()] [OutputType('OnetimeSecret.SecretStatus')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string[]] $SecretId, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $identifiers = New-Object -TypeName System.Collections.Generic.List[string] } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { foreach ($item in $SecretId) { try { $identifiers.Add((ConvertTo-OtsIdentifier -Value $item -Kind Secret)) } catch { $PSCmdlet.WriteError($_) } } } end { if ($identifiers.Count -eq 0) { return } try { if ($identifiers.Count -eq 1) { $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path ('secret/{0}/status' -f [uri]::EscapeDataString($identifiers[0])) ConvertFrom-OtsSecretStatus -Record (Get-OtsProperty $response 'record') -Details (Get-OtsProperty $response 'details') -RequestedId $identifiers[0] -Connection $resolved return } $response = Invoke-OtsRequest -Connection $resolved -Method POST -Path 'secret/status' -Body @{ identifiers = @($identifiers) } $records = @(Get-OtsProperty $response 'records' @()) # Keep the caller's order and report identifiers the server left out. $byId = @{} foreach ($record in $records) { $key = [string] (Get-OtsProperty $record 'identifier') if ($key) { $byId[$key] = $record } } foreach ($identifier in $identifiers) { $record = $null if ($byId.ContainsKey($identifier)) { $record = $byId[$identifier] } ConvertFrom-OtsSecretStatus -Record $record -RequestedId $identifier -Connection $resolved } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } #endregion Get-OtsSecretStatus.ps1 #region Get-OtsStatus.ps1 function Get-OtsStatus { <# .SYNOPSIS Reports whether the Onetime Secret service is up. .DESCRIPTION Calls the unauthenticated status endpoint and returns the service's own health report plus the server it came from. Useful as a connectivity check before a batch of operations, or in monitoring. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .PARAMETER ServerUri Check a server without connecting first. .EXAMPLE Get-OtsStatus ServerUri Status IsHealthy --------- ------ --------- https://eu.onetimesecret.com/ nominal True .EXAMPLE 'https://us.onetimesecret.com', 'https://eu.onetimesecret.com' | Get-OtsStatus Checks several regions without connecting. .OUTPUTS OnetimeSecret.ServiceStatus .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_systemstatus #> [CmdletBinding(DefaultParameterSetName = 'Connection')] [OutputType('OnetimeSecret.ServiceStatus')] param( [Parameter(ParameterSetName = 'Connection')] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter(Mandatory, ParameterSetName = 'ServerUri', Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Alias('BaseUrl', 'Server')] [string[]] $ServerUri ) process { $targets = @() try { if ($PSCmdlet.ParameterSetName -eq 'ServerUri') { foreach ($uri in $ServerUri) { $targets += New-OtsConnectionObject -ServerUri $uri } } else { $targets += Resolve-OtsConnection -Connection $Connection } } catch { $PSCmdlet.ThrowTerminatingError($_) } foreach ($target in $targets) { try { $response = Invoke-OtsRequest -Connection $target -Method GET -Path 'status' $status = [string] (Get-OtsProperty $response 'status') [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.ServiceStatus' ServerUri = $target.ServerUri Status = $status IsHealthy = (ConvertTo-OtsBoolean (Get-OtsProperty $response 'success')) -and ($status -in @('nominal', 'ok', 'healthy')) Locale = [string] (Get-OtsProperty $response 'locale') RawResponse = $response } } catch { $PSCmdlet.WriteError($_) } } } } #endregion Get-OtsStatus.ps1 #region Get-OtsSupportedLocale.ps1 function Get-OtsSupportedLocale { <# .SYNOPSIS Lists the locales a Onetime Secret server can serve. .DESCRIPTION Calls the unauthenticated supported-locales endpoint and returns the locale codes the server has translations for, its default locale, and the locale it chose for this request. Useful when building a share link or notification for a recipient in a given language, or when checking what a self-hosted instance has enabled. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .PARAMETER ServerUri Query a server without connecting first. .EXAMPLE Get-OtsSupportedLocale ServerUri DefaultLocale Locales --------- ------------- ------- https://eu.onetimesecret.com/ en {ar, bg, ca_ES, cs...} .EXAMPLE (Get-OtsSupportedLocale -ServerUri https://eu.onetimesecret.com).Locales -contains 'de' Checks whether German is available before localising a message. .OUTPUTS OnetimeSecret.SupportedLocales .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_getsupportedlocales #> [CmdletBinding(DefaultParameterSetName = 'Connection')] [OutputType('OnetimeSecret.SupportedLocales')] param( [Parameter(ParameterSetName = 'Connection')] [PSTypeName('OnetimeSecret.Connection')] $Connection, [Parameter(Mandatory, ParameterSetName = 'ServerUri', Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Alias('BaseUrl', 'Server')] [string[]] $ServerUri ) process { $targets = @() try { if ($PSCmdlet.ParameterSetName -eq 'ServerUri') { foreach ($uri in $ServerUri) { $targets += New-OtsConnectionObject -ServerUri $uri } } else { $targets += Resolve-OtsConnection -Connection $Connection } } catch { $PSCmdlet.ThrowTerminatingError($_) } foreach ($target in $targets) { try { $response = Invoke-OtsRequest -Connection $target -Method GET -Path 'supported-locales' [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.SupportedLocales' ServerUri = $target.ServerUri Locales = ConvertTo-OtsStringArray (Get-OtsProperty $response 'locales') DefaultLocale = [string] (Get-OtsProperty $response 'default_locale') Locale = [string] (Get-OtsProperty $response 'locale') RawResponse = $response } } catch { $PSCmdlet.WriteError($_) } } } } #endregion Get-OtsSupportedLocale.ps1 #region Get-OtsVersion.ps1 function Get-OtsVersion { <# .SYNOPSIS Reports the version of the Onetime Secret server you are connected to. .DESCRIPTION Calls the version endpoint and returns the server's build version as a [version] plus the server it came from. Useful for confirming which release a self-hosted instance runs, or for gating features on it. The endpoint requires an authenticated connection. The OpenAPI document lists it without a security requirement, but the server (onetimesecret.com and current self-hosted releases) withholds the build version from anonymous callers because it fingerprints the install for CVE matching. Anonymously, or with bad credentials, the call fails with an AuthenticationError (HTTP 401). Use Get-OtsStatus for an unauthenticated health check. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Get-OtsVersion ServerUri Version Locale --------- ------- ------ https://eu.onetimesecret.com/ 0.26.5 en .EXAMPLE if ((Get-OtsVersion).Version -lt [version] '0.26.0') { throw 'Server too old.' } Checks the server release before relying on a newer feature. .OUTPUTS OnetimeSecret.ServiceVersion .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_systemversion #> [CmdletBinding()] [OutputType('OnetimeSecret.ServiceVersion')] param( [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) process { try { $resolved = Resolve-OtsConnection -Connection $Connection $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path 'version' } catch { $PSCmdlet.ThrowTerminatingError($_) } # The server reports [major, minor, patch]; a self-hosted build may # append a prerelease label. Keep the raw list, and offer a [version] # where the numeric components allow one. $components = @(Get-OtsProperty $response 'version' @() | ForEach-Object { [string] $_ }) $versionText = $components -join '.' $version = $null $numeric = @($components | Where-Object { $_ -match '^\d+$' }) if ($numeric.Count -ge 2) { try { $version = [version] ($numeric -join '.') } catch { $version = $null } } [PSCustomObject] @{ PSTypeName = 'OnetimeSecret.ServiceVersion' ServerUri = $resolved.ServerUri Version = $version VersionString = $versionText VersionComponents = $components Locale = [string] (Get-OtsProperty $response 'locale') RawResponse = $response } } } #endregion Get-OtsVersion.ps1 #region New-OtsSecret.ps1 function New-OtsSecret { <# .SYNOPSIS Creates a one-time secret and returns its receipt, including the share link. .DESCRIPTION Conceals a value you supply (-Secret or -SecureSecret), or asks the server to generate a random one (-Generate). The returned receipt object has the ShareUrl to hand to the recipient, the ReceiptId you keep to check on or burn the secret, and the generated Value when -Generate was used. Anonymous connections use the guest endpoints and are subject to the guest limits (shorter maximum TTL, no custom share domain). .PARAMETER Secret The secret content as plain text. Accepts pipeline input, so `Get-Content token.txt -Raw | New-OtsSecret` works. .PARAMETER SecureSecret The secret content as a SecureString, for callers that already hold it that way. .PARAMETER Generate Have the server generate a random secret instead of supplying one. The generated value is returned in the receipt's Value property. The server discloses it only once, so New-OtsSecret reads it back immediately; it will not appear on later Get-OtsReceipt calls. .PARAMETER Length Length of the generated secret (4-128). The server default applies when omitted. .PARAMETER CharacterSet Character classes to draw the generated secret from: Uppercase, Lowercase, Numbers, Symbols. The server default applies when omitted. .PARAMETER ExcludeAmbiguous Leave out look-alike characters (such as 0/O and 1/l) from a generated secret. .PARAMETER Passphrase Optional passphrase the recipient must supply to reveal the secret. .PARAMETER Ttl How long the secret stays retrievable, as a TimeSpan (for example (New-TimeSpan -Hours 1) or '01:00:00'). Integers are not accepted because PowerShell would read them as ticks. The server default and plan limits apply when omitted. .PARAMETER Recipient Email address(es) to notify with the share link. .PARAMETER ShareDomain Custom domain to build the share link on (authenticated accounts with a verified domain only). .PARAMETER DisplayType How the recipient's page renders the secret: 'text' (default) or 'markdown'. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE New-OtsSecret -Secret 'hunter2' -Ttl (New-TimeSpan -Hours 1) ReceiptId SecretId State Expires ShareUrl --------- -------- ----- ------- -------- rcp5f8... sec3kd... new 20/10/2026 15:04:05 https://eu.onetimesecret.com/secret/sec3kd... .EXAMPLE $pass = Read-Host -AsSecureString 'Passphrase' $link = (New-OtsSecret -Secret $apiKey -Passphrase $pass -Recipient ops@example.com).ShareUrl Protects the secret with a passphrase and emails the link. .EXAMPLE New-OtsSecret -Generate -Length 24 -CharacterSet Uppercase,Lowercase,Numbers | Select-Object Value, ShareUrl Generates a 24-character alphanumeric secret and shows both the value and the link to share. .EXAMPLE Get-Content ./kubeconfig -Raw | New-OtsSecret -Ttl '2:00:00' -DisplayType markdown Conceals a file's contents from the pipeline. .OUTPUTS OnetimeSecret.Receipt .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_concealsecret .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_generatesecret #> [CmdletBinding(DefaultParameterSetName = 'Conceal', SupportsShouldProcess, ConfirmImpact = 'Low')] [OutputType('OnetimeSecret.Receipt')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Secret', Justification = 'The secret content is the payload being shared, not a credential; SecureString input is offered alongside.')] param( [Parameter(Mandatory, ParameterSetName = 'Conceal', Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Alias('Value', 'Content')] [string] $Secret, [Parameter(Mandatory, ParameterSetName = 'ConcealSecure')] [securestring] $SecureSecret, [Parameter(Mandatory, ParameterSetName = 'Generate')] [switch] $Generate, [Parameter(ParameterSetName = 'Generate')] [ValidateRange(4, 128)] [int] $Length, [Parameter(ParameterSetName = 'Generate')] [ValidateSet('Uppercase', 'Lowercase', 'Numbers', 'Symbols')] [string[]] $CharacterSet, [Parameter(ParameterSetName = 'Generate')] [switch] $ExcludeAmbiguous, [Parameter()] [securestring] $Passphrase, [Parameter()] [Alias('TimeToLive', 'ExpiresIn')] [timespan] $Ttl, [Parameter()] [ValidateNotNullOrEmpty()] [string[]] $Recipient, [Parameter()] [ValidateNotNullOrEmpty()] [string] $ShareDomain, [Parameter()] [ValidateSet('text', 'markdown')] [string] $DisplayType, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $prefix = Get-OtsPathPrefix -Connection $resolved if ($PSBoundParameters.ContainsKey('Ttl') -and $Ttl.TotalSeconds -lt 1) { throw 'Ttl must be at least one second. Pass a TimeSpan such as (New-TimeSpan -Hours 1) or ''01:00:00''; a bare integer is interpreted as ticks.' } } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { $isGenerate = $PSCmdlet.ParameterSetName -eq 'Generate' $payload = [ordered] @{ kind = if ($isGenerate) { 'generate' } else { 'conceal' } } if ($isGenerate) { if ($PSBoundParameters.ContainsKey('Length')) { $payload['length'] = $Length } $characterSets = [ordered] @{} if ($PSBoundParameters.ContainsKey('CharacterSet')) { $characterSets['uppercase'] = [bool] ($CharacterSet -contains 'Uppercase') $characterSets['lowercase'] = [bool] ($CharacterSet -contains 'Lowercase') $characterSets['numbers'] = [bool] ($CharacterSet -contains 'Numbers') $characterSets['symbols'] = [bool] ($CharacterSet -contains 'Symbols') } if ($ExcludeAmbiguous) { $characterSets['exclude_ambiguous'] = $true } if ($characterSets.Count -gt 0) { $payload['character_sets'] = $characterSets } } else { $plain = if ($PSCmdlet.ParameterSetName -eq 'ConcealSecure') { ConvertFrom-OtsSecureString -SecureString $SecureSecret } else { $Secret } if ([string]::IsNullOrEmpty($plain)) { throw 'The secret content is empty.' } $payload['secret'] = $plain } if ($Passphrase) { $payload['passphrase'] = ConvertFrom-OtsSecureString -SecureString $Passphrase } if ($PSBoundParameters.ContainsKey('Ttl')) { $payload['ttl'] = [int] [math]::Floor($Ttl.TotalSeconds) } if ($Recipient) { $payload['recipient'] = @($Recipient) } if ($ShareDomain) { $payload['share_domain'] = $ShareDomain } if ($DisplayType) { $payload['display_type'] = $DisplayType } $description = if ($isGenerate) { 'Generate secret' } else { 'Conceal secret' } if (-not $PSCmdlet.ShouldProcess($resolved.ServerUri.ToString(), $description)) { return } $path = if ($isGenerate) { '{0}secret/generate' -f $prefix } else { '{0}secret/conceal' -f $prefix } $response = Invoke-OtsRequest -Connection $resolved -Method POST -Path $path -Body @{ secret = $payload } $record = Get-OtsProperty $response 'record' $receipt = Get-OtsProperty $record 'receipt' $secretRecord = Get-OtsProperty $record 'secret' $details = Get-OtsProperty $response 'details' $domain = [string] (Get-OtsProperty $record 'share_domain') if ($isGenerate) { # The server hands out a generated value exactly once: on the # first read of the receipt after creation, not in the create # response itself. Fetch it now so the caller gets it in Value; # a later Get-OtsReceipt would come back empty. Only the details # are taken: the shown record's links use the server's # configured host, so keeping the created record builds them # from the connection exactly as for a concealed secret. $receiptId = [string] (Get-OtsProperty $receipt 'identifier') try { $shown = Invoke-OtsRequest -Connection $resolved -Method GET -Path ('{0}receipt/{1}' -f $prefix, [uri]::EscapeDataString($receiptId)) $details = Get-OtsProperty $shown 'details' $details } catch { # Name the failure by error id and status only: the body # of a failed or garbled receipt read can hold the value. $reason = ($_.FullyQualifiedErrorId -split ',')[0] $status = [int] $_.Exception.Data['StatusCode'] if ($status -gt 0) { $reason = '{0}, HTTP {1}' -f $reason, $status } Write-Warning ('The secret was generated but its value could not be read back ({0}). Open {1} to see it.' -f $reason, ('{0}/receipt/{1}' -f $resolved.ServerUri.AbsoluteUri.TrimEnd('/'), $receiptId)) } } ConvertFrom-OtsReceipt -Record $receipt -SecretRecord $secretRecord -Details $details -Connection $resolved -ShareDomain $domain } catch { $PSCmdlet.ThrowTerminatingError($_) } } } #endregion New-OtsSecret.ps1 #region Receive-OtsSecret.ps1 function Receive-OtsSecret { <# .SYNOPSIS Reveals a secret's value. This consumes the secret; it cannot be read again. .DESCRIPTION Retrieves the secret content and destroys it on the server in the same step. Supply -Passphrase when the creator set one. Supports -WhatIf and -Confirm because the operation is irreversible. By default Value is a plain string so it can be used directly in scripts; pass -AsSecureString to receive it as a SecureString instead. .PARAMETER SecretId The secret identifier, or the full share link it appears in. .PARAMETER Passphrase The passphrase the creator protected the secret with, if any. .PARAMETER AsSecureString Return Value as a SecureString. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE (Receive-OtsSecret -SecretId 'https://eu.onetimesecret.com/secret/sec3kd9f...').Value Prints the secret content. .EXAMPLE $pass = Read-Host -AsSecureString 'Passphrase' Receive-OtsSecret sec3kd9f... -Passphrase $pass -AsSecureString Reveals a passphrase-protected secret and keeps the value as a SecureString. .EXAMPLE Receive-OtsSecret sec3kd9f... -WhatIf Shows what would happen without consuming the secret. .OUTPUTS OnetimeSecret.Secret .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_secret_revealsecret #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType('OnetimeSecret.Secret')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string] $SecretId, [Parameter()] [securestring] $Passphrase, [Parameter()] [switch] $AsSecureString, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $prefix = Get-OtsPathPrefix -Connection $resolved } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { $identifier = ConvertTo-OtsIdentifier -Value $SecretId -Kind Secret if (-not $PSCmdlet.ShouldProcess($identifier, 'Reveal and destroy secret')) { return } $body = @{ continue = $true } if ($Passphrase) { $body['passphrase'] = ConvertFrom-OtsSecureString -SecureString $Passphrase } $response = Invoke-OtsRequest -Connection $resolved -Method POST -Path ('{0}secret/{1}/reveal' -f $prefix, [uri]::EscapeDataString($identifier)) -Body $body ConvertFrom-OtsSecret -Record (Get-OtsProperty $response 'record') -Connection $resolved -AsSecureString:$AsSecureString } catch { $PSCmdlet.ThrowTerminatingError($_) } } } #endregion Receive-OtsSecret.ps1 #region Remove-OtsSecret.ps1 function Remove-OtsSecret { <# .SYNOPSIS Burns a secret before anyone reads it. .DESCRIPTION Permanently destroys an unread secret using its receipt identifier (the creator's handle; the secret identifier in the share link does not work here). Returns the updated receipt. Supports -WhatIf and -Confirm. .PARAMETER ReceiptId The receipt identifier, or the receipt link it appears in. Receipt objects from New-OtsSecret and Get-OtsReceipt bind this from the pipeline. .PARAMETER Passphrase The passphrase the secret was created with, if any. .PARAMETER PassThru Return the updated receipt. By default nothing is returned, in keeping with other Remove-* cmdlets. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Remove-OtsSecret -ReceiptId rcp5f8d... .EXAMPLE Get-OtsReceipt | Where-Object State -eq 'new' | Remove-OtsSecret -Confirm:$false Burns every unread secret the account created in the last 30 days. .EXAMPLE $receipt = New-OtsSecret -Secret 'oops' $receipt | Remove-OtsSecret -PassThru Creates and immediately burns a secret, returning the burned receipt. .OUTPUTS OnetimeSecret.Receipt (only with -PassThru) .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_receipt_burnsecret #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType('OnetimeSecret.Receipt')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string[]] $ReceiptId, [Parameter()] [securestring] $Passphrase, [Parameter()] [switch] $PassThru, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection $prefix = Get-OtsPathPrefix -Connection $resolved } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { foreach ($item in $ReceiptId) { try { $identifier = ConvertTo-OtsIdentifier -Value $item -Kind Receipt if (-not $PSCmdlet.ShouldProcess($identifier, 'Burn secret')) { continue } $body = @{ continue = $true } if ($Passphrase) { $body['passphrase'] = ConvertFrom-OtsSecureString -SecureString $Passphrase } $null = Invoke-OtsRequest -Connection $resolved -Method POST -Path ('{0}receipt/{1}/burn' -f $prefix, [uri]::EscapeDataString($identifier)) -Body $body if ($PassThru) { # The burn response echoes the receipt as it was before the # burn, so read it again to report the real state. $response = Invoke-OtsRequest -Connection $resolved -Method GET -Path ('{0}receipt/{1}' -f $prefix, [uri]::EscapeDataString($identifier)) ConvertFrom-OtsReceipt -Record (Get-OtsProperty $response 'record') -Details (Get-OtsProperty $response 'details') -Connection $resolved } } catch { $PSCmdlet.WriteError($_) } } } } #endregion Remove-OtsSecret.ps1 #region Set-OtsReceipt.ps1 function Set-OtsReceipt { <# .SYNOPSIS Updates a receipt's memo. .DESCRIPTION Sets the private memo on a receipt (up to 500 characters) so you can note what a secret was for. Only the receipt's owner can do this, so an authenticated connection is required. Returns the updated receipt. .PARAMETER ReceiptId The receipt identifier, or the receipt link it appears in. .PARAMETER Memo The memo text. Pass an empty string to clear it. .PARAMETER PassThru Return the updated receipt. .PARAMETER Connection Connection to use instead of the default from Connect-OtsAccount. .EXAMPLE Set-OtsReceipt -ReceiptId rcp5f8d... -Memo 'DB password for the migration on Friday' .EXAMPLE New-OtsSecret -Generate | Set-OtsReceipt -Memo 'Temporary SFTP credential' -PassThru .OUTPUTS OnetimeSecret.Receipt (only with -PassThru) .LINK https://api.onetimesecret.com/doc/api-v2/operation/operation-v2_updatereceipt #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] [OutputType('OnetimeSecret.Receipt')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Id', 'Key')] [string[]] $ReceiptId, [Parameter(Mandatory, Position = 1)] [AllowEmptyString()] [ValidateLength(0, 500)] [string] $Memo, [Parameter()] [switch] $PassThru, [Parameter()] [PSTypeName('OnetimeSecret.Connection')] $Connection ) begin { try { $resolved = Resolve-OtsConnection -Connection $Connection Assert-OtsAuthenticated -Connection $resolved -Operation 'Updating a receipt' } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { foreach ($item in $ReceiptId) { try { $identifier = ConvertTo-OtsIdentifier -Value $item -Kind Receipt if (-not $PSCmdlet.ShouldProcess($identifier, 'Set memo')) { continue } $response = Invoke-OtsRequest -Connection $resolved -Method PATCH -Path ('receipt/{0}' -f [uri]::EscapeDataString($identifier)) -Body @{ memo = $Memo } if ($PassThru) { ConvertFrom-OtsReceipt -Record (Get-OtsProperty $response 'record') -Details (Get-OtsProperty $response 'details') -Connection $resolved } } catch { $PSCmdlet.WriteError($_) } } } } #endregion Set-OtsReceipt.ps1 Export-ModuleMember -Function @( 'Connect-OtsAccount' 'Disconnect-OtsAccount' 'Get-OtsConnection' 'Get-OtsReceipt' 'Get-OtsSecret' 'Get-OtsSecretStatus' 'Get-OtsStatus' 'Get-OtsSupportedLocale' 'Get-OtsVersion' 'New-OtsSecret' 'Receive-OtsSecret' 'Remove-OtsSecret' 'Set-OtsReceipt' ) |