From 2d00ee7e80e00def6159acfc486c006d98ce704e Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:23:37 -0800
Subject: [PATCH 01/12] Squashed changes from PR #1507
---
src/Private/Logging.ps1 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1
index 394311e4a..3b35edb35 100644
--- a/src/Private/Logging.ps1
+++ b/src/Private/Logging.ps1
@@ -63,10 +63,10 @@ function Get-PodeLoggingFileMethod {
$item.ToString() | Out-File -FilePath $path -Encoding utf8 -Append -Force
# if set, remove log files beyond days set (ensure this is only run once a day)
- if (($options.MaxDays -gt 0) -and ($options.NextClearDown -lt [DateTime]::Now.Date)) {
+ if (($options.MaxDays -gt 0) -and ($options.NextClearDown -le [DateTime]::Now.Date)) {
$date = [DateTime]::Now.Date.AddDays(-$options.MaxDays)
- $null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force |
+ $null = Get-ChildItem -Path $options.Path -Filter "$($options.Name)_*.log" -Force |
Where-Object { $_.CreationTime -lt $date } |
Remove-Item -Force
From cc0560a77f2cefee653a0f60bac2b1d63701d306 Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:23:43 -0800
Subject: [PATCH 02/12] Squashed changes from PR #1504
---
examples/OpenApi-TuttiFrutti.ps1 | 16 +++---
pode.build.ps1 | 58 +++++++++++++++++--
src/Pode.Internal.psm1 | 5 +-
src/Pode.psd1 | 3 +
src/Pode.psm1 | 20 +++++--
src/Private/Helpers.ps1 | 72 ++++++++++++++----------
src/Public/OAComponents.ps1 | 14 +----
src/Public/OAProperties.ps1 | 5 --
src/Public/Utilities.ps1 | 47 +++++++++-------
tests/unit/OpenApi.Tests.ps1 | 96 ++++++++++++++++----------------
10 files changed, 205 insertions(+), 131 deletions(-)
diff --git a/examples/OpenApi-TuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1
index e80ff8023..d5c739e60 100644
--- a/examples/OpenApi-TuttiFrutti.ps1
+++ b/examples/OpenApi-TuttiFrutti.ps1
@@ -162,7 +162,7 @@ Some useful links:
New-PodeOAStringProperty -Name 'shipDate' -Format Date-Time |
New-PodeOAStringProperty -Name 'status' -Description 'Order Status' -Example 'approved' -Enum @('placed', 'approved', 'delivered') |
New-PodeOABoolProperty -Name 'complete' |
- New-PodeOASchemaProperty -Name 'Address' -Reference 'Address' |
+ New-PodeOAComponentSchemaProperty -Name 'Address' -Reference 'Address' |
New-PodeOAObjectProperty -Name 'Order' -XmlName 'order' -AdditionalProperties (New-PodeOAStringProperty ) |
Add-PodeOAComponentSchema -Name 'Order'
@@ -196,10 +196,10 @@ Some useful links:
New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' -Properties (
New-PodeOAIntProperty -Name 'id'-Format Int64 -Example @(10, 2, 4) -ReadOnly |
New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required |
- New-PodeOASchemaProperty -Name 'category' -Reference 'Category' |
+ New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' |
New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required |
New-PodeOAStringProperty -Name 'photoUrls' -Array |
- New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' |
+ New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag' |
New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold')
))
@@ -226,7 +226,7 @@ Some useful links:
New-PodeOAStringProperty -Name 'name' |
New-PodeOAStringProperty -Name 'type' |
- New-PodeOASchemaProperty -Name 'children' -Array -Reference 'StructPart' |
+ New-PodeOAComponentSchemaProperty -Name 'children' -Array -Reference 'StructPart' |
New-PodeOAObjectProperty |
Add-PodeOAComponentSchema -Name 'StructPart'
@@ -252,10 +252,10 @@ Some useful links:
New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' } -Properties @(
(New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly),
(New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required),
- (New-PodeOASchemaProperty -Name 'category' -Component 'Category'),
+ (New-PodeOAComponentSchemaProperty -Name 'category' -Component 'Category'),
(New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required),
(New-PodeOAStringProperty -Name 'photoUrls' -Array),
- (New-PodeOASchemaProperty -Name 'tags' -Component 'Tag')
+ (New-PodeOAComponentSchemaProperty -Name 'tags' -Component 'Tag')
(New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold'))
)) #>
@@ -309,7 +309,7 @@ Some useful links:
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
( New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
- Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -Reference 'Pet' -array }) -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOAComponentSchemaProperty -Reference 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{ 'text/html' = 'ApiResponse' }) -PassThru
@@ -746,7 +746,7 @@ Some useful links:
New-PodeOAStringProperty -name 'id' -format 'uuid' |
New-PodeOAObjectProperty -name 'address' -NoProperties |
New-PodeOAStringProperty -name 'children' -array |
- New-PodeOASchemaProperty -Name 'addresses' -Reference 'Address' -Array |
+ New-PodeOAComponentSchemaProperty -Name 'addresses' -Reference 'Address' -Array |
New-PodeOAObjectProperty
}) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -PassThru |
Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru |
diff --git a/pode.build.ps1 b/pode.build.ps1
index ae8862739..658146921 100644
--- a/pode.build.ps1
+++ b/pode.build.ps1
@@ -9,6 +9,10 @@
.PARAMETER Version
Specifies the project version for stamping, packaging, and documentation. Defaults to '0.0.0'.
+.PARAMETER Prerelease
+ Specifies the prerelease label to append to the module version, following semantic versioning conventions.
+ Examples include 'alpha.1', 'alpha.2', 'beta.1', etc. This label indicates the stability and iteration of the prerelease version.
+
.PARAMETER PesterVerbosity
Sets the verbosity level for Pester tests. Options: None, Normal, Detailed, Diagnostic.
@@ -90,6 +94,9 @@ param(
[string]
$Version = '0.0.0',
+ [string]
+ $Prerelease,
+
[string]
[ValidateSet('None', 'Normal' , 'Detailed', 'Diagnostic')]
$PesterVerbosity = 'Normal',
@@ -487,7 +494,15 @@ function Invoke-PodeBuildDotnetBuild {
# Optionally set assembly version
if ($Version) {
Write-Output "Assembly Version: $Version"
- $AssemblyVersion = "-p:Version=$Version"
+
+ if ($Prerelease) {
+ $AssemblyVersion = "-p:VersionPrefix=$Version"
+ $AssemblyPrerelease = "-p:VersionSuffix=$Prerelease"
+ }
+ else {
+ $AssemblyVersion = "-p:Version=$Version"
+ $AssemblyPrerelease = ''
+ }
}
else {
$AssemblyVersion = ''
@@ -497,7 +512,7 @@ function Invoke-PodeBuildDotnetBuild {
dotnet restore
# Use dotnet publish for .NET Core and .NET 5+
- dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion --output ../Libs/$target
+ dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion $AssemblyPrerelease --output ../Libs/$target
if (!$?) {
throw "Build failed for target framework '$target'."
@@ -889,6 +904,38 @@ if (($null -eq $PSCmdlet.MyInvocation) -or ($PSCmdlet.MyInvocation.BoundParamete
return
}
+# Import Version File if needed
+if ($Version -eq '0.0.0' -and (Test-Path './Version.json' -PathType Leaf)) {
+ $importedVersion = Get-Content -Path './Version.json' | ConvertFrom-Json
+ if ($importedVersion.Version) {
+ $Version = $importedVersion.Version
+ }
+ if ($importedVersion.Prerelease) {
+ $Prerelease = $importedVersion.Prerelease
+ }
+}
+
+Write-Host '---------------------------------------------------' -ForegroundColor DarkCyan
+
+# Display the Pode build version
+if ($Prerelease) {
+ Write-Host "Pode Build: v$Version-$Prerelease (Pre-release)" -ForegroundColor DarkCyan
+}
+else {
+ if ($Version -eq '0.0.0') {
+ Write-Host 'Pode Build: [Development Version]' -ForegroundColor DarkCyan
+ }
+ else {
+ Write-Host "Pode Build: v$Version" -ForegroundColor DarkCyan
+ }
+}
+
+# Display the current UTC time in a readable format
+$utcTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss 'UTC'")
+Write-Host "Start Time: $utcTime" -ForegroundColor DarkCyan
+
+Write-Host '---------------------------------------------------' -ForegroundColor DarkCyan
+
Add-BuildTask Default {
Write-Host 'Tasks in the Build Script:' -ForegroundColor DarkMagenta
@@ -940,7 +987,10 @@ Add-BuildTask Default {
# Synopsis: Stamps the version onto the Module
Add-BuildTask StampVersion {
$pwshVersions = Get-PodeBuildPwshEOL
- (Get-Content ./pkg/Pode.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version -replace '\$versionsUntested\$', $pwshVersions.eol -replace '\$versionsSupported\$', $pwshVersions.supported -replace '\$buildyear\$', ((get-date).Year) } | Set-Content ./pkg/Pode.psd1
+ if ($Prerelease) {
+ $prereleaseValue = "Prerelease = '$Prerelease'"
+ }
+ (Get-Content ./pkg/Pode.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version -replace '\$versionsUntested\$', $pwshVersions.eol -replace '\$versionsSupported\$', $pwshVersions.supported -replace '\$buildyear\$', ((get-date).Year) -replace '#\$Prerelease-Here\$', $prereleaseValue } | Set-Content ./pkg/Pode.psd1
(Get-Content ./pkg/Pode.Internal.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./pkg/Pode.Internal.psd1
(Get-Content ./packers/choco/pode_template.nuspec) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/pode.nuspec
(Get-Content ./packers/choco/tools/ChocolateyInstall_template.ps1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/tools/ChocolateyInstall.ps1
@@ -1712,4 +1762,4 @@ task ReleaseNotes {
$categories[$category] | Sort-Object | ForEach-Object { Write-Host $_ }
Write-Host ''
}
-}
+}
\ No newline at end of file
diff --git a/src/Pode.Internal.psm1 b/src/Pode.Internal.psm1
index dcb516be0..96176933a 100644
--- a/src/Pode.Internal.psm1
+++ b/src/Pode.Internal.psm1
@@ -14,4 +14,7 @@ Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::Ge
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
# export the module's public functions
-Export-ModuleMember -Function ($funcs.Name)
\ No newline at end of file
+Export-ModuleMember -Function ($funcs.Name)
+
+# Ensure backward compatibility by creating aliases for legacy Pode OpenAPI function names.
+New-PodeFunctionAlias
\ No newline at end of file
diff --git a/src/Pode.psd1 b/src/Pode.psd1
index b520f389c..fa158a2ec 100644
--- a/src/Pode.psd1
+++ b/src/Pode.psd1
@@ -541,6 +541,9 @@
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
+
+ #$Prerelease-Here$
+
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @(
'powershell', 'web', 'server', 'http', 'https', 'listener', 'rest', 'api', 'tcp',
diff --git a/src/Pode.psm1 b/src/Pode.psm1
index 34a812c3a..8e117f136 100644
--- a/src/Pode.psm1
+++ b/src/Pode.psm1
@@ -75,18 +75,26 @@ try {
$moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1'
# Import the module manifest to access its properties
- $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath -ErrorAction Stop
+ $PodeManifest = Import-PowerShellDataFile -Path $moduleManifestPath -ErrorAction Stop
$podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' }
if ($podeDll) {
- if ( $moduleManifest.ModuleVersion -ne '$version$') {
- $moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0'))
+ if ( $PodeManifest.ModuleVersion -ne '$version$') {
+ $moduleVersion = ([version]::new($PodeManifest.ModuleVersion + '.0'))
if ($podeDll.GetName().Version -ne $moduleVersion) {
# An existing incompatible Pode.DLL version {0} is loaded. Version {1} is required. Open a new Powershell/pwsh session and retry.
throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $podeDll.GetName().Version, $moduleVersion)
}
+ $assemblyInformationalVersion = $podeDll.CustomAttributes.Where({ $_.AttributeType -eq [System.Reflection.AssemblyInformationalVersionAttribute] })
+ if ($null -ne $PodeManifest.PrivateData.PSData.Prerelease) {
+ if (! $assemblyInformationalVersion.ConstructorArguments.Value.Contains($PodeManifest.PrivateData.PSData.Prerelease)) {
+ throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $assemblyInformationalVersion.ConstructorArguments.Value, "$moduleVersion-$($PodeManifest.PrivateData.PSData.Prerelease)")
+ }
+ }elseif($assemblyInformationalVersion.ConstructorArguments.Value.Contains('-')){
+ throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $assemblyInformationalVersion.ConstructorArguments.Value, $moduleVersion)
+ }
}
}
else {
@@ -123,6 +131,9 @@ try {
# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }
+ # Ensure backward compatibility by creating aliases for legacy Pode OpenAPI function names.
+ New-PodeFunctionAlias
+
# get functions from memory and compare to existing to find new functions added
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
$aliases = Get-ChildItem Alias: | Where-Object { $sysaliases -notcontains $_ }
@@ -141,6 +152,5 @@ catch {
}
finally {
# Cleanup temporary variables
- Remove-Variable -Name 'tmpPodeLocale', 'localesPath', 'moduleManifest', 'root', 'version', 'libsPath', 'netFolder', 'podeDll', 'sysfuncs', 'sysaliases', 'funcs', 'aliases', 'moduleManifestPath', 'moduleVersion' -ErrorAction SilentlyContinue
+ Remove-Variable -Name 'tmpPodeLocale', 'localesPath', 'root', 'version', 'libsPath', 'netFolder', 'podeDll', 'sysfuncs', 'sysaliases', 'funcs', 'aliases', 'moduleManifestPath', 'moduleVersion' -ErrorAction SilentlyContinue
}
-
diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1
index 804e5e7d2..b2a32e66c 100644
--- a/src/Private/Helpers.ps1
+++ b/src/Private/Helpers.ps1
@@ -3278,30 +3278,6 @@ function Test-PodePlaceholder {
}
-<#
-.SYNOPSIS
-Retrieves the PowerShell module manifest object for the specified module.
-
-.DESCRIPTION
-This function constructs the path to a PowerShell module manifest file (.psd1) located in the parent directory of the script root. It then imports the module manifest file to access its properties and returns the manifest object. This can be useful for scripts that need to dynamically discover and utilize module metadata, such as version, dependencies, and exported functions.
-
-.PARAMETERS
-This function does not accept any parameters.
-
-.EXAMPLE
-$manifest = Get-PodeModuleManifest
-This example calls the `Get-PodeModuleManifest` function to retrieve the module manifest object and stores it in the variable `$manifest`.
-
-#>
-function Get-PodeModuleManifest {
- # Construct the path to the module manifest (.psd1 file)
- $moduleManifestPath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Pode.psd1'
-
- # Import the module manifest to access its properties
- $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath
- return $moduleManifest
-}
-
<#
.SYNOPSIS
Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions.
@@ -3335,8 +3311,7 @@ function Test-PodeVersionPwshEOL {
param(
[switch] $ReportUntested
)
- $moduleManifest = Get-PodeModuleManifest
- if ($moduleManifest.ModuleVersion -eq '$version$') {
+ if ($PodeManifest.ModuleVersion -eq '$version$') {
return @{
eol = $false
supported = $true
@@ -3344,7 +3319,7 @@ function Test-PodeVersionPwshEOL {
}
$psVersion = $PSVersionTable.PSVersion
- $eolVersions = $moduleManifest.PrivateData.PwshVersions.Untested -split ','
+ $eolVersions = $PodeManifest.PrivateData.PwshVersions.Untested -split ','
$isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions
if ($isEol) {
@@ -3352,7 +3327,7 @@ function Test-PodeVersionPwshEOL {
Write-PodeHost ($PodeLocale.eolPowerShellWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow
}
- $SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ','
+ $SupportedVersions = $PodeManifest.PrivateData.PwshVersions.Supported -split ','
$isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions
if ((! $isSupported) -and (! $isEol) -and $ReportUntested) {
@@ -3966,3 +3941,44 @@ function ConvertTo-PodeSleep {
function Test-PodeIsISEHost {
return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name))
}
+
+<#
+.SYNOPSIS
+ Creates aliases for Pode OpenAPI functions to support legacy naming conventions.
+.DESCRIPTION
+ This function sets up the following aliases in the current script scope:
+ - New-PodeOASchemaProperty as an alias for New-PodeOAComponentSchemaProperty.
+ - Enable-PodeOpenApiViewer as an alias for Enable-PodeOAViewer.
+ - Enable-PodeOA as an alias for Enable-PodeOpenApi.
+ - Get-PodeOpenApiDefinition as an alias for Get-PodeOADefinition.
+ The function helps maintain backward compatibility and simplifies calling Pode OpenAPI functions.
+
+.PARAMETER None
+ This function does not accept any parameters.
+.OUTPUTS
+ None. The function creates aliases and does not output any objects.
+.EXAMPLE
+ PS C:\> New-PodeFunctionAlias
+ The function creates the necessary aliases for Pode OpenAPI functions in the current session.
+.NOTES
+ This function is part of the Pode project and adheres to the coding standards defined in the Pode GitHub Repository.
+ Internal function subject to change.
+#>
+function New-PodeFunctionAlias {
+ # Alias
+ if (!(Test-Path Alias:New-PodeOASchemaProperty)) {
+ New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty -Scope Script
+ }
+
+ if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) {
+ New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer -Scope Script
+ }
+
+ if (!(Test-Path Alias:Enable-PodeOA)) {
+ New-Alias Enable-PodeOA -Value Enable-PodeOpenApi -Scope Script
+ }
+
+ if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) {
+ New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition -Scope Script
+ }
+}
\ No newline at end of file
diff --git a/src/Public/OAComponents.ps1 b/src/Public/OAComponents.ps1
index a829f5274..7a49d94f6 100644
--- a/src/Public/OAComponents.ps1
+++ b/src/Public/OAComponents.ps1
@@ -950,16 +950,4 @@ function Remove-PodeOAComponent {
$PodeContext.Server.OpenAPI.Definitions[$tag].components[$field ].remove($Name)
}
}
-}
-
-if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) {
- New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer
-}
-
-if (!(Test-Path Alias:Enable-PodeOA)) {
- New-Alias Enable-PodeOA -Value Enable-PodeOpenApi
-}
-
-if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) {
- New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition
-}
+}
\ No newline at end of file
diff --git a/src/Public/OAProperties.ps1 b/src/Public/OAProperties.ps1
index 322bc2f21..ae4adc834 100644
--- a/src/Public/OAProperties.ps1
+++ b/src/Public/OAProperties.ps1
@@ -2072,9 +2072,4 @@ function New-PodeOAComponentSchemaProperty {
return $param
}
}
-}
-
-
-if (!(Test-Path Alias:New-PodeOASchemaProperty)) {
- New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty
}
\ No newline at end of file
diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1
index e193bbbc3..9391e8845 100644
--- a/src/Public/Utilities.ps1
+++ b/src/Public/Utilities.ps1
@@ -1356,38 +1356,47 @@ function New-PodeCron {
<#
.SYNOPSIS
-Gets the version of the Pode module.
+ Retrieves the version of the Pode module.
.DESCRIPTION
-The Get-PodeVersion function checks the version of the Pode module specified in the module manifest. If the module version is not a placeholder value ('$version$'), it returns the actual version prefixed with 'v.'. If the module version is the placeholder value, indicating the development branch, it returns '[develop branch]'.
+ The `Get-PodeVersion` function checks the version of the Pode module as specified in the module manifest.
+ If the module version is **not** the placeholder value (`'$version$'`), it returns the actual version prefixed with `'v'`.
+ If the module version **is** the placeholder value, indicating the development branch, it returns `"[dev]"`.
-.PARAMETER None
-This function does not accept any parameters.
+.PARAMETER Raw
+ If specified, the function returns only the raw module version without the `'v'` prefix.
+ By default, the function formats the version as `'vX.Y.Z'` unless the module is in development mode.
.OUTPUTS
-System.String
-Returns a string indicating the version of the Pode module or '[dev]' if on a development version.
+ System.String
+ Returns a string representing the Pode module version in one of the following formats:
+ - `"vX.Y.Z"` for a release version (e.g., `"v1.2.3"`).
+ - `"[dev]"` for development versions.
.EXAMPLE
-PS> $moduleManifest = @{ ModuleVersion = '1.2.3' }
-PS> Get-PodeVersion
-
-Returns 'v1.2.3'.
+ PS> Get-PodeVersion
+ Returns the Pode module version, e.g., `'v1.2.3'` for release versions or `"[dev]"` if in development.
.EXAMPLE
-PS> $moduleManifest = @{ ModuleVersion = '$version$' }
-PS> Get-PodeVersion
-
-Returns '[dev]'.
+ PS> Get-PodeVersion -Raw
+ Returns the raw version number, e.g., `'1.2.3'`, without the `'v'` prefix.
.NOTES
-This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion.
-
+ - If the module version is a placeholder (`'$version$'`), the function assumes it's running from the development branch.
#>
function Get-PodeVersion {
- $moduleManifest = Get-PodeModuleManifest
- if ($moduleManifest.ModuleVersion -ne '$version$') {
- return "v$($moduleManifest.ModuleVersion)"
+ param (
+ [switch]
+ $Raw
+ )
+
+ if ($PodeManifest.ModuleVersion -ne '$version$') {
+ $prefix = if ($Raw) { '' } else { 'v' }
+ if ($PodeManifest.PrivateData.PSData.Prerelease) {
+ return "$prefix$($PodeManifest.ModuleVersion)-$($PodeManifest.PrivateData.PSData.Prerelease)"
+ }
+ return "$prefix$($PodeManifest.ModuleVersion)"
+
}
else {
return '[dev]'
diff --git a/tests/unit/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1
index b62b29b89..f72474521 100644
--- a/tests/unit/OpenApi.Tests.ps1
+++ b/tests/unit/OpenApi.Tests.ps1
@@ -1797,7 +1797,7 @@ Describe 'OpenApi' {
}
- Context 'New-PodeOASchemaProperty' {
+ Context 'New-PodeOAComponentSchemaProperty' {
BeforeEach {
Add-PodeOAComponentSchema -Name 'Cat' -Schema (
New-PodeOAObjectProperty -Properties @(
@@ -1807,12 +1807,12 @@ Describe 'OpenApi' {
}
# Check if the function exists
- It 'New-PodeOASchemaProperty function exists' {
- Get-Command New-PodeOASchemaProperty | Should -Not -Be $null
+ It 'New-PodeOAComponentSchemaProperty function exists' {
+ Get-Command New-PodeOAComponentSchemaProperty | Should -Not -Be $null
}
It 'Standard' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Reference 'Cat'
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Reference 'Cat'
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 0
$result.type | Should -Be 'schema'
@@ -1821,15 +1821,15 @@ Describe 'OpenApi' {
}
It 'ArrayNoSwitchesUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4 -UniqueItems
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
$result.minItems | Should -BeTrue
@@ -1837,15 +1837,15 @@ Describe 'OpenApi' {
}
It 'ArrayDeprecatedUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 -UniqueItems
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result.deprecated | Should -Be $true
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
@@ -1853,15 +1853,15 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayNullableUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 -UniqueItems
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['nullable'] | Should -Be $true
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
@@ -1869,15 +1869,15 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayWriteOnlyUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['writeOnly'] | Should -Be $true
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
@@ -1885,15 +1885,15 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayReadOnlyUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['readOnly'] | Should -Be $true
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
@@ -1902,75 +1902,75 @@ Describe 'OpenApi' {
}
It 'ArrayNoSwitches' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
$result.maxItems | Should -BeTrue
}
It 'ArrayDeprecated' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result.deprecated | Should -Be $true
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
$result.maxItems | Should -BeTrue
}
It 'ArrayNullable' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['nullable'] | Should -Be $true
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
$result.maxItems | Should -BeTrue
}
It 'ArrayWriteOnly' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['writeOnly'] | Should -Be $true
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
$result.maxItems | Should -BeTrue
}
It 'ArrayReadOnly' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
- -Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4
+ $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' `
+ -Example 'Example for New-PodeOAComponentSchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
$result.type | Should -Be 'schema'
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
- $result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
+ $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty'
+ $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty'
$result['readOnly'] | Should -Be $true
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
@@ -3352,10 +3352,10 @@ Describe 'OpenApi' {
$Pet = New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' -Properties (
(New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly ),
(New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required) ,
- (New-PodeOASchemaProperty -Name 'category' -Reference 'Category' ),
+ (New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' ),
(New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required) ,
(New-PodeOAStringProperty -Name 'photoUrls' -Array) ,
- (New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag') ,
+ (New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag') ,
(New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold'))
)
$Pet.type | Should -be 'object'
@@ -3407,10 +3407,10 @@ Describe 'OpenApi' {
It 'By Pipeline' {
$Pet = New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly |
New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required |
- New-PodeOASchemaProperty -Name 'category' -Reference 'Category' |
+ New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' |
New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required |
New-PodeOAStringProperty -Name 'photoUrls' -Array |
- New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' |
+ New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag' |
New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') |
New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet'
$Pet.type | Should -be 'object'
From 71eee920de2d3929e2707c56a6d3f282fa3543bd Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:23:49 -0800
Subject: [PATCH 03/12] Squashed changes from PR #1509
---
docs/Tutorials/Endpoints/Basics.md | 36 ++++++++++++
src/Misc/favicon.ico | Bin 0 -> 1150 bytes
src/Private/PodeServer.ps1 | 87 ++++++++++++++++-------------
src/Public/Endpoint.ps1 | 49 ++++++++++++----
4 files changed, 121 insertions(+), 51 deletions(-)
create mode 100644 src/Misc/favicon.ico
diff --git a/docs/Tutorials/Endpoints/Basics.md b/docs/Tutorials/Endpoints/Basics.md
index 888a529ba..1060a5bb8 100644
--- a/docs/Tutorials/Endpoints/Basics.md
+++ b/docs/Tutorials/Endpoints/Basics.md
@@ -188,3 +188,39 @@ To set this property, include it in `server.psd1` configuration file as shown be
}
}
```
+
+## Favicons
+
+Pode allows you to customize or disable the favicon for HTTP/HTTPS endpoints. By default, Pode serves a built-in `favicon.ico`, but you can override this behavior using the `-Favicon` and `-NoFavicon` parameters.
+
+- **`-Favicon` (byte[])**: Allows you to specify a custom favicon as a byte array.
+- **`-NoFavicon` (switch)**: Disables the favicon, preventing browsers from requesting it.
+
+### **Favicon Format and Specifications**
+
+Favicons are typically stored in the `.ico` format, which is a container that can hold multiple image sizes and color depths. This ensures compatibility with different browsers and devices. Some modern browsers also support `.png` and `.svg` favicons.
+
+For more details on favicon formats and specifications, refer to the [Favicon specification](https://en.wikipedia.org/wiki/Favicon) and [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988).
+
+### **Favicon Size Recommendations**
+
+Favicons should include multiple resolutions for optimal display across different devices. Recommended sizes include:
+
+- **16x16** → Used in browser tabs, bookmarks, and address bars.
+- **32x32** → Used in browser tabs on higher-resolution displays.
+- **48x48** → Used by some older browsers and web applications.
+- **64x64+** → Generally not used by browsers but can be helpful for scalability in web apps.
+- **256x256** → Mainly for **Windows app icons** (not typically used as a favicon in browsers).
+
+### **Usage Example**
+
+```powershell
+# Load a custom favicon from file
+$iconBytes = [System.IO.File]::ReadAllBytes("C:\path\to\custom.ico")
+Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Favicon $iconBytes
+
+# Disable favicon for an endpoint
+Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -NoFavicon
+```
+
+Using a favicon enhances the user experience by providing a recognizable site icon in browser tabs and bookmarks.
diff --git a/src/Misc/favicon.ico b/src/Misc/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..0b937da25e5f7176dcab9c4220fe1955959f1a6e
GIT binary patch
literal 1150
zcmbVMJ8V*66#jYH!I-{`ZbqG~OMQ$cnz+@)#lgj-lQD62ad0&5J`!DZ@d*?tT0*&S
zxfVkU4NXPSLTwvfX#o|eJU`EWE)NysAmse_{^y+Uyw4TFK>v{uLFZv{%`C(LA;d8v
zVi8B@(?bZ7jqTNm?{q|zNd#;H!5K4=u|pq}mQjfD68{>VG
zP{~%nYJ_SZ;+SK>vy9tG+mPRQ%?2W?vl;X1)
z@F_aYI)$7IzUPGZpCanullOsE10nYq=OBGlN>ThMuOS}3L~#p_r&ifSG3Oq{ryc<9
zHv5yh)$bqJmpmHv0@)pB8};1`O1Zf~d{W}k)2o&fI=gVU>t7z1=iB-Xu_cyukvSxbxZ*;uU&p(~I$Nu8)E^?l-eqqlM?uD$cLh}?;
z&v1;z{4ep-e90cIhAyDKn`Rq@tP2s}arPmx;kiS|Z$4pJvtFd$PBTW%W~4Vi0&iWz
z*h<`{xvR78R^kp)t2ZczD&Mo~52+3Zx%b`q;rZ{(uWC1OoMF#V?4+%Xk;k`fm1aes
zGu1|W;M8}kH~+kUaxdiGS*U&nJ*R~!d|bTD{(j#>(?9o5qdk%&>)qcOxx=0PCRum?
Ky5E8Sef|Vu3g27+
literal 0
HcmV?d00001
diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1
index bb3356d8a..b8e6453c6 100644
--- a/src/Private/PodeServer.ps1
+++ b/src/Private/PodeServer.ps1
@@ -210,59 +210,66 @@ function Start-PodeWebServer {
if ($Request.IsAborted) {
throw $Request.Error
}
-
- # if we have an sse clientId, verify it and then set details in WebEvent
- if ($WebEvent.Request.HasSseClientId) {
- if (!(Test-PodeSseClientIdValid)) {
- throw [Pode.PodeRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)")
- }
-
- if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) {
- throw [Pode.PodeRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)", 404)
- }
-
- $WebEvent.Sse = @{
- Name = $WebEvent.Request.SseClientName
- Group = $WebEvent.Request.SseClientGroup
- ClientId = $WebEvent.Request.SseClientId
- LastEventId = $null
- IsLocal = $false
- }
+
+ # deal with favicon if available
+ if ($WebEvent.Path -eq '/favicon.ico' -and ($null -ne $PodeContext.Server.Endpoints[$context.EndpointName].Favicon)) {
+ # Write the file content as the HTTP response
+ Write-PodeTextResponse -Bytes $PodeContext.Server.Endpoints[$context.EndpointName].Favicon -ContentType 'image/png' -StatusCode 200
}
+ else {
+ # if we have an sse clientId, verify it and then set details in WebEvent
+ if ($WebEvent.Request.HasSseClientId) {
+ if (!(Test-PodeSseClientIdValid)) {
+ throw [Pode.PodeRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)")
+ }
+
+ if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) {
+ throw [Pode.PodeRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)", 404)
+ }
- # invoke global and route middleware
- if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
- # has the request been aborted
- if ($Request.IsAborted) {
- throw $Request.Error
+ $WebEvent.Sse = @{
+ Name = $WebEvent.Request.SseClientName
+ Group = $WebEvent.Request.SseClientGroup
+ ClientId = $WebEvent.Request.SseClientId
+ LastEventId = $null
+ IsLocal = $false
+ }
}
- if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
+ # invoke global and route middleware
+ if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
}
- # invoke the route
- if ($null -ne $WebEvent.StaticContent) {
- $fileBrowser = $WebEvent.Route.FileBrowser
- if ($WebEvent.StaticContent.IsDownload) {
- Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
+ if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
+ # has the request been aborted
+ if ($Request.IsAborted) {
+ throw $Request.Error
}
- elseif ($WebEvent.StaticContent.RedirectToDefault) {
- $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
- Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
+
+ # invoke the route
+ if ($null -ne $WebEvent.StaticContent) {
+ $fileBrowser = $WebEvent.Route.FileBrowser
+ if ($WebEvent.StaticContent.IsDownload) {
+ Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
+ }
+ elseif ($WebEvent.StaticContent.RedirectToDefault) {
+ $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
+ Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
+ }
+ else {
+ $cachable = $WebEvent.StaticContent.IsCachable
+ Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
+ -Cache:$cachable -FileBrowser:$fileBrowser
+ }
}
- else {
- $cachable = $WebEvent.StaticContent.IsCachable
- Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
- -Cache:$cachable -FileBrowser:$fileBrowser
+ elseif ($null -ne $WebEvent.Route.Logic) {
+ $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
+ -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
- elseif ($null -ne $WebEvent.Route.Logic) {
- $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
- -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
- }
}
}
}
diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1
index d6b1c97c1..d2d9fa523 100644
--- a/src/Public/Endpoint.ps1
+++ b/src/Public/Endpoint.ps1
@@ -1,4 +1,3 @@
-
<#
.SYNOPSIS
Bind an endpoint to listen for incoming Requests.
@@ -19,13 +18,13 @@ An optional hostname for the endpoint, specifying a hostname restricts access to
The protocol of the supplied endpoint.
.PARAMETER Certificate
-The path to a certificate that can be use to enable HTTPS
+The path to a certificate that can be used to enable HTTPS.
.PARAMETER CertificatePassword
-The password for the certificate file referenced in Certificate
+The password for the certificate file referenced in Certificate.
.PARAMETER CertificateKey
-A key file to be paired with a PEM certificate file referenced in Certificate
+A key file to be paired with a PEM certificate file referenced in Certificate.
.PARAMETER CertificateThumbprint
A certificate thumbprint to bind onto HTTPS endpoints (Windows).
@@ -34,13 +33,13 @@ A certificate thumbprint to bind onto HTTPS endpoints (Windows).
A certificate subject name to bind onto HTTPS endpoints (Windows).
.PARAMETER CertificateStoreName
-The name of a certifcate store where a certificate can be found (Default: My) (Windows).
+The name of a certificate store where a certificate can be found (Default: My) (Windows).
.PARAMETER CertificateStoreLocation
-The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows).
+The location of a certificate store where a certificate can be found (Default: CurrentUser) (Windows).
.PARAMETER X509Certificate
-The raw X509 certificate that can be use to enable HTTPS
+The raw X509 certificate that can be used to enable HTTPS.
.PARAMETER TlsMode
The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit).
@@ -58,16 +57,16 @@ A quick description of the Endpoint - normally used in OpenAPI.
An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only.
.PARAMETER SslProtocol
-One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS).
+One or more optional SSL Protocols this endpoint supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS).
.PARAMETER CRLFMessageEnd
If supplied, TCP endpoints will expect incoming data to end with CRLF.
.PARAMETER Force
-Ignore Adminstrator checks for non-localhost endpoints.
+Ignore Administrator checks for non-localhost endpoints.
.PARAMETER SelfSigned
-Create and bind a self-signed certifcate for HTTPS endpoints.
+Create and bind a self-signed certificate for HTTPS endpoints.
.PARAMETER AllowClientCertificate
Allow for client certificates to be sent on requests.
@@ -85,6 +84,12 @@ For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 ad
.PARAMETER Default
If supplied, this endpoint will be the default one used for internally generating URLs.
+.PARAMETER Favicon
+A byte array representing a custom favicon for HTTP/HTTPS endpoints.
+
+.PARAMETER NoFavicon
+If supplied, disables the default favicon for HTTP/HTTPS endpoints.
+
.EXAMPLE
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http
@@ -208,7 +213,13 @@ function Add-PodeEndpoint {
$DualMode,
[switch]
- $Default
+ $Default,
+
+ [byte[]]
+ $Favicon,
+
+ [switch]
+ $NoFavicon
)
# error if serverless
@@ -292,6 +303,20 @@ function Add-PodeEndpoint {
throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage)
}
+ if (! $NoFavicon ) {
+ $Favicon = $null
+ }
+ # Load the default favicon
+ elseif ( ($null -eq $Favicon) -and (@('Http', 'Https') -icontains $Protocol)) {
+ $podeRoot = Get-PodeModuleMiscPath
+ if (Test-PodeIsPSCore) {
+ $Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -AsByteStream)
+ }
+ else {
+ $Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -Encoding byte)
+ }
+ }
+
# new endpoint object
$obj = @{
Name = $Name
@@ -324,8 +349,10 @@ function Add-PodeEndpoint {
Acknowledge = $Acknowledge
CRLFMessageEnd = $CRLFMessageEnd
}
+ Favicon = $Favicon
}
+
# set ssl protocols
if (!(Test-PodeIsEmpty $SslProtocol)) {
$obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol)
From 86885c757ee078cbd057fde5abefc25d620b0a26 Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:24:49 -0800
Subject: [PATCH 04/12] Squashed changes from PR #1386
---
docs/Tutorials/Routes/Overview.md | 91 +-
docs/Tutorials/Routes/Parameters/Body.md | 100 ++
docs/Tutorials/Routes/Parameters/Cookies.md | 107 ++
docs/Tutorials/Routes/Parameters/Headers.md | 102 ++
docs/Tutorials/Routes/Parameters/Paths.md | 108 ++
docs/Tutorials/Routes/Parameters/Queries.md | 107 ++
src/Pode.psd1 | 5 +
src/Public/Cookies.ps1 | 99 +-
src/Public/Headers.ps1 | 76 +-
src/Public/Utilities.ps1 | 1329 ++++++++++++++++++-
tests/unit/Utility.Tests.ps1 | 956 +++++++++++++
11 files changed, 2948 insertions(+), 132 deletions(-)
create mode 100644 docs/Tutorials/Routes/Parameters/Body.md
create mode 100644 docs/Tutorials/Routes/Parameters/Cookies.md
create mode 100644 docs/Tutorials/Routes/Parameters/Headers.md
create mode 100644 docs/Tutorials/Routes/Parameters/Paths.md
create mode 100644 docs/Tutorials/Routes/Parameters/Queries.md
create mode 100644 tests/unit/Utility.Tests.ps1
diff --git a/docs/Tutorials/Routes/Overview.md b/docs/Tutorials/Routes/Overview.md
index a9acd7efe..aba0f2b6d 100644
--- a/docs/Tutorials/Routes/Overview.md
+++ b/docs/Tutorials/Routes/Overview.md
@@ -39,96 +39,21 @@ The scriptblock for the route will have access to the `$WebEvent` variable which
You can add your routes straight into the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) scriptblock, or separate them into different files. These files can then be dot-sourced, or you can use [`Use-PodeRoutes`](../../../Functions/Routes/Use-PodeRoutes) to automatically load all ps1 files within a `/routes` directory at the root of your server.
-## Payloads
+## Retrieving Client Parameters
-The following is an example of using data from a request's payload - ie, the data in the body of POST request. To retrieve values from the payload you can use the `.Data` property on the `$WebEvent` variable to a route's logic.
+When working with REST calls, data can be passed by the client using various methods, including Cookies, Headers, Paths, Queries, and Body. Each of these methods has specific ways to retrieve the data:
-Depending the the Content-Type supplied, Pode has inbuilt body-parsing logic for JSON, XML, CSV, and Form data.
+- **Cookies**: Cookies sent by the client can be accessed using the `$WebEvent.Cookies` property or the `Get-PodeCookie` function for more advanced handling. For more details, refer to the [Cookies Documentation](./Parameters/Cookies.md).
-This example will get the `userId` and "find" user, returning the users data:
+- **Headers**: Headers can be retrieved using the `$WebEvent.Request.Headers` property or the `Get-PodeHeader` function, which provides additional deserialization options. Learn more in the [Headers Documentation](./Parameters/Headers.md).
-```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+- **Paths**: Parameters passed through the URL path can be accessed using the `$WebEvent.Parameters` property or the `Get-PodePathParameter` function. Detailed information can be found in the [Path Parameters Documentation](./Parameters/Paths.md).
- Add-PodeRoute -Method Post -Path '/users' -ScriptBlock {
- # get the user
- $user = Get-DummyUser -UserId $WebEvent.Data.userId
-
- # return the user
- Write-PodeJsonResponse -Value @{
- Username = $user.username
- Age = $user.age
- }
- }
-}
-```
-
-The following request will invoke the above route:
-
-```powershell
-Invoke-WebRequest -Uri 'http://localhost:8080/users' -Method Post -Body '{ "userId": 12345 }' -ContentType 'application/json'
-```
+- **Queries**: Query parameters from the URL can be accessed via `$WebEvent.Query` or retrieved using the `Get-PodeQueryParameter` function for deserialization support. Check the [Query Parameters Documentation](./Parameters/Queries.md).
-!!! important
- The `ContentType` is required as it informs Pode on how to parse the requests payload. For example, if the content type were `application/json`, then Pode will attempt to parse the body of the request as JSON - converting it to a hashtable.
+- **Body**: Data sent in the request body, such as in POST requests, can be retrieved using the `$WebEvent.Data` property or the `Get-PodeBodyData` function for enhanced deserialization capabilities. See the [Body Data Documentation](./Parameters/Body.md) for more information.
-!!! important
- On PowerShell 5 referencing JSON data on `$WebEvent.Data` must be done as `$WebEvent.Data.userId`. This also works in PowerShell 6+, but you can also use `$WebEvent.Data['userId']` on PowerShell 6+.
-
-## Query Strings
-
-The following is an example of using data from a request's query string. To retrieve values from the query string you can use the `.Query` property from the `$WebEvent` variable. This example will return a user based on the `userId` supplied:
-
-```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
-
- Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {
- # get the user
- $user = Get-DummyUser -UserId $WebEvent.Query['userId']
-
- # return the user
- Write-PodeJsonResponse -Value @{
- Username = $user.username
- Age = $user.age
- }
- }
-}
-```
-
-The following request will invoke the above route:
-
-```powershell
-Invoke-WebRequest -Uri 'http://localhost:8080/users?userId=12345' -Method Get
-```
-
-## Parameters
-
-The following is an example of using values supplied on a request's URL using parameters. To retrieve values that match a request's URL parameters you can use the `.Parameters` property from the `$WebEvent` variable. This example will get the `:userId` and "find" user, returning the users data:
-
-```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
-
- Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock {
- # get the user
- $user = Get-DummyUser -UserId $WebEvent.Parameters['userId']
-
- # return the user
- Write-PodeJsonResponse -Value @{
- Username = $user.username
- Age = $user.age
- }
- }
-}
-```
-
-The following request will invoke the above route:
-
-```powershell
-Invoke-WebRequest -Uri 'http://localhost:8080/users/12345' -Method Get
-```
+Each link provides detailed usage and examples to help you retrieve and manipulate the parameters passed by the client effectively.
## Script from File
diff --git a/docs/Tutorials/Routes/Parameters/Body.md b/docs/Tutorials/Routes/Parameters/Body.md
new file mode 100644
index 000000000..b620d3a54
--- /dev/null
+++ b/docs/Tutorials/Routes/Parameters/Body.md
@@ -0,0 +1,100 @@
+# Body Payloads
+
+The following is an example of using data from a request's payload—i.e., the data in the body of a POST request. To retrieve values from the payload, you can use the `.Data` property on the `$WebEvent` variable in a route's logic.
+
+Alternatively, you can use the `Get-PodeBodyData` function to retrieve the body data, with additional support for deserialization.
+
+Depending on the Content-Type supplied, Pode has built-in body-parsing logic for JSON, XML, CSV, and Form data.
+
+This example will get the `userId` and "find" the user, returning the user's data:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Post -Path '/users' -ScriptBlock {
+ # get the user
+ $user = Get-DummyUser -UserId $WebEvent.Data.userId
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+The following request will invoke the above route:
+
+```powershell
+Invoke-WebRequest -Uri 'http://localhost:8080/users' -Method Post -Body '{ "userId": 12345 }' -ContentType 'application/json'
+```
+
+!!! important
+ The `ContentType` is required as it informs Pode on how to parse the request's payload. For example, if the content type is `application/json`, Pode will attempt to parse the body of the request as JSON—converting it to a hashtable.
+
+!!! important
+ On PowerShell 5, referencing JSON data on `$WebEvent.Data` must be done as `$WebEvent.Data.userId`. This also works in PowerShell 6+, but you can also use `$WebEvent.Data['userId']` on PowerShell 6+.
+
+### Using Get-PodeBodyData
+
+Alternatively, you can use the `Get-PodeBodyData` function to retrieve the body data. This function works similarly to the `.Data` property on `$WebEvent` and supports the same content types.
+
+Here is the same example using `Get-PodeBodyData`:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Post -Path '/users' -ScriptBlock {
+ # get the body data
+ $body = Get-PodeBodyData
+
+ # get the user
+ $user = Get-DummyUser -UserId $body.userId
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+### Deserialization with Get-PodeBodyData
+
+Typically the request body is encoded in Json,Xml or Yaml but if it's required the `Get-PodeBodyData` function can also deserialize body data from requests, allowing for more complex data handling scenarios where the only allowed ContentTypes are `application/x-www-form-urlencoded` or `multipart/form-data`. This feature can be especially useful when dealing with serialized data structures that require specific interpretation styles.
+
+To enable deserialization, use the `-Deserialize` switch along with the following options:
+
+- **`-NoExplode`**: Prevents deserialization from exploding arrays in the body data. This is useful when dealing with comma-separated values where array expansion is not desired.
+- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, `'Matrix'`, `'Form'`, `'SpaceDelimited'`, `'PipeDelimited'`, `'DeepObject'`) to interpret the body data correctly. The default style is `'Form'`.
+- **`-KeyName`**: Specifies the key name to use when deserializing, allowing accurate mapping of the body data. The default value for `KeyName` is `'id'`.
+
+### Example with Deserialization
+
+This example demonstrates deserialization of body data using specific styles and options:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Post -Path '/items' -ScriptBlock {
+ # retrieve and deserialize the body data
+ $body = Get-PodeBodyData -Deserialize -Style 'Matrix' -NoExplode
+
+ # get the item based on the deserialized data
+ $item = Get-DummyItem -ItemId $body.id
+
+ # return the item details
+ Write-PodeJsonResponse -Value @{
+ Name = $item.name
+ Quantity = $item.quantity
+ }
+ }
+}
+```
+
+In this example, `Get-PodeBodyData` is used to deserialize the body data with the `'Matrix'` style and prevent array explosion (`-NoExplode`). This approach provides flexible and precise handling of incoming body data, enhancing the capability of your Pode routes to manage complex payloads.
\ No newline at end of file
diff --git a/docs/Tutorials/Routes/Parameters/Cookies.md b/docs/Tutorials/Routes/Parameters/Cookies.md
new file mode 100644
index 000000000..f1222aa2e
--- /dev/null
+++ b/docs/Tutorials/Routes/Parameters/Cookies.md
@@ -0,0 +1,107 @@
+
+# Cookies
+
+The following is an example of using values supplied in a request's cookies. To retrieve values from the cookies, you can use the `Cookies` property from the `$WebEvent` variable.
+
+Alternatively, you can use the `Get-PodeCookie` function to retrieve the cookie data, with additional support for deserialization and secure handling.
+
+This example will get the `SessionId` cookie and use it to authenticate the user, returning a success message:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/authenticate' -ScriptBlock {
+ # get the session ID from the cookie
+ $sessionId = $WebEvent.Cookies['SessionId']
+
+ # authenticate the session
+ $isAuthenticated = Authenticate-Session -SessionId $sessionId
+
+ # return the result
+ Write-PodeJsonResponse -Value @{
+ Authenticated = $isAuthenticated
+ }
+ }
+}
+```
+
+The following request will invoke the above route:
+
+```powershell
+Invoke-WebRequest -Uri 'http://localhost:8080/authenticate' -Method Get -Headers @{ Cookie = 'SessionId=abc123' }
+```
+
+## Using Get-PodeCookie
+
+Alternatively, you can use the `Get-PodeCookie` function to retrieve the cookie data. This function works similarly to the `Cookies` property on `$WebEvent`, but it provides additional options for deserialization and secure cookie handling.
+
+Here is the same example using `Get-PodeCookie`:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/authenticate' -ScriptBlock {
+ # get the session ID from the cookie
+ $sessionId = Get-PodeCookie -Name 'SessionId'
+
+ # authenticate the session
+ $isAuthenticated = Authenticate-Session -SessionId $sessionId
+
+ # return the result
+ Write-PodeJsonResponse -Value @{
+ Authenticated = $isAuthenticated
+ }
+ }
+}
+```
+
+### Deserialization with Get-PodeCookie
+
+The `Get-PodeCookie` function can also deserialize cookie values, allowing for more complex handling of serialized data sent in cookies. This feature is particularly useful when cookies contain encoded or structured content that needs specific parsing.
+
+To enable deserialization, use the `-Deserialize` switch along with the following options:
+
+- **`-NoExplode`**: Prevents deserialization from exploding arrays in the cookie value. This is useful when handling comma-separated values where array expansion is not desired.
+- **`-Deserialize`**: Indicates that the retrieved cookie value should be deserialized, interpreting the content based on the provided deserialization style and options.
+
+
+
+#### Supported Deserialization Styles
+
+| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) |
+|-------|---------|--------------|--------------------------|------------------------|------------------------------------------------------|
+| form* | true* | | Cookie: id=5 | | |
+| form | false | id={id} | Cookie: id=5 | Cookie: id=3,4,5 | Cookie: id=role,admin,firstName,Alex |
+
+\* Default serialization method
+
+### Example with Deserialization
+
+This example demonstrates deserialization of a cookie value:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/deserialize-cookie' -ScriptBlock {
+ # retrieve and deserialize the 'Session' cookie
+ $sessionData = Get-PodeCookie -Name 'Session' -Deserialize -NoExplode
+
+ # process the deserialized cookie data
+ # (example processing logic here)
+
+ # return the processed cookie data
+ Write-PodeJsonResponse -Value @{
+ SessionData = $sessionData
+ }
+ }
+}
+```
+
+In this example, `Get-PodeCookie` is used to deserialize the `Session` cookie, interpreting it according to the provided deserialization options. The `-NoExplode` switch ensures that any arrays within the cookie value are not expanded during deserialization.
+
+For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570).
+
+For further information on general usage and retrieving cookies, please refer to the [Headers Documentation](Cookies.md).
\ No newline at end of file
diff --git a/docs/Tutorials/Routes/Parameters/Headers.md b/docs/Tutorials/Routes/Parameters/Headers.md
new file mode 100644
index 000000000..913b860bf
--- /dev/null
+++ b/docs/Tutorials/Routes/Parameters/Headers.md
@@ -0,0 +1,102 @@
+# Headers
+
+The following is an example of using values supplied in a request's headers. To retrieve values from the headers, you can use the `Headers` property from the `$WebEvent.Request` variable. Alternatively, you can use the `Get-PodeHeader` function to retrieve the header data.
+
+This example will get the Authorization header and validate the token, returning a success message:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/validate' -ScriptBlock {
+ # get the token
+ $token = $WebEvent.Request.Headers['Authorization']
+
+ # validate the token
+ $isValid = Test-PodeJwt -payload $token
+
+ # return the result
+ Write-PodeJsonResponse -Value @{
+ Success = $isValid
+ }
+ }
+}
+```
+
+The following request will invoke the above route:
+
+```powershell
+Invoke-WebRequest -Uri 'http://localhost:8080/validate' -Method Get -Headers @{ Authorization = 'Bearer some_token' }
+```
+
+## Using Get-PodeHeader
+
+Alternatively, you can use the `Get-PodeHeader` function to retrieve the header data. This function works similarly to the `Headers` property on `$WebEvent.Request`.
+
+Here is the same example using `Get-PodeHeader`:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/validate' -ScriptBlock {
+ # get the token
+ $token = Get-PodeHeader -Name 'Authorization'
+
+ # validate the token
+ $isValid = Test-PodeJwt -payload $token
+
+ # return the result
+ Write-PodeJsonResponse -Value @{
+ Success = $isValid
+ }
+ }
+}
+```
+
+### Deserialization with Get-PodeHeader
+
+The `Get-PodeHeader` function can also deserialize header values, enabling more advanced handling of serialized data sent in headers. This feature is useful when dealing with complex data structures or when headers contain encoded or serialized content.
+
+To enable deserialization, use the `-Deserialize` switch along with the following options:
+
+- **`-Explode`**: Specifies whether the deserialization process should explode arrays in the header value. This is useful when handling comma-separated values within the header.
+- **`-Deserialize`**: Indicates that the retrieved header value should be deserialized, interpreting the content based on the deserialization style and options.
+
+#### Supported Deserialization Styles
+
+| Style | Explode | URI Template | Primitive Value (X-MyHeader = 5) | Array (X-MyHeader = [3, 4, 5]) | Object (X-MyHeader = {"role": "admin", "firstName": "Alex"}) |
+|---------|---------|--------------|----------------------------------|--------------------------------|--------------------------------------------------------------|
+| simple* | false* | {id} | X-MyHeader: 5 | X-MyHeader: 3,4,5 | X-MyHeader: role,admin,firstName,Alex |
+| simple | true | {id*} | X-MyHeader: 5 | X-MyHeader: 3,4,5 | X-MyHeader: role=admin,firstName=Alex |
+
+\* Default serialization method
+
+### Example with Deserialization
+
+This example demonstrates deserialization of a header value:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/deserialize' -ScriptBlock {
+ # retrieve and deserialize the 'X-SerializedHeader' header
+ $headerData = Get-PodeHeader -Name 'X-SerializedHeader' -Deserialize -Explode
+
+ # process the deserialized header data
+ # (example processing logic here)
+
+ # return the processed header data
+ Write-PodeJsonResponse -Value @{
+ HeaderData = $headerData
+ }
+ }
+}
+```
+
+In this example, `Get-PodeHeader` is used to deserialize the `X-SerializedHeader` header, interpreting it according to the provided deserialization options. The `-Explode` switch ensures that any arrays within the header value are properly expanded during deserialization.
+
+For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570).
+
+For further information on general usage and retrieving headers, please refer to the [Headers Documentation](Headers.md).
diff --git a/docs/Tutorials/Routes/Parameters/Paths.md b/docs/Tutorials/Routes/Parameters/Paths.md
new file mode 100644
index 000000000..e1a3d3e5a
--- /dev/null
+++ b/docs/Tutorials/Routes/Parameters/Paths.md
@@ -0,0 +1,108 @@
+
+# Paths
+
+The following is an example of using values supplied on a request's URL using parameters. To retrieve values that match a request's URL parameters, you can use the `Parameters` property from the `$WebEvent` variable.
+
+Alternatively, you can use the `Get-PodePathParameter` function to retrieve the parameter data.
+
+This example will get the `:userId` and "find" user, returning the user's data:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock {
+ # get the user
+ $user = Get-DummyUser -UserId $WebEvent.Parameters['userId']
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+The following request will invoke the above route:
+
+```powershell
+Invoke-WebRequest -Uri 'http://localhost:8080/users/12345' -Method Get
+```
+
+### Using Get-PodePathParameter
+
+Alternatively, you can use the `Get-PodePathParameter` function to retrieve the parameter data. This function works similarly to the `Parameters` property on `$WebEvent` but provides additional options for deserialization when needed.
+
+Here is the same example using `Get-PodePathParameter`:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/users/:userId' -ScriptBlock {
+ # get the parameter data
+ $userId = Get-PodePathParameter -Name 'userId'
+
+ # get the user
+ $user = Get-DummyUser -UserId $userId
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+#### Deserialization with Get-PodePathParameter
+
+The `Get-PodePathParameter` function can handle deserialization of parameters passed in the URL path, query string, or body, using specific styles to interpret the data correctly. This is useful when dealing with more complex data structures or encoded parameter values.
+
+To enable deserialization, use the `-Deserialize` switch along with the following options:
+
+- **`-Explode`**: Specifies whether to explode arrays when deserializing, useful when parameters contain comma-separated values.
+- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, or `'Matrix'`) to interpret the parameter value correctly. The default style is `'Simple'`.
+- **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the parameter data accurately. The default value for `KeyName` is `'id'`.
+
+#### Supported Deserialization Styles
+
+| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) |
+|---------|---------|---------------|--------------------------|------------------------|------------------------------------------------------|
+| simple* | false* | /users/{id} | /users/5 | /users/3,4,5 | /users/role,admin,firstName,Alex |
+| simple | true | /users/{id*} | /users/5 | /users/3,4,5 | /users/role=admin,firstName=Alex |
+| label | false | /users/{.id} | /users/.5 | /users/.3,4,5 | /users/.role,admin,firstName,Alex |
+| label | true | /users/{.id*} | /users/.5 | /users/.3.4.5 | /users/.role=admin.firstName=Alex |
+| matrix | false | /users/{;id} | /users/;id=5 | /users/;id=3,4,5 | /users/;id=role,admin,firstName,Alex |
+| matrix | true | /users/{;id*} | /users/;id=5 | /users/;id=3;id=4;id=5 | /users/;role=admin;firstName=Alex |
+
+\* Default serialization method
+
+#### Example with Deserialization
+
+This example demonstrates deserialization of a parameter that is styled and exploded as part of the request:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/items/:itemId' -ScriptBlock {
+ # retrieve and deserialize the 'itemId' parameter
+ $itemId = Get-PodePathParameter -Name 'itemId' -Deserialize -Style 'Label' -Explode
+
+ # get the item based on the deserialized data
+ $item = Get-DummyItem -ItemId $itemId
+
+ # return the item details
+ Write-PodeJsonResponse -Value @{
+ Name = $item.name
+ Quantity = $item.quantity
+ }
+ }
+}
+```
+
+In this example, the `Get-PodePathParameter` function is used to deserialize the `itemId` parameter, interpreting it according to the specified style (`Label`) and handling arrays if present (`-Explode`). The default `KeyName` is `'id'`, but it can be customized as needed. This approach allows for dynamic and precise handling of incoming request data, making your Pode routes more versatile and resilient.
+
+For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570).
\ No newline at end of file
diff --git a/docs/Tutorials/Routes/Parameters/Queries.md b/docs/Tutorials/Routes/Parameters/Queries.md
new file mode 100644
index 000000000..41b839b98
--- /dev/null
+++ b/docs/Tutorials/Routes/Parameters/Queries.md
@@ -0,0 +1,107 @@
+# Queries
+
+The following is an example of using data from a request's query string. To retrieve values from the query parameters, you can use the `Query` property on the `$WebEvent` variable in a route's logic.
+
+Alternatively, you can use the `Get-PodeQueryParameter` function to retrieve the query parameter data, with additional support for deserialization.
+
+This example will return a user based on the `userId` supplied:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {
+ # get the user
+ $user = Get-DummyUser -UserId $WebEvent.Query['userId']
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+The following request will invoke the above route:
+
+```powershell
+Invoke-WebRequest -Uri 'http://localhost:8080/users?userId=12345' -Method Get
+```
+
+### Using Get-PodeQueryParameter
+
+Alternatively, you can use the `Get-PodeQueryParameter` function to retrieve the query data. This function works similarly to the `Query` property on `$WebEvent` but provides additional options for deserialization when needed.
+
+Here is the same example using `Get-PodeQueryParameter`:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {
+ # get the query data
+ $userId = Get-PodeQueryParameter -Name 'userId'
+
+ # get the user
+ $user = Get-DummyUser -UserId $userId
+
+ # return the user
+ Write-PodeJsonResponse -Value @{
+ Username = $user.username
+ Age = $user.age
+ }
+ }
+}
+```
+
+#### Deserialization with Get-PodeQueryParameter
+
+The `Get-PodeQueryParameter` function can also deserialize query parameters passed in the URL, using specific styles to interpret the data correctly. This feature is particularly useful when handling complex data structures or encoded parameter values.
+
+To enable deserialization, use the `-Deserialize` switch along with the following options:
+
+- **`-NoExplode`**: Prevents deserialization from exploding arrays when handling comma-separated values. This is useful when array expansion is not desired.
+- **`-Style`**: Defines the deserialization style (`'Simple'`, `'Label'`, `'Matrix'`, `'Form'`, `'SpaceDelimited'`, `'PipeDelimited'`, `'DeepObject'`) to interpret the query parameter value correctly. The default style is `'Form'`.
+- **`-KeyName`**: Specifies the key name to use when deserializing, allowing you to map the query parameter data accurately. The default value for `KeyName` is `'id'`.
+
+#### Supported Deserialization Styles
+
+
+| Style | Explode | URI Template | Primitive Value (id = 5) | Array (id = [3, 4, 5]) | Object (id = {"role": "admin", "firstName": "Alex"}) |
+|----------------|---------|--------------|--------------------------|------------------------|------------------------------------------------------|
+| form* | true* | /users{?id*} | /users?id=5 | /users?id=3&id=4&id=5 | /users?role=admin&firstName=Alex |
+| form | false | /users{?id} | /users?id=5 | /users?id=3,4,5 | /users?id=role,admin,firstName,Alex |
+| spaceDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a |
+| spaceDelimited | false | n/a | n/a | /users?id=3%204%205 | n/a |
+| pipeDelimited | true | /users{?id*} | n/a | /users?id=3&id=4&id=5 | n/a |
+| pipeDelimited | false | n/a | n/a | /users?id=3\|4\|5 | n/a |
+| deepObject | true | n/a | n/a | n/a | /users?id[role]=admin&id[firstName]=Alex |
+
+
+\* Default serialization method
+
+#### Example with Deserialization
+
+This example demonstrates deserialization of a query parameter with specific styles and options:
+
+```powershell
+Start-PodeServer {
+ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
+
+ Add-PodeRoute -Method Get -Path '/items' -ScriptBlock {
+ # retrieve and deserialize the 'filter' query parameter
+ $filter = Get-PodeQueryParameter -Name 'filter' -Deserialize -Style 'SpaceDelimited' -NoExplode
+
+ # get items based on the deserialized filter data
+ $items = Get-DummyItems -Filter $filter
+
+ # return the item details
+ Write-PodeJsonResponse -Value $items
+ }
+}
+```
+
+In this example, the `Get-PodeQueryParameter` function is used to deserialize the `filter` query parameter, interpreting it according to the specified style (`SpaceDelimited`) and preventing array explosion (`-NoExplode`). This approach allows for dynamic and precise handling of complex query data, enhancing the flexibility of your Pode routes.
+
+For further information regarding serialization, please refer to the [RFC6570](https://tools.ietf.org/html/rfc6570).
\ No newline at end of file
diff --git a/src/Pode.psd1 b/src/Pode.psd1
index fa158a2ec..3d9cb9286 100644
--- a/src/Pode.psd1
+++ b/src/Pode.psd1
@@ -141,6 +141,11 @@
'ConvertFrom-PodeXml',
'Set-PodeDefaultFolder',
'Get-PodeDefaultFolder',
+ 'Get-PodeBodyData',
+ 'Get-PodeQueryParameter',
+ 'Get-PodePathParameter',
+ 'ConvertFrom-PodeSerializedString',
+ 'ConvertTo-PodeSerializedString',
'Get-PodeCurrentRunspaceName',
'Set-PodeCurrentRunspaceName',
'Invoke-PodeGC',
diff --git a/src/Public/Cookies.ps1 b/src/Public/Cookies.ps1
index 6bff3e2a0..65245f41d 100644
--- a/src/Public/Cookies.ps1
+++ b/src/Public/Cookies.ps1
@@ -109,69 +109,112 @@ function Set-PodeCookie {
<#
.SYNOPSIS
-Retrieves a cookie from the Request.
+ Retrieves a specified cookie from the incoming request.
.DESCRIPTION
-Retrieves a cookie from the Request, with the option to supply a secret to unsign the cookie's value.
+ The `Get-PodeCookie` function retrieves a cookie from the incoming request.
+ It can unsign the cookie's value using a specified secret, which can be extended with the client request's UserAgent and RemoteIPAddress if `-Strict` is specified. The function also allows for returning the raw .NET Cookie object for direct manipulation or deserializing serialized cookie values for more complex handling.
.PARAMETER Name
-The name of the cookie to retrieve.
+ The name of the cookie to retrieve. This parameter is mandatory.
.PARAMETER Secret
-The secret used to unsign the cookie's value.
+ The secret used to unsign the cookie's value, ensuring the integrity and authenticity of the cookie data.
+ Applicable only in the 'BuiltIn' parameter set.
.PARAMETER Strict
-If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
+ If specified, the secret is extended using the client's UserAgent and RemoteIPAddress, adding an extra layer of
+ security when unsigning the cookie. Applicable only in the 'BuiltIn' parameter set.
.PARAMETER Raw
-If supplied, the cookie returned will be the raw .NET Cookie object for manipulation.
+ If specified, the cookie returned will be the raw .NET Cookie object, allowing for direct manipulation of
+ the cookie. This is useful for scenarios where the full cookie object is needed. Applicable only in the 'BuiltIn' parameter set.
+
+.PARAMETER Deserialize
+ Indicates that the retrieved cookie value should be deserialized. When this switch is used, the value will be
+ interpreted based on the deserialization options provided. This parameter is mandatory in the 'Deserialize' parameter set.
+
+.PARAMETER NoExplode
+ Prevents deserialization from exploding arrays in the cookie value, which is useful when handling comma-separated
+ values without expanding them into arrays. Applicable only when the `-Deserialize` switch is used.
+
+.EXAMPLE
+ Get-PodeCookie -Name 'Views'
+ Retrieves the value of the 'Views' cookie from the request.
.EXAMPLE
-Get-PodeCookie -Name 'Views'
+ Get-PodeCookie -Name 'Views' -Secret 'hunter2'
+ Retrieves and unsigns the 'Views' cookie using the specified secret.
.EXAMPLE
-Get-PodeCookie -Name 'Views' -Secret 'hunter2'
+ Get-PodeCookie -Name 'Session' -Deserialize -NoExplode
+ Retrieves and deserializes the 'Session' cookie value without exploding arrays.
+
+.EXAMPLE
+ Get-PodeCookie -Name 'AuthToken' -Raw
+ Retrieves the raw .NET Cookie object for the 'AuthToken' cookie, allowing for direct manipulation.
+
+.NOTES
+ This function should be used within a route's script block in a Pode server. The `-Deserialize` switch provides
+ advanced handling of serialized cookie values, while the `-Secret` and `-Strict` options offer secure methods for
+ unsigning cookies.
#>
function Get-PodeCookie {
- [CmdletBinding()]
- [OutputType([hashtable])]
+ [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
param(
- [Parameter(Mandatory = $true)]
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
[string]
$Name,
- [Parameter()]
+ [Parameter(ParameterSetName = 'BuiltIn')]
[string]
$Secret,
+ [Parameter(ParameterSetName = 'BuiltIn')]
[switch]
$Strict,
+ [Parameter(ParameterSetName = 'BuiltIn')]
[switch]
- $Raw
+ $Raw,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [switch]
+ $NoExplode,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [switch]
+ $Deserialize
)
+ if ($WebEvent) {
+ # get the cookie from the request
+ $cookie = $WebEvent.Cookies[$Name]
+ if (!$Raw) {
+ $cookie = (ConvertTo-PodeCookie -Cookie $cookie)
+ }
- # get the cookie from the request
- $cookie = $WebEvent.Cookies[$Name]
- if (!$Raw) {
- $cookie = (ConvertTo-PodeCookie -Cookie $cookie)
- }
+ if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) {
+ return $null
+ }
- if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) {
- return $null
- }
+ if ($Deserialize.IsPresent) {
+ $cookie.Value = ConvertFrom-PodeSerializedString -SerializedString $cookie.Value -Style 'Form' -Explode:(!$NoExplode)
+ }
- # if a secret was supplied, attempt to unsign the cookie
- if (![string]::IsNullOrWhiteSpace($Secret)) {
- $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret -Strict:$Strict)
- if (![string]::IsNullOrWhiteSpace($value)) {
- $cookie.Value = $value
+ # if a secret was supplied, attempt to unsign the cookie
+ if (![string]::IsNullOrWhiteSpace($Secret)) {
+ $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret -Strict:$Strict)
+ if (![string]::IsNullOrWhiteSpace($value)) {
+ $cookie.Value = $value
+ }
}
- }
- return $cookie
+ return $cookie
+ }
}
+
<#
.SYNOPSIS
Retrieves the value of a cookie from the Request.
diff --git a/src/Public/Headers.ps1 b/src/Public/Headers.ps1
index 84a907b2f..9b8db7173 100644
--- a/src/Public/Headers.ps1
+++ b/src/Public/Headers.ps1
@@ -133,48 +133,88 @@ function Test-PodeHeader {
<#
.SYNOPSIS
-Retrieves the value of a header from the Request.
+ Retrieves the value of a specified header from the incoming request.
.DESCRIPTION
-Retrieves the value of a header from the Request.
+ The `Get-PodeHeader` function retrieves the value of a specified header from the incoming request.
+ It supports deserialization of header values and can optionally unsign the header using a specified secret.
+ The unsigning process can be further secured with the client's UserAgent and RemoteIPAddress if `-Strict` is specified.
.PARAMETER Name
-The name of the header to retrieve.
+ The name of the header to retrieve. This parameter is mandatory.
.PARAMETER Secret
-The secret used to unsign the header's value.
+ The secret used to unsign the header's value. This option is useful when working with signed headers to ensure
+ the integrity and authenticity of the value. Applicable only in the 'BuiltIn' parameter set.
.PARAMETER Strict
-If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
+ If specified, the secret is extended using the client's UserAgent and RemoteIPAddress, providing an additional
+ layer of security during the unsigning process. Applicable only in the 'BuiltIn' parameter set.
+
+.PARAMETER Deserialize
+ Indicates that the retrieved header value should be deserialized. When this switch is used, the value will be
+ interpreted based on the provided deserialization options. This parameter is mandatory in the 'Deserialize' parameter set.
+
+.PARAMETER Explode
+ Specifies whether the deserialization process should explode arrays in the header value. This is useful when
+ handling comma-separated values within the header. Applicable only when the `-Deserialize` switch is used.
+
+.EXAMPLE
+ Get-PodeHeader -Name 'X-AuthToken'
+ Retrieves the value of the 'X-AuthToken' header from the request.
.EXAMPLE
-Get-PodeHeader -Name 'X-AuthToken'
+ Get-PodeHeader -Name 'X-SerializedHeader' -Deserialize -Explode
+ Retrieves and deserializes the value of the 'X-SerializedHeader' header, exploding arrays if present.
+
+.EXAMPLE
+ Get-PodeHeader -Name 'X-AuthToken' -Secret 'MySecret' -Strict
+ Retrieves and unsigns the 'X-AuthToken' header using the specified secret, extending it with UserAgent and
+ RemoteIPAddress information for added security.
+
+.NOTES
+ This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables
+ advanced handling of serialized header values, while the `-Secret` and `-Strict` options provide secure unsigning
+ capabilities for signed headers.
#>
function Get-PodeHeader {
- [CmdletBinding()]
- [OutputType([string])]
+ [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
param(
- [Parameter(Mandatory = $true)]
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
[string]
$Name,
- [Parameter()]
+ [Parameter(ParameterSetName = 'BuiltIn')]
[string]
$Secret,
+ [Parameter(ParameterSetName = 'BuiltIn')]
[switch]
- $Strict
+ $Strict,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [switch]
+ $Explode,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [switch]
+ $Deserialize
)
+ if ($WebEvent) {
+ # get the value for the header from the request
+ $header = $WebEvent.Request.Headers.$Name
- # get the value for the header from the request
- $header = $WebEvent.Request.Headers.$Name
+ if ($Deserialize.IsPresent) {
+ return ConvertFrom-PodeSerializedString -SerializedString $header -Style 'Simple' -Explode:$Explode
+ }
+ # if a secret was supplied, attempt to unsign the header's value
+ if (![string]::IsNullOrWhiteSpace($Secret)) {
+ $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict)
+ }
- # if a secret was supplied, attempt to unsign the header's value
- if (![string]::IsNullOrWhiteSpace($Secret)) {
- $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict)
+ return $header
}
-
- return $header
}
<#
diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1
index 9391e8845..64d21b677 100644
--- a/src/Public/Utilities.ps1
+++ b/src/Public/Utilities.ps1
@@ -1529,13 +1529,13 @@ function ConvertFrom-PodeXml {
<#
.SYNOPSIS
-Invokes the garbage collector.
+ Invokes the garbage collector.
.DESCRIPTION
-Invokes the garbage collector.
+ Invokes the garbage collector.
.EXAMPLE
-Invoke-PodeGC
+ Invoke-PodeGC
#>
function Invoke-PodeGC {
[CmdletBinding()]
@@ -1620,3 +1620,1326 @@ function Start-PodeSleep {
+
+<#
+.SYNOPSIS
+ Converts an object (hashtable or array) to a serialized string using a specified serialization style.
+
+.DESCRIPTION
+ The `ConvertTo-PodeSerializedString` function takes a hashtable or array and converts it into a serialized string
+ according to the specified serialization style. It supports various styles such as 'Simple', 'Label', 'Matrix',
+ 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'.
+
+ By default, parameter names and values are URL-encoded to ensure safe inclusion in URLs. You can disable URL encoding
+ by using the `-NoUrlEncode` switch.
+
+ An optional `-Explode` switch can be used to modify the serialization format for certain styles, altering how arrays
+ and objects are represented in the serialized string.
+
+.PARAMETER InputObject
+ The object to be serialized. This can be a hashtable (or ordered dictionary) or an array. Supports pipeline input.
+
+.PARAMETER Style
+ The serialization style to use. Valid values are 'Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited',
+ 'PipeDelimited', and 'DeepObject'. Defaults to 'Simple'.
+
+.PARAMETER Explode
+ An optional switch to modify the serialization format for certain styles. When used, arrays and objects are
+ serialized in an expanded form.
+
+.PARAMETER NoUrlEncode
+ An optional switch to disable URL encoding of the serialized output. By default, parameter names and values are
+ URL-encoded individually. Use this switch if you require the output without URL encoding.
+
+.PARAMETER ParameterName
+ Specifies the name of the parameter to use in the serialized output. Defaults to 'id' if not specified.
+
+.EXAMPLE
+ $item = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ }
+ $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form'
+ Write-Output $serialized
+
+ # Output:
+ # ?id=name%2Cvalue%2CanotherName%2CanotherValue
+
+.EXAMPLE
+ $item = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ }
+ $serializedExplode = ConvertTo-PodeSerializedString -InputObject $item -Style 'DeepObject' -Explode
+ Write-Output $serializedExplode
+
+ # Output:
+ # ?id[name]=value&id[anotherName]=anotherValue
+
+.EXAMPLE
+ $array = @('3', '4', '5')
+ $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -Explode
+ Write-Output $serialized
+
+ # Output:
+ # ?id=3&id=4&id=5
+
+.EXAMPLE
+ $array = @('3', '4', '5')
+ $serialized = ConvertTo-PodeSerializedString -InputObject $array -Style 'SpaceDelimited' -NoUrlEncode
+ Write-Output $serialized
+
+ # Output:
+ # ?id=3 4 5
+
+.EXAMPLE
+ $item = @{
+ 'user name' = 'Alice & Bob'
+ 'role' = 'Admin/User'
+ }
+ $serialized = ConvertTo-PodeSerializedString -InputObject $item -Style 'Form' -ParameterName 'account' -NoUrlEncode
+ Write-Output $serialized
+
+ # Output:
+ # ?account=user name,Alice & Bob,role,Admin/User
+
+.NOTES
+ - 'SpaceDelimited' and 'PipeDelimited' styles for hashtables are not implemented as they are not defined by RFC 6570.
+ - The 'Form' style with 'Explode' for arrays is not implemented for the same reason.
+ - The 'Explode' option for 'SpaceDelimited' and 'PipeDelimited' styles for arrays is implemented as per the OpenAPI Specification.
+
+ Additional information regarding serialization:
+ - OpenAPI Specification Serialization: https://swagger.io/docs/specification/serialization/
+ - RFC 6570 - URI Template: https://tools.ietf.org/html/rfc6570
+#>
+function ConvertTo-PodeSerializedString {
+ param (
+ [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)]
+ [psobject[]]
+ $InputObject,
+
+ [Parameter()]
+ [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')]
+ [string]
+ $Style = 'Simple',
+
+ [Parameter()]
+ [switch]
+ $Explode,
+
+ [Parameter()]
+ [switch]
+ $NoUrlEncode,
+
+ [Parameter()]
+ [string]
+ $ParameterName = 'id' # Default parameter name
+ )
+
+ begin {
+ # Initialize an array to collect pipeline input
+ $pipelineValue = @()
+ }
+
+ process {
+ # Collect each input object from the pipeline
+ $pipelineValue += $_
+ }
+
+ end {
+ # Determine if multiple objects were provided via pipeline
+ if ($pipelineValue.Count -gt 1) {
+ $inputObjects = $pipelineValue
+ }
+ else {
+ $inputObjects = $InputObject
+ }
+
+ # Initialize an array to store the serialized strings
+ $serializedArray = @()
+
+ # return '' if the inputObjects is null
+ if($null -eq $inputObjects){
+ return ''
+ }
+
+ # Check if there are input objects to process
+ if ( $inputObjects.Count -gt 0) {
+
+ # Check if the first input object is a hashtable or ordered dictionary
+ if ($inputObjects[0] -is [hashtable] -or $inputObjects[0] -is [System.Collections.Specialized.OrderedDictionary]) {
+
+ # Process each hashtable item
+ foreach ($item in $inputObjects) {
+ switch ($Style) {
+
+ 'Simple' {
+ # Handle 'Simple' style for hashtables
+ if ($Explode) {
+ # Serialize each key-value pair with '=' and join with ','
+ $serializedArray += ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ # URL-encode unless $NoUrlEncode is specified
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key=$value"
+ }) -join ',' )
+ }
+ else {
+ # Serialize each key-value pair with ',' and join with ','
+ $serializedArray += ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key,$value"
+ }) -join ',' )
+ }
+ break
+ }
+
+ 'Label' {
+ # Handle 'Label' style for hashtables
+ if ($Explode) {
+ # Prepend '.' and serialize each key-value pair with '='
+ $serializedArray += '.' + ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key=$value"
+ }) -join ',' )
+ }
+ else {
+ # Prepend '.' and serialize each key-value pair with ','
+ $serializedArray += '.' + ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key,$value"
+ }) -join ',' )
+ }
+ break
+ }
+
+ 'Matrix' {
+ # Handle 'Matrix' style for hashtables
+ if ($Explode) {
+ # Serialize each key-value pair with ';' prefix
+ $serializedArray += ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ ";$key=$value"
+ }) -join '' )
+ }
+ else {
+ # Serialize key-value pairs into a single parameter
+ $valueString = ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key,$value"
+ }) -join ',' )
+ # Encode parameter name if necessary
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ $serializedArray += ";$parameterName=$valueString"
+ }
+ break
+ }
+
+ 'Form' {
+ # Handle 'Form' style for hashtables
+ if ($Explode) {
+ # Serialize each key-value pair as query parameters
+ $serializedArray += '?' + ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key=$value"
+ }) -join '&' )
+ }
+ else {
+ # Serialize key-value pairs into a single query parameter
+ $valueString = ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$key,$value"
+ }) -join ',' )
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ $serializedArray += "?$parameterName=$valueString"
+ }
+ break
+ }
+
+ 'DeepObject' {
+ # Handle 'DeepObject' style for hashtables
+ # Encode parameter name once outside the loop
+ if (-not $NoUrlEncode) {
+ $parameterNameEncoded = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterNameEncoded = $ParameterName
+ }
+ # Serialize each key-value pair using bracket notation
+ $serializedArray += '?' + ( ($item.Keys | ForEach-Object {
+ $key = $_
+ $value = $item[$_]
+ if (-not $NoUrlEncode) {
+ $key = [uri]::EscapeDataString($key)
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$parameterNameEncoded`[$key`]=$value"
+ }) -join '&' )
+ break
+ }
+
+ # Styles not defined for hashtables
+ 'SpaceDelimited' {
+ $serializedArray += ''
+ Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570."
+ }
+
+ 'PipeDelimited' {
+ $serializedArray += ''
+ Write-Verbose "Serialization for objects using '$Style' style is not defined by RFC 6570."
+ }
+ }
+ }
+ }
+ else {
+ # Process input as an array
+ switch ($Style) {
+
+ 'Simple' {
+ # Handle 'Simple' style for arrays
+ # Both 'Explode' and non-'Explode' result in the same output
+ $serializedArray += ( ($inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ $value
+ }) -join ',' )
+ break
+ }
+
+ 'Label' {
+ # Handle 'Label' style for arrays
+ $serializedArray += '.' + ( ($inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ $value
+ }) -join ',' )
+ break
+ }
+
+ 'Matrix' {
+ # Handle 'Matrix' style for arrays
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ if ($Explode) {
+ # Serialize each value with parameter name
+ $serializedArray += ';' + ( ($inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$parameterName=$value"
+ }) -join ';' )
+ }
+ else {
+ # Serialize values into a single parameter
+ $valueString = ( ($inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ $value
+ }) -join ',' )
+ $serializedArray += ";$parameterName=$valueString"
+ }
+ break
+ }
+
+ 'SpaceDelimited' {
+ # Handle 'SpaceDelimited' style for arrays
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ if ($Explode) {
+ # Serialize each value as a separate parameter
+ $valueStrings = $inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$parameterName=$value"
+ }
+ $serializedArray += '?' + ($valueStrings -join '&')
+ }
+ else {
+ # Join values with a space
+ $valueString = ($inputObjects -join ' ')
+ if (-not $NoUrlEncode) {
+ $valueString = [uri]::EscapeDataString($valueString)
+ }
+ $serializedArray += "?$parameterName=$valueString"
+ }
+ break
+ }
+
+ 'PipeDelimited' {
+ # Handle 'PipeDelimited' style for arrays
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ if ($Explode) {
+ # Serialize each value as a separate parameter
+ $valueStrings = $inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ "$parameterName=$value"
+ }
+ $serializedArray += '?' + ($valueStrings -join '&')
+ }
+ else {
+ # Join values with a pipe '|'
+ $valueString = ($inputObjects -join '|')
+ if (-not $NoUrlEncode) {
+ $valueString = [uri]::EscapeDataString($valueString)
+ }
+ $serializedArray += "?$parameterName=$valueString"
+ }
+ break
+ }
+
+ 'Form' {
+ # Handle 'Form' style for arrays
+ if (-not $NoUrlEncode) {
+ $parameterName = [uri]::EscapeDataString($ParameterName)
+ }
+ else {
+ $parameterName = $ParameterName
+ }
+ if ($Explode) {
+ # 'Explode' is not defined for arrays in 'Form' style
+ $serializedArray += ''
+ Write-Verbose "Serialization for array using '$Style' style with 'Explode' is not defined by RFC 6570."
+ }
+ else {
+ # Serialize values into a single parameter
+ $valueString = ( ($inputObjects | ForEach-Object {
+ $value = $_
+ if (-not $NoUrlEncode) {
+ $value = [uri]::EscapeDataString($value)
+ }
+ $value
+ }) -join ',' )
+ $serializedArray += "$parameterName=$valueString"
+ }
+ break
+ }
+
+ # 'DeepObject' is not defined for arrays
+ 'DeepObject' {
+ $serializedArray += ''
+ Write-Verbose "Serialization for arrays using '$Style' style is not defined by RFC 6570."
+ }
+ }
+ }
+ }
+
+ # Return the serialized string(s)
+ return $serializedArray
+ }
+}
+
+
+<#
+.SYNOPSIS
+ Converts a serialized string back into its original data structure based on the specified serialization style.
+
+.DESCRIPTION
+ The `ConvertFrom-PodeSerializedString` function takes a serialized string and converts it back into its original data structure (e.g., hashtable, array).
+ The function requires the serialization style to be specified via the `-Style` parameter.
+ Supported styles are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'.
+ The function also accepts an optional `-Explode` switch to indicate whether the string uses exploded serialization.
+ The `-ParameterName` parameter can be used to specify the key name when processing certain styles, such as 'Matrix' and 'DeepObject'.
+
+.PARAMETER SerializedInput
+ The serialized string to be converted back into its original data structure.
+
+.PARAMETER Style
+ The serialization style to use for deserialization. Options are 'Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'.
+
+.PARAMETER Explode
+ Indicates whether the string uses exploded serialization (`-Explode`) or not (omit `-Explode`). This affects how arrays and objects are handled.
+
+.PARAMETER ParameterName
+ Specifies the key name to match when processing certain styles, such as 'Matrix' and 'DeepObject'. The default is 'id'.
+
+.PARAMETER UrlDecode
+ If specified, the function will decode the input string using URL decoding before processing it. This is useful
+ for handling serialized inputs that include URL-encoded characters, such as `%20` for spaces.
+
+.EXAMPLE
+ # Simple style, explode = true
+ $serialized = "name=value,anotherName=anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple' -Explode
+ Write-Output $result
+
+.EXAMPLE
+ # Simple style, explode = false
+ $serialized = "name,value,anotherName,anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Simple'
+ Write-Output $result
+
+.EXAMPLE
+ # Label style, explode = true
+ $serialized = ".name=value.anotherName=anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label' -Explode
+ Write-Output $result
+
+.EXAMPLE
+ # Label style, explode = false
+ $serialized = ".name,value,anotherName,anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Label'
+ Write-Output $result
+
+.EXAMPLE
+ # Matrix style, explode = true
+ $serialized = ";name=value;anotherName=anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -Explode
+ Write-Output $result
+
+.EXAMPLE
+ # Matrix style, explode = false
+ $serialized = ";id=3,4,5"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Matrix' -ParameterName 'id'
+ Write-Output $result
+
+.EXAMPLE
+ # Query style, explode = true
+ $serialized = "?name=value&anotherName=anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query' -Explode
+ Write-Output $result
+
+.EXAMPLE
+ # Query style, explode = false
+ $serialized = "?name,value,anotherName,anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Query'
+ Write-Output $result
+
+.EXAMPLE
+ # Form style, explode = true
+ $serialized = "?name=value&anotherName=anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form' -Explode
+ Write-Output $result
+
+.EXAMPLE
+ # Form style, explode = false
+ $serialized = "?name,value,anotherName,anotherValue"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'Form'
+ Write-Output $result
+
+.EXAMPLE
+ # SpaceDelimited style, explode = true
+ $serialized = "?id=3&id=4&id=5"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -Explode -ParameterName 'id'
+ Write-Output $result
+
+.EXAMPLE
+ # SpaceDelimited style, explode = false
+ $serialized = "?id=3%204%205"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'SpaceDelimited' -ParameterName 'id'
+ Write-Output $result
+
+.EXAMPLE
+ # PipeDelimited style, explode = true
+ $serialized = "?id=3&id=4&id=5"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -Explode -ParameterName 'id'
+ Write-Output $result
+
+.EXAMPLE
+ # PipeDelimited style, explode = false
+ $serialized = "?id=3|4|5"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'PipeDelimited' -ParameterName 'id'
+ Write-Output $result
+
+.EXAMPLE
+ # DeepObject style
+ $serialized = "myId[role]=admin&myId[firstName]=Alex"
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'DeepObject' -ParameterName 'myId'
+ Write-Output $result
+
+.NOTES
+ For more information on serialization styles, refer to:
+ - https://swagger.io/docs/specification/serialization/
+ - https://tools.ietf.org/html/rfc6570
+#>
+
+function ConvertFrom-PodeSerializedString {
+ param (
+ [Parameter(Mandatory, ValueFromPipeline = $true, Position = 0)]
+ [string] $SerializedInput,
+
+ [Parameter()]
+ [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
+ [string]
+ $Style = 'Form',
+
+ [Parameter()]
+ [switch]
+ $Explode,
+
+ [Parameter()]
+ [string]
+ $ParameterName = 'id', # Default key name if not specified
+
+ [Parameter()]
+ [switch]
+ $UrlDecode
+ )
+
+ process {
+ if($UrlDecode){
+ $SerializedInput = [System.Web.HttpUtility]::UrlDecode($SerializedInput)
+ }
+ # Main deserialization logic based on style
+ switch ($Style) {
+ 'Simple' {
+ # Check for header pattern and extract it if present
+ if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') {
+ # Extract the variable name and strip it from the serialized string
+ $headerName = $matches[1]
+ $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim()
+ }
+
+ $segments = $SerializedInput -split ','
+
+ # If there's only one segment, return it directly
+ if ($segments.Count -eq 1) {
+ $result = $segments[0]
+ }
+ else {
+ if ($Explode) {
+ # Handling explode=true case
+
+ # Check if the number of '=' is equal to the count of segments
+ if ((($SerializedInput -split '=').Count - 1) -eq $segments.Count) {
+ $obj = @{}
+ foreach ($pair in $segments) {
+ if ($pair.Contains('=')) {
+ $key, $value = $pair -split '=', 2 # Split into exactly two parts
+ $obj[$key] = $value
+ }
+ }
+ $result = $obj
+ }
+ else {
+ # Return as an array if the explode conditions don't match
+ $result = $segments
+ }
+ }
+ else {
+ # Handling explode=false case
+
+ # Check if it's likely an object by checking if the count of segments is even
+ if ($segments.Count % 2 -eq 0) {
+ # Try to parse as an object
+ $obj = @{}
+ for ($i = 0; $i -lt $segments.Count; $i += 2) {
+ $key = $segments[$i]
+ # Validate the key format
+ if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') {
+ $obj[$key] = $segments[$i + 1]
+ }
+ else {
+ # If the key is invalid, return the original segments as an array
+ $result = $segments
+ }
+ }
+ # Return the object if all keys are valid
+ $result = $obj
+ }
+ else {
+ # If not an object, treat it as an array
+ $result = $segments
+ }
+ }
+ }
+
+ if ($headerName) {
+ return @{$headerName = $result }
+ }
+ else {
+ return $result
+ }
+
+ }
+ 'Label' {
+ # Remove the leading dot (.) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart('.')
+
+ # Split the string by dot
+ $segments = $SerializedInput -split '\.'
+
+ # Handle the explode=true case
+ if ($Explode) {
+ # Handling explode=true: each segment is a key=value pair
+ $obj = @{}
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ $key, $value = $segment -split '=', 2 # Split into exactly two parts
+ $obj[$key] = $value
+ }
+ else {
+ # If a segment does not contain '=', treat it as an array element
+ return $segments -split ','
+ }
+ }
+ return $obj
+ }
+ else {
+ # Handling explode=false: all segments form a combined structure
+ # Split the string by commas within each segment
+ $combinedSegments = ($SerializedInput -split ',')
+
+ # Check if it's likely an object by checking if the count is even
+ if ($combinedSegments.Count % 2 -eq 0) {
+ # Try to parse as an object
+ $obj = @{}
+ for ($i = 0; $i -lt $combinedSegments.Count; $i += 2) {
+ $key = $combinedSegments[$i]
+
+ # Validate if the key is a suitable key
+ if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') {
+ $value = $combinedSegments[$i + 1]
+ $obj[$key] = $value
+ }
+ else {
+ # If validation fails, return segments as array
+ return $combinedSegments
+ }
+ }
+ return $obj
+ }
+
+ # If not an object, return as an array
+ return $combinedSegments
+ }
+ }
+ 'Matrix' {
+ # Handle the explode=true case
+ if ($Explode) {
+ # Remove the leading semicolon (;) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart(';')
+
+ # Split by semicolon to get segments
+ $segments = $SerializedInput -split ';'
+
+ # If each segment doesn't contain '=', treat it as an array
+ if ($segments -notmatch '=') {
+ # Return as an array of individual elements split by commas
+ return $segments -split ','
+ }
+
+ # Initialize an empty hashtable to store key-value pairs
+ $obj = @{}
+ $values = @()
+
+
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ $key, $value = $segment -split '=', 2
+
+ # If the key matches the specified key name
+ if ($key -eq $ParameterName) {
+ $values += $value
+ }
+ else {
+ # If a key doesn't match, treat as a normal key-value pair in the hashtable
+ $obj[$key] = $value
+ }
+ }
+ }
+
+ # If all segments matched the specified key name, return the values as an array
+ if ($values.Count -eq $segments.Count) {
+ if ($values.Count -eq 1) {
+ return $values[0]
+ }
+ return $values
+ }
+
+ # Merge values back into the object if any key matches the KeyName
+ if ($values.Count -gt 0) {
+ $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values }
+ }
+
+ # Return the hashtable if it contains any key-value pairs
+ if ($obj.Count -gt 0) {
+ return $obj
+ }
+ else {
+ return $values
+ }
+ }
+ else {
+ # Handling explode=false:
+
+ # Remove the leading semicolon (;) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart(";$ParameterName=")
+
+ # Split by semicolon to get segments
+ $segments = $SerializedInput -split ','
+
+ # If there's only one segment, return it directly
+ if ($segments.Count -eq 1) {
+ return $segments[0]
+ }
+
+ # Check if it's likely an object by checking if the count of segments is even
+ if ($segments.Count % 2 -eq 0) {
+ # Try to parse as an object
+ $obj = @{}
+ for ($i = 0; $i -lt $segments.Count; $i += 2) {
+ $key = $segments[$i]
+ # Validate the key format
+ if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') {
+ $obj[$key] = $segments[$i + 1]
+ }
+ else {
+ # If the key is invalid, return the original segments as an array
+ return $segments
+ }
+ }
+ # Return the object if all keys are valid
+ return $obj
+ }
+
+ # If not an object, treat it as an array
+ return $segments
+ }
+ }
+
+ 'Form' {
+ # Check for header pattern and extract it if present
+ if ($SerializedInput -match '^([a-zA-Z0-9_-]+):') {
+ # Extract the variable name and strip it from the serialized string
+ $headerName = $matches[1]
+ $SerializedInput = ($SerializedInput -replace "^$($headerName):", '').Trim().TrimStart("$ParameterName=")
+ }
+ else {
+ if ($Explode) {
+ # Remove the leading semicolon (;) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart('?')
+ }
+ else {
+ # Remove the leading semicolon (;) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart("?$ParameterName=")
+ }
+ }
+
+ # Handle the explode=true case
+ if ($Explode) {
+ # Split by semicolon to get segments
+ $segments = $SerializedInput -split '&'
+
+ # If each segment doesn't contain '=', treat it as an array
+ if ($segments -notmatch '=') {
+ # Return as an array of individual elements split by commas
+ $result = $segments -split ','
+ }
+ else {
+ # Initialize an empty hashtable to store key-value pairs
+ $obj = @{}
+ $values = @()
+
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ $key, $value = $segment -split '=', 2
+
+ # If the key matches the specified key name
+ if ($key -eq $ParameterName) {
+ $values += $value
+ }
+ else {
+ # If a key doesn't match, treat as a normal key-value pair in the hashtable
+ $obj[$key] = $value
+ }
+ }
+ }
+
+ # If all segments matched the specified key name, return the values as an array
+ if ($values.Count -eq $segments.Count) {
+ if ($values.Count -eq 1) {
+ $result = $values[0]
+ }
+ else {
+ $result = $values
+ }
+ }
+ else {
+
+ # Merge values back into the object if any key matches the KeyName
+ if ($values.Count -gt 0) {
+ $obj[$ParameterName] = if ($values.Count -eq 1) { $values[0] } else { $values }
+ }
+
+ # Return the hashtable if it contains any key-value pairs
+ if ($obj.Count -gt 0) {
+ return $obj
+ }
+ else {
+ return $values
+ }
+ }
+ }
+ }
+ else {
+ # Handling explode=false
+
+ # Split by semicolon to get segments
+ $segments = $SerializedInput -split ','
+
+ # If there's only one segment, return it directly
+ if ($segments.Count -eq 1) {
+ $result = $segments[0]
+ }
+ # Check if it's likely an object by checking if the count of segments is even
+ elseif ($segments.Count % 2 -eq 0) {
+ # Try to parse as an object
+ $obj = @{}
+ for ($i = 0; $i -lt $segments.Count; $i += 2) {
+ $key = $segments[$i]
+ # Validate the key format
+ if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') {
+ $obj[$key] = $segments[$i + 1]
+ }
+ else {
+ # If the key is invalid, return the original segments as an array
+ $result = $segments
+ break
+ }
+ }
+ if (!$result) {
+ # Return the object if all keys are valid
+ $result = $obj
+ }
+ }
+ else {
+ # If not an object, treat it as an array
+ $result = $segments
+ }
+
+ }
+
+ if ($headerName) {
+ return @{$headerName = $result }
+ }
+ else {
+ return $result
+ }
+ }
+
+ 'SpaceDelimited' {
+ if ($Explode) {
+ # Remove the leading semicolon (;) prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart('?')
+
+ # For explode=true, split by '&' to treat each value as a separate occurrence
+ $segments = $SerializedInput -split '&'
+
+ # Initialize an array to store values that match the specified KeyName
+ $values = @()
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ $key, $value = $segment -split '=', 2
+ # Only add values where the key matches the specified KeyName
+ if ($key -eq $ParameterName) {
+ $values += $value
+ }
+ }
+ }
+ # Return the array of values that matched the KeyName
+ return $values
+ }
+ else {
+ # Remove the leading semicolon '?id=' prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart('?id=')
+ # For explode=false, split by space (%20) to handle the combined string format
+ return $SerializedInput -split ' '
+ }
+ }
+
+ 'PipeDelimited' {
+ if ($Explode) {
+ $SerializedInput = $SerializedInput.TrimStart('?')
+ # For explode=true, split by '&' to treat each value as a separate occurrence
+ $segments = $SerializedInput -split '&'
+
+ # Initialize an array to store values that match the specified KeyName
+ $values = @()
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ $key, $value = $segment -split '=', 2
+ # Only add values where the key matches the specified KeyName
+ if ($key -eq $ParameterName) {
+ $values += $value
+ }
+ }
+ }
+ # Return the array of values that matched the KeyName
+ return $values
+ }
+ else {
+ # Remove the leading '?id=' prefix from the serialized string
+ $SerializedInput = $SerializedInput.TrimStart('?id=')
+ # For explode=false, split by | to handle the combined string format
+ return $SerializedInput -split '\|'
+ }
+ }
+
+ 'DeepObject' {
+ $SerializedInput = $SerializedInput.TrimStart('?')
+
+ # Split the string by '&' to get each key-value pair
+ $segments = $SerializedInput -split '&'
+
+ # Initialize an empty hashtable to store the nested key-value pairs
+ $obj = @{}
+ foreach ($segment in $segments) {
+ if ($segment.Contains('=')) {
+ # Split each segment by '=' into key and value
+ $key, $value = $segment -split '=', 2
+
+ # Extract the main key and nested keys using regex
+ $allMatches = [regex]::Matches($key, '([^\[\]]+)')
+
+ # Extract the main key (first match) and remaining nested keys
+ $mainKey = $allMatches[0].Groups[1].Value
+ # Manually extract remaining nested keys as a list of strings
+ $nestedKeys = @()
+ for ($i = 1; $i -lt $allMatches.Count; $i++) {
+ $nestedKeys += $allMatches[$i].Groups[1].Value
+ }
+
+ # Only process the segment if the main key matches the specified KeyName
+ if ($mainKey -eq $ParameterName) {
+ # Initialize a reference to the root object
+ $current = $obj
+
+ # Iterate over the nested keys to build the structure
+ foreach ($nestedKey in $nestedKeys) {
+ # If this is the last key, assign the value
+ if ($nestedKey -eq $nestedKeys[-1]) {
+ $current[$nestedKey] = $value
+ }
+ else {
+ # Create a new hashtable if the nested key doesn't exist
+ if (-not $current.ContainsKey($nestedKey)) {
+ $current[$nestedKey] = @{}
+ }
+ # Move deeper into the nested structure
+ $current = $current[$nestedKey]
+ }
+ }
+ }
+ }
+ }
+
+ # Return the constructed hashtable with nested keys and values
+ return $obj
+ }
+
+ }
+ }
+}
+
+<#
+.SYNOPSIS
+ Retrieves a specific parameter value from the current Pode web event.
+
+.DESCRIPTION
+ The `Get-PodePathParameter` function extracts and returns the value of a specified parameter
+ from the current Pode web event. This function can access parameters passed in the URL path, query string,
+ or body of a web request, making it useful in web applications to dynamically handle incoming data.
+
+ The function supports deserialization of parameter values when the `-Deserialize` switch is used.
+ This allows for interpreting serialized data structures, like arrays or complex objects, from the web request.
+
+.PARAMETER Name
+ The name of the parameter to retrieve. This parameter is mandatory.
+
+.PARAMETER Deserialize
+ Specifies that the parameter value should be deserialized. When this switch is used, the value will be interpreted
+ based on the provided style and other deserialization options.
+
+.PARAMETER Explode
+ Specifies whether to explode arrays when deserializing the parameter value. This is useful when parameters contain
+ comma-separated values. Applicable only when the `-Deserialize` switch is used.
+
+.PARAMETER Style
+ Defines the deserialization style to use when interpreting the parameter value. Valid options are 'Simple', 'Label',
+ and 'Matrix'. The default is 'Simple'. Applicable only when the `-Deserialize` switch is used.
+
+.PARAMETER ParameterName
+ Specifies the key name to use when deserializing the parameter value. The default value is 'id'.
+ This option is useful for mapping the parameter data accurately during deserialization. Applicable only
+ when the `-Deserialize` switch is used.
+
+.EXAMPLE
+ Get-PodePathParameter -Name 'action'
+ Returns the value of the 'action' parameter from the current web event.
+
+.EXAMPLE
+ Get-PodePathParameter -Name 'item' -Deserialize -Style 'Label' -Explode
+ Retrieves and deserializes the value of the 'item' parameter using the 'Label' style and exploding arrays.
+
+.EXAMPLE
+ Get-PodePathParameter -Name 'id' -Deserialize -KeyName 'userId'
+ Deserializes the 'id' parameter using the key name 'userId'.
+
+.NOTES
+ This function should be used within a route's script block in a Pode server.
+ The `-Deserialize` switch enables more advanced handling of complex data structures.
+#>
+function Get-PodePathParameter {
+ [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
+ param(
+ [Parameter(Mandatory, ParameterSetName = 'Deserialize')]
+ [Parameter(Mandatory, ParameterSetName = 'BuiltIn')]
+ [string]
+ $Name,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [switch]
+ $Deserialize,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [switch]
+ $Explode,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [ValidateSet('Simple', 'Label', 'Matrix')]
+ [string]
+ $Style = 'Simple',
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [string]
+ $ParameterName = 'id'
+
+ )
+ if ($WebEvent) {
+ if ($Deserialize.IsPresent) {
+ return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Parameters[$Name] -Style $Style -Explode:$Explode -ParameterName $ParameterName
+ }
+ return $WebEvent.Parameters[$Name]
+ }
+}
+
+<#
+.SYNOPSIS
+ Retrieves the body data from the current Pode web event.
+
+.DESCRIPTION
+ The `Get-PodeBodyData` function extracts and returns the body data of the current Pode web event.
+ This function is designed to access the main content sent in web requests, including methods such as PUT, POST, or any other HTTP methods that support a request body.
+ It also supports deserialization of the body data, allowing for the interpretation of serialized content.
+
+.PARAMETER Deserialize
+ Specifies that the body data should be deserialized. When this switch is used, the body data will be interpreted
+ based on the provided style and other deserialization options.
+
+.PARAMETER NoExplode
+ Prevents deserialization from exploding arrays in the body data. This is useful when handling parameters that
+ contain comma-separated values and when array expansion is not desired. Applicable only when the `-Deserialize`
+ switch is used.
+
+.PARAMETER Style
+ Defines the deserialization style to use when interpreting the body data. Valid options are 'Simple', 'Label',
+ 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'. Applicable only
+ when the `-Deserialize` switch is used.
+
+.PARAMETER ParameterName
+ Specifies the key name to use when deserializing the body data. The default value is 'id'. This option is useful
+ for mapping the body data accurately during deserialization. Applicable only when the `-Deserialize` switch is used.
+
+.EXAMPLE
+ Get-PodeBodyData
+ Returns the body data of the current web event.
+
+.EXAMPLE
+ Get-PodeBodyData -Deserialize -Style 'Matrix'
+ Retrieves and deserializes the body data using the 'Matrix' style.
+
+.EXAMPLE
+ Get-PodeBodyData -Deserialize -NoExplode
+ Deserializes the body data without exploding arrays.
+
+.NOTES
+ This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables
+ advanced handling of complex body data structures.
+#>
+function Get-PodeBodyData {
+ [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
+ param(
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [switch]
+ $Deserialize,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [switch]
+ $NoExplode,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject')]
+ [string]
+ $Style = 'Form',
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [string]
+ $ParameterName = 'id'
+ )
+ if ($WebEvent) {
+ if ($Deserialize.IsPresent) {
+ return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Data -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName
+ }
+ return $WebEvent.Data
+ }
+}
+
+
+
+<#
+.SYNOPSIS
+ Retrieves a specific query parameter value from the current Pode web event.
+
+.DESCRIPTION
+ The `Get-PodeQueryParameter` function extracts and returns the value of a specified query parameter
+ from the current Pode web event. This function is designed to access query parameters passed in the URL of a web request,
+ enabling the handling of incoming data in web applications.
+
+ The function supports deserialization of query parameter values when the `-Deserialize` switch is used,
+ allowing for interpretation of complex data structures from the query string.
+
+.PARAMETER Name
+ The name of the query parameter to retrieve. This parameter is mandatory.
+
+.PARAMETER Deserialize
+ Specifies that the query parameter value should be deserialized. When this switch is used, the value will be
+ interpreted based on the provided style and other deserialization options.
+
+.PARAMETER NoExplode
+ Prevents deserialization from exploding arrays in the query parameter value. This is useful when handling
+ parameters that contain comma-separated values and when array expansion is not desired. Applicable only when
+ the `-Deserialize` switch is used.
+
+.PARAMETER Style
+ Defines the deserialization style to use when interpreting the query parameter value. Valid options are 'Simple',
+ 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', and 'DeepObject'. The default is 'Form'.
+ Applicable only when the `-Deserialize` switch is used.
+
+.PARAMETER ParameterName
+ Specifies the key name to use when deserializing the query parameter value. The default value is 'id'.
+ This option is useful for mapping the query parameter data accurately during deserialization. Applicable only
+ when the `-Deserialize` switch is used.
+
+.EXAMPLE
+ Get-PodeQueryParameter -Name 'userId'
+ Returns the value of the 'userId' query parameter from the current web event.
+
+.EXAMPLE
+ Get-PodeQueryParameter -Name 'filter' -Deserialize -Style 'SpaceDelimited'
+ Retrieves and deserializes the value of the 'filter' query parameter, using the 'SpaceDelimited' style.
+
+.EXAMPLE
+ Get-PodeQueryParameter -Name 'data' -Deserialize -NoExplode
+ Deserializes the 'data' query parameter value without exploding arrays.
+
+.NOTES
+ This function should be used within a route's script block in a Pode server. The `-Deserialize` switch enables
+ advanced handling of complex query parameter data structures.
+#>
+function Get-PodeQueryParameter {
+ [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
+ param(
+ [Parameter(Mandatory, ParameterSetName = 'Deserialize')]
+ [Parameter(Mandatory, ParameterSetName = 'BuiltIn')]
+ [string]
+ $Name,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'Deserialize')]
+ [switch]
+ $Deserialize,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [switch]
+ $NoExplode,
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [ValidateSet('Simple', 'Label', 'Matrix', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
+ [string]
+ $Style = 'Form',
+
+ [Parameter(ParameterSetName = 'Deserialize')]
+ [string]
+ $ParameterName = 'id'
+ )
+ if ($WebEvent) {
+ if ($Deserialize.IsPresent) {
+ return ConvertFrom-PodeSerializedString -SerializedInput $WebEvent.Query[$Name] -Style $Style -Explode:(!$NoExplode) -ParameterName $ParameterName
+ }
+ return $WebEvent.Query[$Name]
+ }
+}
+
diff --git a/tests/unit/Utility.Tests.ps1 b/tests/unit/Utility.Tests.ps1
new file mode 100644
index 000000000..2c9f5d0db
--- /dev/null
+++ b/tests/unit/Utility.Tests.ps1
@@ -0,0 +1,956 @@
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
+param()
+
+BeforeAll {
+ $path = $PSCommandPath
+ $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/'
+ Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ }
+ Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode'
+
+ $PodeContext = @{ 'Server' = $null; }
+}
+
+Describe 'ConvertFrom-PodeSerializedString' {
+
+ Describe 'Path Parameters' {
+ It 'Convert Simple(Explode) style serialized string to a primitive value' {
+ $serialized = '5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $result | Should -be '5'
+ }
+
+ It 'Convert Simple(Explode) style serialized string to hashtable' {
+ $serialized = 'role=admin,firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Simple(Explode) style serialized string to array' {
+ $serialized = '3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Simple style serialized string to a primitive value' {
+ $serialized = '5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $result | Should -be '5'
+ }
+
+ It 'Convert Simple style serialized string to hashtable' {
+ $serialized = 'role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Simple style serialized string to array' {
+ $serialized = '3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $result | Should -be @('3', '4', '5')
+ }
+
+
+ It 'Convert Label(Explode) style serialized string to a primitive value' {
+ $serialized = '.5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode
+ $result | Should -be 5
+ }
+
+ It 'Convert Label(Explode) style serialized string to hashtable' {
+ $serialized = '.role=admin.firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Label(Explode) style serialized string to array' {
+ $serialized = '.3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Simple style serialized string to a primitive value' {
+ $serialized = '.5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label
+ $result | Should -be 5
+ }
+
+ It 'Convert Label style serialized string to hashtable' {
+ $serialized = '.role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Label style serialized string to array' {
+ $serialized = '.3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label
+ $result | Should -be @('3', '4', '5')
+ }
+
+
+
+ It 'Convert Matrix(Explode) style serialized string to a primitive value' {
+ $serialized = ';id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $result | Should -be 5
+ }
+
+ It 'Convert Matrix(Explode) style serialized string to hashtable' {
+ $serialized = ';role=admin;firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Matrix(Explode) style serialized string to array' {
+ $serialized = ';id=3;id=4;id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Simple style serialized string to a primitive value' {
+ $serialized = ';id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $result | Should -be 5
+ }
+
+ It 'Convert Matrix style serialized string to hashtable' {
+ $serialized = ';id=role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Matrix style serialized string to array' {
+ $serialized = ';id=3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $result | Should -be @('3', '4', '5')
+ }
+
+
+ It 'Convert Matrix(Explode) style serialized string to a primitive value' {
+ $serialized = ';id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $result | Should -be 5
+ }
+
+ It 'Convert Matrix(Explode) style serialized string to hashtable' {
+ $serialized = ';role=admin;firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Matrix(Explode) style serialized string to array' {
+ $serialized = ';id=3;id=4;id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Matrix style serialized string to a primitive value' {
+ $serialized = ';id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $result | Should -be 5
+ }
+
+ It 'Convert Matrix style serialized string to hashtable' {
+ $serialized = ';id=role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Matrix style serialized string to array' {
+ $serialized = ';id=3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix
+ $result | Should -be @('3', '4', '5')
+ }
+ }
+
+ Describe 'Query Parameters' {
+ It 'Convert Form(Explode) style serialized string to a primitive value' {
+ $serialized = '?id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode
+ $result | Should -be 5
+ }
+
+ It 'Convert Form(Explode) style serialized string to hashtable' {
+ $serialized = '?role=admin&firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Form(Explode) style serialized string to array' {
+ $serialized = '?id=3&id=4&id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Form style serialized string to a primitive value' {
+ $serialized = '?id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $result | Should -be 5
+ }
+
+ It 'Convert Form style serialized string to hashtable' {
+ $serialized = '?id=role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert Form style serialized string to array' {
+ $serialized = '?id=3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $result | Should -be @('3', '4', '5')
+ }
+
+
+ It 'Convert SpaceDelimited(Explode) style serialized string to array' {
+ $serialized = '?id=3&id=4&id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert SpaceDelimited style serialized string to array' {
+ $serialized = '?id=3 4 5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited
+ $result | Should -be @('3', '4', '5')
+ }
+
+
+ It 'Convert pipeDelimited(Explode) style serialized string to array' {
+ $serialized = '?id=3&id=4&id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style pipeDelimited -Explode
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert pipeDelimited style serialized string to array' {
+ $serialized = '?id=3|4|5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style pipeDelimited
+ $result | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert DeepObject(Explode) style serialized string to hashtable' {
+ $serialized = '?id[role]=admin&id[firstName]=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ # Ensure both hashtables have the same number of keys
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+
+ # Compare values for each key
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Convert DeepObject(Explode) style nested object serialized to hashtable' {
+ $serialized = '?id[role][type]=admin&id[role][level]=high&id[firstName]=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject
+ $expected = @{
+ role = @{
+ type = 'admin'
+ level = 'high'
+ }
+ firstName = 'Alex'
+ }
+ $result['role'].GetEnumerator() | ForEach-Object {
+ $expected['role'][$_.Key] | Should -Be $_.Value
+ }
+ $result['firstName'] | Should -Be $expected['firstName']
+ }
+
+ }
+
+
+ Describe 'Header Parameters' {
+ It 'Convert Simple(Explode) style serialized string to a primitive value' {
+ $serialized = 'X-MyHeader: 5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $result['X-MyHeader'] | Should -be 5
+ }
+
+ It 'Convert Simple(Explode) style serialized string to hashtable' {
+ $serialized = 'X-MyHeader: role=admin,firstName=Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ $result['X-MyHeader'].GetEnumerator() | ForEach-Object {
+ $expected[$_.Key] | Should -Be $_.Value
+ }
+ }
+
+ It 'Convert Simple(Explode) style serialized string to array' {
+ $serialized = 'X-MyHeader: 3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $result['X-MyHeader'] | Should -be @('3', '4', '5')
+ }
+
+ It 'Convert Simple style serialized string to a primitive value' {
+ $serialized = 'X-MyHeader: 5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $result['X-MyHeader'] | Should -be 5
+ }
+
+ It 'Convert Simple style serialized string to hashtable' {
+ $serialized = 'X-MyHeader: role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ $result['X-MyHeader'].GetEnumerator() | ForEach-Object {
+ $expected[$_.Key] | Should -Be $_.Value
+ }
+ }
+
+ It 'Convert Simple style serialized string to array' {
+ $serialized = 'X-MyHeader: 3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $result['X-MyHeader'] | Should -be @('3', '4', '5')
+ }
+ }
+
+ Describe 'Cookie Parameters' {
+ It 'Convert Form(Explode) style serialized string to a primitive value' {
+ $serialized = 'Cookie: id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode
+ $result['Cookie'] | Should -be 5
+ }
+
+ It 'Convert Form style serialized string to a primitive value' {
+ $serialized = 'Cookie: id=5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $result['Cookie'] | Should -be 5
+ }
+
+ It 'Convert Form style serialized string to hashtable' {
+ $serialized = 'Cookie: id=role,admin,firstName,Alex'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $expected = @{
+ role = 'admin'
+ firstName = 'Alex'
+ }
+ $result['Cookie'].GetEnumerator() | ForEach-Object {
+ $expected[$_.Key] | Should -Be $_.Value
+ }
+ }
+
+ It 'Convert Form style serialized string to array' {
+ $serialized = 'Cookie: id=3,4,5'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form
+ $result['Cookie'] | Should -be @('3', '4', '5')
+ }
+ }
+
+ Describe 'Edge cases' {
+
+ It 'Throws an error for invalid serialization style' {
+ $serialized = 'some data'
+ { ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style 'InvalidStyle' } | Should -Throw
+ }
+
+ It 'Properly decodes URL-encoded characters' {
+ $serialized = 'name%3DJohn%20Doe%2Cage%3D30'
+
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode -UrlDecode
+
+ # Define the expected hashtable
+ $expected = @{
+ 'name' = 'John Doe'
+ 'age' = '30'
+ }
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+
+ It 'Handles special characters in keys and values' {
+ $serialized = 'na!me=Jo@hn,do#e=30$'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode
+ $expected = @{
+ 'na!me' = 'Jo@hn'
+ 'do#e' = '30$'
+ }
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Parses deeply nested structures in DeepObject style' {
+ $serialized = '?user[address][street]=Main St&user[address][city]=Anytown&user[details][age]=30'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style DeepObject -ParameterName 'user'
+ $expected = @{
+ 'address' = @{
+ 'street' = 'Main St'
+ 'city' = 'Anytown'
+ }
+ 'details' = @{
+ 'age' = '30'
+ }
+ }
+ # Recursive comparison function
+ function Compare-Hashtable($expected, $actual) {
+ $expected.Keys.Count | Should -Be $actual.Keys.Count
+ foreach ($key in $expected.Keys) {
+ $actual.ContainsKey($key) | Should -BeTrue -Because "Key '$key' is missing."
+ if ($expected[$key] -is [hashtable]) {
+ Compare-Hashtable $expected[$key] $actual[$key]
+ }
+ else {
+ $actual[$key] | Should -Be $expected[$key]
+ }
+ }
+ }
+ Compare-Hashtable $expected $result
+ }
+
+
+ It 'Handles multiple occurrences of the same parameter in Query style' {
+ $serialized = '?id=1&id=2&id=3'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode -ParameterName 'id'
+ $result | Should -Be @('1', '2', '3')
+ }
+
+ It 'Handles single value in SpaceDelimited style without wrapping in an array' {
+ $serialized = '?id=42'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style SpaceDelimited -ParameterName 'id'
+ $result | Should -Be '42'
+ }
+
+ It 'Parses Matrix style without explode correctly' {
+ $serialized = ';id=1,2,3'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Matrix -ParameterName 'id'
+ $result | Should -Be @('1', '2', '3')
+ }
+ It 'Handles missing dot prefix in Label style gracefully' {
+ $serialized = 'name=value'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Label -Explode
+ $expected = @{ 'name' = 'value' }
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ It 'Parses headers with multiple values correctly' {
+ $serialized = 'X-Custom-Header: value1,value2,value3'
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple
+ $result['X-Custom-Header'] | Should -Be @('value1', 'value2', 'value3')
+ }
+
+ It 'return the SerializedString content for malformed input string' {
+ $serialized = 'name===value'
+ ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Simple -Explode | Should -be $serialized
+ }
+
+ It 'Throws an error for unsupported characters in Style parameter' {
+ { ConvertFrom-PodeSerializedString -SerializedInput 'data' -Style 'S!mple' } | Should -Throw
+ }
+
+ It 'Parses complex real-world query strings correctly' {
+ $serialized = '?filter=name%20eq%20%27John%27&sort=asc&limit=10'
+
+ $result = ConvertFrom-PodeSerializedString -SerializedInput $serialized -Style Form -Explode -UrlDecode
+
+ $expected = @{
+ 'filter' = "name eq 'John'"
+ 'sort' = 'asc'
+ 'limit' = '10'
+ }
+ $result.Keys.Count | Should -Be $expected.Keys.Count
+ foreach ($key in $expected.Keys) {
+ $result[$key] | Should -Be $expected[$key]
+ }
+ }
+
+ }
+
+}
+
+
+
+Describe 'ConvertTo-PodeSerializedString' {
+
+ BeforeAll {
+ function SortSerializedString {
+ param (
+ [string] $SerializedString,
+ [string] $Delimiter,
+ [switch] $GroupPairs,
+ [string] $SkipHead = '',
+ [string] $RemovePattern = ''
+ )
+
+ # If a head to skip is specified, separate it from the rest of the string
+ if ($SkipHead -and $SerializedString.StartsWith($SkipHead)) {
+ # Extract the head and the rest of the string
+ $head = $SkipHead
+ $SerializedString = $SerializedString.Substring($SkipHead.Length)
+ }
+ else {
+ $head = ''
+ }
+
+ # Split the remaining string into individual elements
+ $elements = $SerializedString -split $Delimiter
+
+ # Apply pattern removal if specified
+ if ($RemovePattern) {
+ $elements = $elements.ForEach({
+ $_ -replace $RemovePattern, ''
+ })
+ }
+
+ if ($GroupPairs) {
+ # Group elements into pairs (key-value)
+ $pairs = for ($i = 0; $i -lt $elements.Count; $i += 2) {
+ # Check if the next element exists to avoid a trailing delimiter
+ if ($i + 1 -lt $elements.Count) {
+ "$($elements[$i])$Delimiter$($elements[$i + 1])"
+ }
+ else {
+ # If the last element doesn't have a pair, add it as is
+ $elements[$i]
+ }
+ }
+
+ # Sort the pairs
+ $sortedPairs = $pairs | Sort-Object
+
+ # Join sorted pairs back into a single string
+ $sortedString = $sortedPairs -join $Delimiter
+ }
+ else {
+ # Sort elements individually without grouping into pairs
+ $sortedElements = $elements | Sort-Object
+
+ # Join sorted elements back into a single string
+ $sortedString = $sortedElements -join $Delimiter
+ }
+
+ # Reattach the head (if any) at the start of the sorted string
+ $result = "$head$sortedString"
+
+ # Remove any trailing delimiter that may have been inadvertently added
+ if ($result.EndsWith($Delimiter)) {
+ $result = $result.Substring(0, $result.Length - 1)
+ }
+
+ return $result
+ }
+ }
+ It 'should convert hashtable to Simple style serialized string' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple'
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs
+ $expected = 'name,value,number,10,anotherName,anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Simple style serialized string with Explode' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple' -Explode
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ','
+ $expected = 'name=value,number=10,anotherName=anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ','
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Label style serialized string' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label'
+
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.'
+ $expected = '.anotherName,anotherValue,number,10,name,value'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.'
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Label style serialized string with Explode' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Label' -Explode
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -SkipHead '.'
+ $expected = '.anotherName=anotherValue,number=10,name=value'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -SkipHead '.'
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Matrix style serialized string' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Matrix'
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs -SkipHead ';id='
+ $expected = ';id=name,value,number,10,anotherName,anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead ';id='
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Matrix style serialized string with Explode' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Matrix' -Explode
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ';' -SkipHead ';'
+ $expected = ';name=value;number=10;anotherName=anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ';' -SkipHead ';'
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Form style serialized string' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form'
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter ',' -GroupPairs -SkipHead '?id='
+ $expected = '?id=name,value,number,10,anotherName,anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter ',' -GroupPairs -SkipHead '?id='
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert hashtable to Form style serialized string with Explode' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter '&' -SkipHead '?'
+ $expected = '?name=value&number=10&anotherName=anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter '&' -SkipHead '?'
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+
+
+ It 'should convert hashtable to DeepObject style serialized string' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'DeepObject' -Explode
+ $sortedResult = SortSerializedString -SerializedInput $result -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]'
+ $expected = '?id[name]=value&id[number]=10&id[anotherName]=anotherValue'
+ $sortedExpected = SortSerializedString -SerializedInput $expected -Delimiter '&' -SkipHead '?' -RemovePattern 'id\[|\]'
+ $sortedResult | Should -Be $sortedExpected
+ }
+
+ It 'should convert array to Simple style serialized string' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Simple'
+ $result | Should -Be '3,4,5'
+ }
+
+ It 'should convert array to Simple style serialized string with Explode' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Simple' -Explode
+ $result | Should -Be '3,4,5'
+ }
+
+ It 'should convert array to Label style serialized string' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Label'
+ $result | Should -Be '.3,4,5'
+ }
+
+ It 'should convert array to Label style serialized string with Explode' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Label' -Explode
+ $result | Should -Be '.3,4,5'
+ }
+
+ It 'should convert array to Matrix style serialized string' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix'
+ $result | Should -Be ';id=3,4,5'
+ }
+
+ It 'should convert array to Matrix style serialized string with Explode' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -Explode
+ $result | Should -Be ';id=3;id=4;id=5'
+ }
+
+ It 'should convert array to SpaceDelimited style serialized string' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'SpaceDelimited'
+ $result | Should -Be '?id=3%204%205'
+ }
+
+ It 'should convert array to SpaceDelimited style serialized string with Explode' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'SpaceDelimited' -Explode
+ $result | Should -Be '?id=3&id=4&id=5'
+ }
+
+ It 'should convert array to PipeDelimited style serialized string' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'PipeDelimited'
+ $result | Should -Be '?id=3%7C4%7C5'
+ }
+
+ It 'should convert array to PipeDelimited style serialized string with Explode' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'PipeDelimited' -Explode
+ $result | Should -Be '?id=3&id=4&id=5'
+ }
+
+
+ It 'should throw an error for unsupported serialization style' {
+ $hashtable = @{
+ name = 'value'
+ anotherName = 'anotherValue'
+ number = 10
+ }
+ { ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Unsupported' } | Should -Throw
+ }
+
+
+ It 'should convert array to Matrix style without URL encoding' {
+ $array = @('value one', 'value/two', 'value&three')
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -NoUrlEncode
+ $result | Should -Be ';id=value one,value/two,value&three'
+ }
+
+ It 'should handle special characters with URL encoding' {
+ $array = @('value one', 'value/two', 'value&three')
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix'
+ $result | Should -Be ';id=value%20one,value%2Ftwo,value%26three'
+ }
+
+ It 'should handle empty array input' {
+ $array = @()
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Simple'
+ $result | Should -Be ''
+ }
+
+ It 'should handle empty hashtable input by returning an empty string' {
+ $hashtable = @{}
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Simple'
+ $result | Should -Be ''
+ }
+
+ It 'should use custom parameter name' {
+ $array = @(3, 4, 5)
+ $result = $array | ConvertTo-PodeSerializedString -Style 'Matrix' -ParameterName 'customId'
+ $result | Should -Be ';customId=3,4,5'
+ }
+
+ It 'should correctly serialize single-element array' {
+ $array = @('singleValue')
+ $result = ConvertTo-PodeSerializedString -InputObject $array -Style 'Simple'
+ $result | Should -Be 'singleValue'
+ }
+
+ It 'should correctly serialize single-entry hashtable' {
+ $hashtable = @{ key = 'value' }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode
+ $result | Should -Be '?key=value'
+ }
+
+ It 'should URL-encode special characters in keys and values' {
+ $hashtable = @{
+ 'name with spaces' = 'value/with/special&chars'
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode
+ $expected = '?name%20with%20spaces=value%2Fwith%2Fspecial%26chars'
+ $result | Should -Be $expected
+ }
+ It 'should not URL-encode when NoUrlEncode switch is used' {
+ $hashtable = @{
+ 'name with spaces' = 'value/with/special&chars'
+ }
+ $result = ConvertTo-PodeSerializedString -InputObject $hashtable -Style 'Form' -Explode -NoUrlEncode
+ $expected = '?name with spaces=value/with/special&chars'
+ $result | Should -Be $expected
+ }
+
+ It 'should use custom ParameterName in serialization' {
+ $array = @(1, 2, 3)
+ $result = ConvertTo-PodeSerializedString -InputObject $array -Style 'Matrix' -ParameterName 'customParam'
+ $result | Should -Be ';customParam=1,2,3'
+ }
+
+
+}
+
+
+Describe 'Get-PodePathParameter' {
+ BeforeEach {
+ # Mock the $WebEvent variable
+ $Script:WebEvent = [PSCustomObject]@{
+ Parameters = @{ 'action' = 'create' }
+ }
+ }
+
+ It 'should return the specified parameter value from the web event' {
+ # Call the function
+ $result = Get-PodePathParameter -Name 'action'
+
+ # Assert the result
+ $result | Should -Be 'create'
+ }
+}
+
+
+Describe 'Get-PodeQueryParameter' {
+ BeforeEach {
+ # Mock the $WebEvent variable
+ $Script:WebEvent = [PSCustomObject]@{
+ Query = @{ 'userId' = '12345' }
+ }
+ }
+
+ It 'should return the specified query parameter value from the web event' {
+ # Call the function
+ $result = Get-PodeQueryParameter -Name 'userId'
+
+ # Assert the result
+ $result | Should -Be '12345'
+ }
+}
+
+
+Describe 'Get-PodeBodyData' {
+ BeforeEach {
+ # Mock the $WebEvent variable
+ $Script:WebEvent = [PSCustomObject]@{
+ Data = 'This is the body data'
+ }
+ }
+
+ It 'should return the body data of the web event' {
+ # Call the function
+ $result = Get-PodeBodyData
+
+ # Assert the result
+ $result | Should -Be 'This is the body data'
+ }
+}
\ No newline at end of file
From 02ba1bd412919e3158672b25e0b170c64d069cde Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:32:32 -0800
Subject: [PATCH 05/12] Squashed commit of the following:
commit 7cbeabca0e71c1480520587d4a3eaa5744e3ecf5
Author: mdaneri
Date: Sun Mar 2 09:16:22 2025 -0800
Fix PSObject to hashtable conversion
commit 11ced8e2898cd7cafb91a04aca2c847ca15a28c2
Author: mdaneri
Date: Sun Mar 2 08:35:38 2025 -0800
fix tests
commit d15f3995ac5a0486dd2c587311f8864a2b555aaf
Author: mdaneri
Date: Sat Mar 1 17:56:58 2025 -0800
simplify merge
commit a662515a692bd8870888ca89e096fb23df0d19f8
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sat Mar 1 23:36:31 2025 +0000
fix helper
commit 77a56a1f045243d243b653dee8ec18ae37a49a46
Merge: e40e0d9d 67505f56
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sun Feb 23 07:30:37 2025 -0800
Merge branch 'develop' into threadsafe-state
commit e40e0d9dfcf8f95dee253333b3ffd795350864a9
Merge: a3de2b35 cbdc62fe
Author: mdaneri
Date: Sat Feb 22 09:18:33 2025 -0800
Merge remote-tracking branch 'upstream/develop' into threadsafe-state
commit a3de2b35ae8646a7f98c02299f8c48feccc11959
Merge: f073f50f fbf6ecfb
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sat Feb 22 06:31:37 2025 -0800
Merge branch 'develop' into threadsafe-state
commit f073f50f0dcb569e197f5fd862b78a2467ea6942
Merge: a1beca34 a76741bc
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sun Feb 16 06:15:51 2025 -0800
Merge branch 'develop' into threadsafe-state
commit a1beca34859a2c817a3761b6ee9594d80ce543b7
Merge: be8f4070 a236a1a0
Author: mdaneri
Date: Tue Feb 11 18:49:41 2025 -0800
Merge remote-tracking branch 'upstream/develop' into threadsafe-state
commit be8f40702f0a6b20e8d1738ae359feed22c08645
Merge: b7b085cf 75e29626
Author: mdaneri
Date: Sun Feb 9 07:22:47 2025 -0800
Merge remote-tracking branch 'upstream/develop' into threadsafe-state
commit b7b085cf8ac8d999b5987b00bb068354b08010fd
Merge: a8931271 6b23fc33
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Wed Feb 5 13:13:11 2025 -0800
Merge branch 'develop' into threadsafe-state
commit a89312714674f0e9da96ce6e7ebfab7a63e24dea
Author: mdaneri
Date: Tue Feb 4 08:06:04 2025 -0800
Get-PodeUtcNow
commit 18dcdd61f32379c19381a2c2bb340e556d831ce6
Author: mdaneri
Date: Mon Feb 3 18:30:32 2025 -0800
Update Convert.ps1
commit f2d70fa9579480620d7823ec532fc8ead45b0220
Author: mdaneri
Date: Mon Feb 3 18:13:20 2025 -0800
Update State.Tests.ps1
commit aae3d5d05e9dace30110d1db272f1549581e34d7
Author: mdaneri
Date: Mon Feb 3 18:08:02 2025 -0800
fix tests
commit 8f50237d4fef081cf8fcf438f9da2d3b073170d3
Author: mdaneri
Date: Mon Feb 3 17:39:06 2025 -0800
improvement
commit 8594cc57bedd1b3f67848d6947308a3d88ec2d64
Author: mdaneri
Date: Mon Feb 3 17:30:51 2025 -0800
remove Get-PodeApplicationName
commit fc41e3c5303289156d2885fe5086861cf2d5d18f
Author: mdaneri
Date: Mon Feb 3 17:25:56 2025 -0800
Add translations
commit 81802c1705664eb2c09c5bae589224bf6ee31dd7
Author: mdaneri
Date: Mon Feb 3 16:38:32 2025 -0800
fix compatibility with older versions
commit c634e0a2b338938a675e16dde6ca4129f098e795
Author: mdaneri
Date: Mon Feb 3 16:22:17 2025 -0800
Delete System.Collections.Hashtable
commit 556ef81b8e95453de20d5b6ef63b432eb557d9cf
Author: mdaneri
Date: Mon Feb 3 16:20:43 2025 -0800
update
commit aab9fa57b8bebec982d04c4c2171957bbc7dece2
Author: mdaneri
Date: Mon Feb 3 09:12:49 2025 -0800
Update SharedState.md
commit 37226cf7c41317e86691c99fca05ed4ddc4d87a1
Author: mdaneri
Date: Mon Feb 3 08:48:14 2025 -0800
fixes and doc update
commit 1ad233461dee01f9e04379c2489212d202fc969d
Author: mdaneri
Date: Sun Feb 2 17:09:38 2025 -0800
fixes
commit 0e0694c8f4aed712cd40369d3bef38a5ea6730b9
Author: mdaneri
Date: Sun Feb 2 16:05:51 2025 -0800
final
commit afaabe9f290a6ba02f810e3e1400048091a0748f
Author: mdaneri
Date: Sun Feb 2 10:13:58 2025 -0800
working version
commit 2e55aac25ceb1d21414339e1d3c6a1241f3e0d63
Author: mdaneri
Date: Sun Feb 2 08:14:31 2025 -0800
first drop
---
.gitignore | 2 +
docs/Tutorials/SharedState.md | 174 ++++++-----
examples/Shared-State.ps1 | 32 +-
examples/Shared-ThreadSafeState.ps1 | 138 +++++++++
examples/ThreadSafeState.json | 0
examples/test.json | 1 +
src/Locales/ar/Pode.psd1 | 5 +
src/Locales/de/Pode.psd1 | 5 +
src/Locales/en-us/Pode.psd1 | 5 +
src/Locales/en/Pode.psd1 | 5 +
src/Locales/es/Pode.psd1 | 5 +
src/Locales/fr/Pode.psd1 | 5 +
src/Locales/it/Pode.psd1 | 5 +
src/Locales/ja/Pode.psd1 | 5 +
src/Locales/ko/Pode.psd1 | 5 +
src/Locales/nl/Pode.psd1 | 5 +
src/Locales/pl/Pode.psd1 | 5 +
src/Locales/pt/Pode.psd1 | 5 +
src/Locales/zh/Pode.psd1 | 5 +
src/Private/Context.ps1 | 3 +-
src/Private/Convert.ps1 | 455 ++++++++++++++++++++++++++++
src/Private/Helpers.ps1 | 73 ++++-
src/Private/ScopedVariables.ps1 | 2 +-
src/Public/Core.ps1 | 2 +-
src/Public/State.ps1 | 338 +++++++++++++--------
src/Public/Utilities.ps1 | 16 +
tests/shared/TestHelper.ps1 | 142 +++++++++
tests/unit/Convert.Tests.ps1 | 130 ++++++++
tests/unit/State.Tests.ps1 | 123 +++++++-
29 files changed, 1470 insertions(+), 226 deletions(-)
create mode 100644 examples/Shared-ThreadSafeState.ps1
create mode 100644 examples/ThreadSafeState.json
create mode 100644 examples/test.json
create mode 100644 src/Private/Convert.ps1
create mode 100644 tests/unit/Convert.Tests.ps1
diff --git a/.gitignore b/.gitignore
index 6938c5f15..99de26566 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ docs/[Ff]unctions/
examples/state.json
examples/issue-*
examples/issues/
+examples/State/
pkg/
deliverable/
.vs/
@@ -269,3 +270,4 @@ docs/Getting-Started/Samples.md
# Dump Folder
Dump
+
diff --git a/docs/Tutorials/SharedState.md b/docs/Tutorials/SharedState.md
index b34ba137e..b596c2721 100644
--- a/docs/Tutorials/SharedState.md
+++ b/docs/Tutorials/SharedState.md
@@ -1,79 +1,122 @@
# Shared State
-Most things in Pode run in isolated runspaces: routes, middleware, schedules - to name a few. This means you can't create a variable in a timer, and then access that variable in a route. To overcome this limitation you can use the Shared State feature within Pode, which allows you to set/get variables on a state shared between all runspaces. This lets you can create a variable in a timer and store it within the shared state; then you can retrieve the variable from the state in a route.
+Most things in Pode run in isolated runspaces: routes, middleware, schedules - to name a few. This means you can't create a variable in a timer, and then access that variable in a route. To overcome this limitation you can use the Shared State feature within Pode, which allows you to set/get variables on a state shared between all runspaces. This lets you create a variable in a timer and store it within the shared state; then you can retrieve the variable from the state in a route.
You also have the option of saving the current state to a file, and then restoring the state back on server start. This way you won't lose state between server restarts.
-You can also use the State in combination with [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject) to ensure thread safety - if needed.
+Pode supports various structures for shared state, some of which are thread-safe:
+
+**Thread-Safe Structures:**
+
+- `ConcurrentDictionary`
+- `ConcurrentBag`
+- `ConcurrentQueue`
+- `ConcurrentStack`
+
+**Non-Thread-Safe Structures (Require Locking):**
+
+- `OrderedDictionary`
+- `Hashtable`
+- `PSCustomObject`When using a thread-safe object, `Lock-PodeObject` is no longer required.
!!! tip
- It's wise to use the State in conjunction with [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject), to ensure thread safety between runspaces.
+It's wise to use the State in conjunction with [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject) when dealing with non-thread-safe objects to ensure thread safety between runspaces.
!!! warning
- If you omit the use of [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject), you might run into errors due to multi-threading. Only omit if you are *absolutely confident* you do not need locking. (ie: you set in state once and then only ever retrieve, never updating the variable).
+If you are using a non-thread-safe object, such as `Hashtable`, `OrderedDictionary`, or `PSCustomObject`, you should use [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject) to ensure thread safety between runspaces. Omitting this may lead to concurrency issues and unpredictable behavior.
## Usage
-Where possible use the same casing for the `-Name` of state keys. When using [`Restore-PodeState`](../../Functions/State/Restore-PodeState) the state will become case-sensitive due to the nature of how `ConvertFrom-Json` works.
+Where possible, use the same casing for the `-Name` of state keys. When using [`Restore-PodeState`](../../Functions/State/Restore-PodeState), the state will become case-sensitive due to the nature of how `ConvertFrom-Json` works.
### Set
-The [`Set-PodeState`](../../Functions/State/Set-PodeState) function will create/update a variable in the state. You need to supply a name and a value to set on the state, there's also an optional scope that can be supplied - which lets you save specific state objects with a certain scope.
+#### **NewCollectionType Parameter**
+
+The `-NewCollectionType` parameter allows users to specify the type of collection to initialize within the shared state. This eliminates the need to manually instantiate collections before setting them in the state.
+
+**Supported Collection Types:**
+
+- `Hashtable`
+- `ConcurrentDictionary`
+- `OrderedDictionary`
+- `ConcurrentBag`
+- `ConcurrentQueue`
+- `ConcurrentStack`
+
+If `-NewCollectionType` is used, the specified collection type will be created and stored in the state. The `-Value` parameter is ignored when this option is used.
-An example of setting a hashtable variable in the state is as follows:
+**Examples:**
```powershell
-Start-PodeServer {
- Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock {
- Lock-PodeObject -ScriptBlock {
- Set-PodeState -Name 'data' -Value @{ 'Name' = 'Rick Sanchez' } | Out-Null
- }
- }
-}
+# Set a simple hashtable in shared state
+Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
+
+# Initialize a ConcurrentDictionary instead of providing a value
+Set-PodeState -Name 'Cache' -NewCollectionType 'ConcurrentDictionary'
+
+# Create a ConcurrentQueue for shared state management
+Set-PodeState -Name 'Tasks' -NewCollectionType 'ConcurrentQueue'
+```
+
+The [`Set-PodeState`](../../Functions/State/Set-PodeState) function will create/update a variable in the state. You need to supply a name and a value to set on the state, and there's also an optional scope that can be supplied - which lets you save specific state objects with a certain scope.
+
+!!! tip
+The .NET collections `ConcurrentDictionary` and `OrderedDictionary` are case-sensitive by default. To make them case-insensitive, initialize them as follows:
+
+```powershell
+# Case-insensitive ConcurrentDictionary
+Set-PodeState -Name 'Cache' -Value ([System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase))
+
+# Case-insensitive OrderedDictionary
+Set-PodeState -Name 'Config' -Value ([System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase))
+
+# Case-insensitive OrderedDictionary
+Set-PodeState -Name 'Running' -Value ([ordered]@{})
+```
+
+Alternatively, you can use:
+
+```powershell
+Set-PodeState -Name 'Cache' -NewCollectionType 'ConcurrentDictionary'
+Set-PodeState -Name 'Config' -NewCollectionType 'OrderedDictionary'
+Set-PodeState -Name 'Running' -NewCollectionType 'OrderedDictionary'
```
-Alternatively you could use the `$state:` variable scope to set a variable in state. This variable will be scopeless, so if you need scope then use [`Set-PodeState`](../../Functions/State/Set-PodeState). `$state:` can be used anywhere, but keep in mind that like `$session:` Pode can only remap the this in scriptblocks it's aware of; so using it in a function of a custom module won't work. Similar to the example above:
+#### Example: Non-Thread-Safe Objects
+
+If using a non-thread-safe object, such as `Hashtable`, `OrderedDictionary`, or `PSCustomObject`, wrap access to the state in `Lock-PodeObject` to prevent concurrency issues.
```powershell
Start-PodeServer {
Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock {
Lock-PodeObject -ScriptBlock {
- $state:data = @{ 'Name' = 'Rick Sanchez' }
+ Set-PodeState -Name 'data' -Value @{ 'Name' = 'Rick Sanchez' } | Out-Null
}
}
}
```
-### Get
+The [`Set-PodeState`](../../Functions/State/Set-PodeState) function will create/update a variable in the state. You need to supply a name and a value to set on the state, and there's also an optional scope that can be supplied - which lets you save specific state objects with a certain scope.
-The [`Get-PodeState`](../../Functions/State/Get-PodeState) function will return the value currently stored in the state for a variable. If the variable doesn't exist then `$null` is returned.
-
-An example of retrieving a value from the state is as follows:
+An example of setting a `ConcurrentDictionary` variable in the state is as follows:
```powershell
Start-PodeServer {
Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock {
- $value = $null
-
- Lock-PodeObject -ScriptBlock {
- $value = (Get-PodeState -Name 'data')
- }
-
- # do something with $value
+ Set-PodeState -Name 'data' -Value ([System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()) | Out-Null
}
}
```
-Alternatively you could use the `$state:` variable scope to get a variable in state. `$state:` can be used anywhere, but keep in mind that like `$session:` Pode can only remap the this in scriptblocks it's aware of; so using it in a function of a custom module won't work. Similar to the example above:
+### Get
+
+The [`Get-PodeState`](../../Functions/State/Get-PodeState) function will return the value currently stored in the state for a variable. If the variable doesn't exist, `$null` is returned.
```powershell
Start-PodeServer {
Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock {
- $value = $null
-
- Lock-PodeObject -ScriptBlock {
- $value = $state:data
- }
+ $value = (Get-PodeState -Name 'data')
# do something with $value
}
@@ -84,45 +127,29 @@ Start-PodeServer {
The [`Remove-PodeState`](../../Functions/State/Remove-PodeState) function will remove a variable from the state. It will also return the value stored in the state before removing the variable.
-An example of removing a variable from the state is as follows:
-
```powershell
Start-PodeServer {
Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock {
- Lock-PodeObject -ScriptBlock {
- Remove-PodeState -Name 'data' | Out-Null
- }
+ Remove-PodeState -Name 'data' | Out-Null
}
}
```
### Save
-The [`Save-PodeState`](../../Functions/State/Save-PodeState) function will save the current state, as JSON, to the specified file. The file path can either be relative, or literal. When saving the state, it's recommended to wrap the function within [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject).
-
-An example of saving the current state every hour is as follows:
+The [`Save-PodeState`](../../Functions/State/Save-PodeState) function will save the current state, as JSON, to the specified file. The file path can either be relative or literal. When saving the state, it's recommended to wrap the function within [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject) if dealing with non-thread-safe objects.
```powershell
Start-PodeServer {
Add-PodeSchedule -Name 'save-state' -Cron '@hourly' -ScriptBlock {
- Lock-PodeObject -ScriptBlock {
- Save-PodeState -Path './state.json'
- }
+ Save-PodeState -Path './state.json'
}
}
```
-When saving the state, you can also use the `-Exclude` or `-Include` parameters to exclude/include certain state objects from being saved. Saving also has a `-Scope` parameter, which allows you so save only state objects created with the specified scope(s).
-
-You can use all the above 3 parameter in conjunction, with `-Exclude` having the highest precedence and `-Scope` having the lowest.
-
-By default the JSON will be saved expanded, but you can saved the JSON as compressed by supplying the `-Compress` switch.
-
### Restore
-The [`Restore-PodeState`](../../Functions/State/Restore-PodeState) function will restore the current state from the specified file. The file path can either be relative, or a literal path. if you're restoring the state immediately on server start, you don't need to use [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject).
-
-An example of restore the current state on server start is as follows:
+The [`Restore-PodeState`](../../Functions/State/Restore-PodeState) function will restore the current state from the specified file. The file path can either be relative or a literal path. If you're restoring the state immediately on server start, you don't need to use [`Lock-PodeObject`](../../Functions/Threading/Lock-PodeObject).
```powershell
Start-PodeServer {
@@ -130,54 +157,37 @@ Start-PodeServer {
}
```
-By default, restoring from a state file will overwrite the current state. You can change this so the restored state is merged instead by using the `-Merge` switch. (Note: if you restore a key that already exists in state, this will still overwrite that key).
-
## Full Example
-The following is a full example of using the State functions. It is a simple Timer that creates and updates a `hashtable` variable, and then a Route is used to retrieve that variable. There is also another route that will remove the variable from the state. The state is also saved on every iteration of the timer, and restored on server start:
+The following is a full example of using the State functions. It is a simple Timer that creates and updates a `ConcurrentDictionary` variable, and then a Route is used to retrieve that variable. There is also another route that will remove the variable from the state. The state is also saved on every iteration of the timer and restored on server start:
```powershell
Start-PodeServer {
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
# create the shared variable
- Set-PodeState -Name 'hash' -Value @{ 'values' = @(); } | Out-Null
+ Set-PodeState -Name 'dict' -Value ([System.Collections.Concurrent.ConcurrentDictionary[string, int]]::new()) | Out-Null
- # attempt to re-initialise the state (will do nothing if the file doesn't exist)
+ # attempt to re-initialize the state (will do nothing if the file doesn't exist)
Restore-PodeState -Path './state.json'
# timer to add a random number to the shared state
Add-PodeTimer -Name 'forever' -Interval 2 -ScriptBlock {
- # ensure we're thread safe
- Lock-PodeObject -ScriptBlock {
- # attempt to get the hashtable from the state
- $hash = (Get-PodeState -Name 'hash')
-
- # add a random number
- $hash.values += (Get-Random -Minimum 0 -Maximum 10)
-
- # save the state to file
- Save-PodeState -Path './state.json'
- }
+ $dict = (Get-PodeState -Name 'dict')
+ $dict["random"] = (Get-Random -Minimum 0 -Maximum 10)
+ Save-PodeState -Path './state.json'
}
- # route to return the value of the hashtable from shared state
+ # route to return the value of the dictionary from shared state
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
- # again, ensure we're thread safe
- Lock-PodeObject -ScriptBlock {
- # get the hashtable from the state and return it
- $hash = (Get-PodeState -Name 'hash')
- Write-PodeJsonResponse -Value $hash
- }
+ $dict = (Get-PodeState -Name 'dict')
+ Write-PodeJsonResponse -Value $dict
}
- # route to remove the hashtable from shared state
+ # route to remove the dictionary from shared state
Add-PodeRoute -Method Delete -Path '/' -ScriptBlock {
- # ensure we're thread safe
- Lock-PodeObject -ScriptBlock {
- # remove the hashtable from the state
- Remove-PodeState -Name 'hash' | Out-Null
- }
+ Remove-PodeState -Name 'dict' | Out-Null
}
}
```
+
diff --git a/examples/Shared-State.ps1 b/examples/Shared-State.ps1
index a62f0e687..5f73c3b5d 100644
--- a/examples/Shared-State.ps1
+++ b/examples/Shared-State.ps1
@@ -44,17 +44,34 @@ Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+ $Path = Join-Path (Get-PodeServerPath) "State"
+ if (!(Test-Path -Path $Path -PathType Container)) {
+ New-Item -Path $Path -ItemType Directory
+
+ }
+ $stateScope1Path = Join-Path -Path $Path -ChildPath 'LegacyStateScope1.json'
+ #if no previous state exist create a new one old Pode style
+ if (!( Test-Path $stateScope1Path)) {
+ @{
+ 'hash1' = @{
+ 'Scope' = @('Scope0', 'Scope1')
+ 'Value' = @{
+ 'values' = @(4, 4, 8, 0, 5, 5, 1, 9, 1, 1, 2, 4, 9, 2, 5)
+ }
+ }
+ } | ConvertTo-Json -Depth 10 | Out-File $stateScope1Path
+ }
# re-initialise the state
- Restore-PodeState -Path './state.json'
+ Restore-PodeState -Path $stateScope1Path
# initialise if there was no file
- if ($null -eq ($hash = (Get-PodeState -Name 'hash1'))) {
+ if (!(Test-PodeState -Name 'hash1')) {
$hash = Set-PodeState -Name 'hash1' -Value @{} -Scope Scope0, Scope1
$hash['values'] = @()
}
- if ($null -eq ($hash = (Get-PodeState -Name 'hash2'))) {
+ if (!(Test-PodeState -Name 'hash2')) {
$hash = Set-PodeState -Name 'hash2' -Value @{} -Scope Scope0, Scope2
$hash['values'] = @()
}
@@ -63,14 +80,17 @@ Start-PodeServer {
$state:hash3 = @{ values = @() }
}
+ Save-PodeState -Path $stateScope1Path -Scope Scope1
+
# create timer to update a hashtable and make it globally accessible
- Add-PodeTimer -Name 'forever' -Interval 2 -ScriptBlock {
+ Add-PodeTimer -Name 'forever' -Interval 2 -ArgumentList $stateScope1Path -ScriptBlock {
+ param([string]$stateScope1Path)
$hash = $null
Lock-PodeObject -ScriptBlock {
$hash = (Get-PodeState -Name 'hash1')
$hash.values += (Get-Random -Minimum 0 -Maximum 10)
- Save-PodeState -Path './state.json' -Scope Scope1 #-Exclude 'hash1'
+ Save-PodeState -Path $stateScope1Path -Scope Scope1 #-Exclude 'hash1'
}
Lock-PodeObject -ScriptBlock {
@@ -95,7 +115,7 @@ Start-PodeServer {
# route to remove the hashtable from global state
Add-PodeRoute -Method Delete -Path '/array' -ScriptBlock {
Lock-PodeObject -ScriptBlock {
- $hash = (Set-PodeState -Name 'hash1' -Value @{})
+ $hash = (Set-PodeState -Name 'hash1' -Value @{} -Scope Scope0, Scope1)
$hash.values = @()
}
}
diff --git a/examples/Shared-ThreadSafeState.ps1 b/examples/Shared-ThreadSafeState.ps1
new file mode 100644
index 000000000..923bec1ea
--- /dev/null
+++ b/examples/Shared-ThreadSafeState.ps1
@@ -0,0 +1,138 @@
+<#
+.SYNOPSIS
+ A sample PowerShell script to set up a Pode server with thread-safe state management and logging.
+
+.DESCRIPTION
+ This script sets up a Pode server that listens on port 8081, logs requests and errors to the terminal, and manages state using thread-safe collections such as `ConcurrentDictionary` and `ConcurrentBag`. The server initializes state from a JSON file, updates state periodically using timers, and provides routes to interact with the state.
+
+.EXAMPLE
+ To run the sample: ./Shared-ThreadSafeState.ps1
+
+ Invoke-RestMethod -Uri http://localhost:8081/array -Method Get
+ Invoke-RestMethod -Uri http://localhost:8081/array3 -Method Get
+ Invoke-RestMethod -Uri http://localhost:8081/array -Method Delete
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Shared-ThreadSafeState.ps1
+
+.NOTES
+ Author: Pode Team
+ License: MIT License
+ This script uses `ConcurrentDictionary` and `ConcurrentBag` to ensure thread-safe state handling in a multi-threaded Pode environment.
+#>
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a basic server
+Start-PodeServer {
+
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+ New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+ $Path = Get-PodeRelativePath -Path './State' -JoinRoot -Resolve
+
+ if (!(Test-Path -Path $Path -PathType Container)) {
+ New-Item -Path $Path -ItemType Directory
+
+ }
+ $stateScope1Path = Join-Path -Path $Path -ChildPath 'ThreadSafeStateScope1.json'
+ $stateScope2Path = Join-Path -Path $Path -ChildPath 'ThreadSafeStateScope2.json'
+ $stateScope0Path = Join-Path -Path $Path -ChildPath 'ThreadSafeStateScope0.json'
+ $stateNoScopePath = Join-Path -Path $Path -ChildPath 'ThreadSafeStateNoScope.json'
+ # re-initialise the state
+ Restore-PodeState -Path $stateScope1Path
+ Restore-PodeState -Path $stateScope2Path -Merge
+ Save-PodeState -Path $stateNoScopePath
+ # initialise if there was no file
+ if (!(Test-PodeState -Name 'hash1')) {
+ $hash = (Set-PodeState -Name 'hash1' -NewCollectionType ConcurrentDictionary -Scope Scope0, Scope1 )
+ $hash.bag = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
+ $hash.array = @()
+ $hash.psCustomSum = [PSCustomObject]@{
+ bag = 0
+ array = 0
+ }
+ $hash.string = 'Never deleted'
+ $hash.deleted = 0
+ # Assign a custom PsTypeName
+ $hash.psCustomSum.PSTypeNames.Insert(0, 'Pode.StateSum')
+ }
+
+ if (!(Test-PodeState -Name 'hash2')) {
+ $hash = Set-PodeState -Name 'hash2' -NewCollectionType Hashtable -Scope Scope0, Scope2
+ $hash['values'] = @()
+ }
+
+ if ($null -eq $state:hash3) {
+ $state:hash3 = @{ values = @() }
+ }
+
+ # create timer to update a hashtable and make it globally accessible
+ Add-PodeTimer -Name 'forever' -Interval 2 -ArgumentList $stateScope1Path, $stateScope2Path, $stateScope0Path, $stateNoScopePath -ScriptBlock {
+ param([string]$stateScope1Path, [string]$stateScope2Path, [string]$stateScope0Path, [string]$stateNoScopePath)
+ $hash = $null
+
+ $hash = (Get-PodeState -Name 'hash1')
+ $hash.bag.add((Get-Random -Minimum 0 -Maximum 10))
+ $hash.array += (Get-Random -Minimum 0 -Maximum 10)
+
+ $hash.psCustomSum.bag = $hash.bag.ToArray() | Measure-Object -Sum | Select-Object -ExpandProperty Sum
+ $hash.psCustomSum.array = $hash.array | Measure-Object -Sum | Select-Object -ExpandProperty Sum
+
+
+ $state:hash3.values += (Get-Random -Minimum 0 -Maximum 10)
+
+ $hash2 = (Get-PodeState -Name 'hash2')
+ $hash2.values += (Get-Random -Minimum 100 -Maximum 200)
+ Save-PodeState -Path $stateScope1Path -Scope Scope1 #-Exclude 'hash2'
+ Save-PodeState -Path $stateScope2Path -Scope Scope2
+ Save-PodeState -Path $stateScope0Path -Scope Scope0
+ Save-PodeState -Path $stateNoScopePath
+ }
+
+ # route to retrieve and return the value of the hashtable from global state
+ Add-PodeRoute -Method Get -Path '/array' -ScriptBlock {
+ $hash = (Get-PodeState 'hash1')
+ Write-PodeJsonResponse -Value $hash
+ }
+
+ Add-PodeRoute -Method Get -Path '/array3' -ScriptBlock {
+ Write-PodeJsonResponse -Value $state:hash3
+ }
+
+ # route to remove the hashtable from global state
+ Add-PodeRoute -Method Delete -Path '/array' -ScriptBlock {
+ $value = (Get-PodeState -Name 'hash1' )
+ $hash = (Set-PodeState -Name 'hash1' -NewCollectionType ConcurrentDictionary -Scope Scope0, Scope1 )
+ $hash.bag = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
+ $hash.bag.add((Get-Random -Minimum 0 -Maximum 10))
+ $hash.array = @((Get-Random -Minimum 0 -Maximum 10))
+
+ $hash.psCustomSum = [PSCustomObject]@{
+ bag = $hash.bag.ToArray() | Measure-Object -Sum | Select-Object -ExpandProperty Sum
+ array = $hash.array | Measure-Object -Sum | Select-Object -ExpandProperty Sum
+ }
+ # Assign a custom PsTypeName
+ $hash.psCustomSum.PSTypeNames.Insert(0, 'Pode.StateSum')
+ $hash.deleted = $value.number + 1
+ $hash.string = "Deleted $($hash.number) times"
+
+ }
+
+}
\ No newline at end of file
diff --git a/examples/ThreadSafeState.json b/examples/ThreadSafeState.json
new file mode 100644
index 000000000..e69de29bb
diff --git a/examples/test.json b/examples/test.json
new file mode 100644
index 000000000..14484b493
--- /dev/null
+++ b/examples/test.json
@@ -0,0 +1 @@
+{"Type":"ConcurrentDictionary","Items":[{"Key":"Name","Value":{"Type":"ConcurrentDictionary","Items":[{"Key":"Value","Value":{"Type":"Hashtable","Items":[{"Key":"Name","Value":"Morty"}]}},{"Key":"Scope","Value":null}]}}]}
diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1
index d9617f469..0cdf650f9 100644
--- a/src/Locales/ar/Pode.psd1
+++ b/src/Locales/ar/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للمعدل غير موجودة: {0}'
accessLimitRuleAlreadyExistsExceptionMessage = 'تم تعريف قاعدة الحد الأقصى للوصول بالفعل: {0}'
accessLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للوصول غير موجودة: {0}'
+ invalidPodeStateFormatExceptionMessage = 'ملف PodeState "{0}" يحتوي على تنسيق غير صالح. كان متوقعًا هيكل يشبه القاموس (ConcurrentDictionary أو Hashtable أو OrderedDictionary)، ولكن تم العثور على [{1}]. يرجى التحقق من محتوى الملف أو إعادة تهيئة الحالة.'
+ unknownJsonDictionaryTypeExceptionMessage = 'نوع قاموس/مجموعة غير معروف في JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'البيانات المقدمة لا تمثل حالة Pode صالحة.'
+ podeStateVersionMismatchExceptionMessage = 'بيانات الحالة المقدمة تأتي من إصدار أحدث من Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'بيانات الحالة المقدمة تنتمي إلى تطبيق آخر: {0}.'
}
diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1
index d85929f70..6de56c63d 100644
--- a/src/Locales/de/Pode.psd1
+++ b/src/Locales/de/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "Die Rate-Limit-Regel mit dem Namen '{0}' existiert nicht."
accessLimitRuleAlreadyExistsExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert bereits."
accessLimitRuleDoesNotExistExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert nicht."
+ invalidPodeStateFormatExceptionMessage = 'Die PodeState-Datei "{0}" enthält ein ungültiges Format. Erwartet wurde eine Dictionary-ähnliche Struktur (ConcurrentDictionary, Hashtable oder OrderedDictionary), aber gefunden wurde [{1}]. Bitte überprüfen Sie den Dateiinhalt oder initialisieren Sie den Zustand neu.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Unbekannter Wörterbuch-/Sammlungstyp in JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'Die bereitgestellten Daten stellen keinen gültigen Pode-Status dar.'
+ podeStateVersionMismatchExceptionMessage = 'Die bereitgestellten Statusdaten stammen aus einer neueren Pode-Version: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'Die bereitgestellten Statusdaten gehören zu einer anderen Anwendung: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1
index c2ac6d2b0..1b9cd35da 100644
--- a/src/Locales/en-us/Pode.psd1
+++ b/src/Locales/en-us/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "A rate limit rule with the name '{0}' does not exist."
accessLimitRuleAlreadyExistsExceptionMessage = "An access limit rule with the name '{0}' already exists."
accessLimitRuleDoesNotExistExceptionMessage = "An access limit rule with the name '{0}' does not exist."
+ invalidPodeStateFormatExceptionMessage = 'The PodeState file "{0}" contains an invalid format. Expected a dictionary-like structure (ConcurrentDictionary, Hashtable, or OrderedDictionary), but found [{1}]. Please verify the file content or reinitialize the state.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Unknown dictionary/collection type in JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'The provided data does not represent a valid Pode state.'
+ podeStateVersionMismatchExceptionMessage = 'The provided state data originates from a newer Pode version: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'The provided state data belongs to a different application: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1
index fc04db0d2..58bfcb91d 100644
--- a/src/Locales/en/Pode.psd1
+++ b/src/Locales/en/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "A Rate Limit Rule with the name '{0}' does not exist."
accessLimitRuleAlreadyExistsExceptionMessage = "An Access Limit Rule with the name '{0}' already exists."
accessLimitRuleDoesNotExistExceptionMessage = "An Access Limit Rule with the name '{0}' does not exist."
+ invalidPodeStateFormatExceptionMessage = 'The PodeState file "{0}" contains an invalid format. Expected a dictionary-like structure (ConcurrentDictionary, Hashtable, or OrderedDictionary), but found [{1}]. Please verify the file content or reinitialize the state.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Unknown dictionary/collection type in JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'The provided data does not represent a valid Pode state.'
+ podeStateVersionMismatchExceptionMessage = 'The provided state data originates from a newer Pode version: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'The provided state data belongs to a different application: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1
index d7db64e6c..ea7531bc8 100644
--- a/src/Locales/es/Pode.psd1
+++ b/src/Locales/es/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "La regla de límite de velocidad con el nombre '{0}' no existe."
accessLimitRuleAlreadyExistsExceptionMessage = "La regla de límite de acceso con el nombre '{0}' ya existe."
accessLimitRuleDoesNotExistExceptionMessage = "La regla de límite de acceso con el nombre '{0}' no existe."
+ invalidPodeStateFormatExceptionMessage = 'El archivo PodeState "{0}" tiene un formato no válido. Se esperaba una estructura similar a un diccionario (ConcurrentDictionary, Hashtable o OrderedDictionary), pero se encontró [{1}]. Verifique el contenido del archivo o reinicialice el estado.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Tipo de diccionario/colección desconocido en JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'Los datos proporcionados no representan un estado válido de Pode.'
+ podeStateVersionMismatchExceptionMessage = 'Los datos del estado proporcionados provienen de una versión más reciente de Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'Los datos del estado proporcionados pertenecen a otra aplicación: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1
index a0d769eab..f0d704d92 100644
--- a/src/Locales/fr/Pode.psd1
+++ b/src/Locales/fr/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "La règle de limite de taux '{0}' n'existe pas."
accessLimitRuleAlreadyExistsExceptionMessage = "Une règle de limite d'accès nommée '{0}' existe déjà."
accessLimitRuleDoesNotExistExceptionMessage = "La règle de limite d'accès '{0}' n'existe pas."
+ invalidPodeStateFormatExceptionMessage = 'Il file PodeState "{0}" contiene un formato non valido. Era prevista una struttura simile a un dizionario (ConcurrentDictionary, Hashtable o OrderedDictionary), ma è stato trovato [{1}]. Verifica il contenuto del file o reinizializza lo stato.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Type de dictionnaire/collection inconnu dans le JSON : {0}'
+ invalidPodeStateDataExceptionMessage = 'Les données fournies ne représentent pas un état valide de Pode.'
+ podeStateVersionMismatchExceptionMessage = "Les données d'état fournies proviennent d'une version plus récente de Pode : {0}."
+ podeStateApplicationMismatchExceptionMessage = "Les données d'état fournies appartiennent à une autre application : { 0 }."
}
\ No newline at end of file
diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1
index 5dd3c47f4..2de825473 100644
--- a/src/Locales/it/Pode.psd1
+++ b/src/Locales/it/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione del tasso con il nome '{0}' non esiste."
accessLimitRuleAlreadyExistsExceptionMessage = "Una regola di limitazione dell'accesso con il nome '{0}' esiste già."
accessLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione dell'accesso con il nome '{0}' non esiste."
+ invalidPodeStateFormatExceptionMessage = 'Il file PodeState "{0}" contiene un formato non valido. Era prevista una struttura simile a un dizionario (ConcurrentDictionary, Hashtable o OrderedDictionary), ma è stato trovato [{1}]. Verifica il contenuto del file o reinizializza lo stato.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Tipo di dizionario/collezione sconosciuto in JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'I dati forniti non rappresentano uno stato valido di Pode.'
+ podeStateVersionMismatchExceptionMessage = 'I dati di stato forniti provengono da una versione più recente di Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = "I dati di stato forniti appartengono a un'altra applicazione: { 0 }."
}
\ No newline at end of file
diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1
index 43d8243bc..ae2bf038f 100644
--- a/src/Locales/ja/Pode.psd1
+++ b/src/Locales/ja/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のレート制限ルールは存在しません。"
accessLimitRuleAlreadyExistsExceptionMessage = "名前が '{0}' のアクセス制限ルールは既に存在します。"
accessLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のアクセス制限ルールは存在しません。"
+ invalidPodeStateFormatExceptionMessage = 'PodeState ファイル "{0}" の形式が無効です。辞書のような構造 (ConcurrentDictionary、Hashtable、OrderedDictionary) が期待されましたが、[{1}] が見つかりました。ファイルの内容を確認するか、状態を再初期化してください。'
+ unknownJsonDictionaryTypeExceptionMessage = 'JSON 内の不明な辞書/コレクション型: {0}'
+ invalidPodeStateDataExceptionMessage = '提供されたデータは有効なPodeの状態ではありません。'
+ podeStateVersionMismatchExceptionMessage = '提供された状態データは、新しいバージョンのPode ({0}) からのものです。'
+ podeStateApplicationMismatchExceptionMessage = '提供された状態データは、別のアプリケーション ({0}) のものです。'
}
\ No newline at end of file
diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1
index a0d882588..8dc6a7be5 100644
--- a/src/Locales/ko/Pode.psd1
+++ b/src/Locales/ko/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 비율 제한 규칙이 존재하지 않습니다."
accessLimitRuleAlreadyExistsExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 이미 존재합니다."
accessLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 존재하지 않습니다."
+ invalidPodeStateFormatExceptionMessage = 'Het PodeState-bestand "{0}" bevat een ongeldig formaat. Verwacht werd een dictionary-achtige structuur (ConcurrentDictionary, Hashtable of OrderedDictionary), maar gevonden werd [{1}]. Controleer de inhoud van het bestand of initialiseert u de status opnieuw.'
+ unknownJsonDictionaryTypeExceptionMessage = 'JSON에서 알 수 없는 사전/컬렉션 유형: {0}'
+ invalidPodeStateDataExceptionMessage = '제공된 데이터는 유효한 Pode 상태 데이터가 아닙니다.'
+ podeStateVersionMismatchExceptionMessage = '제공된 상태 데이터는 최신 Pode 버전 ({0}) 에서 생성되었습니다.'
+ podeStateApplicationMismatchExceptionMessage = '제공된 상태 데이터는 다른 애플리케이션 ({0}) 에서 생성되었습니다.'
}
\ No newline at end of file
diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1
index 6aad6ebc4..eb0b5522b 100644
--- a/src/Locales/nl/Pode.psd1
+++ b/src/Locales/nl/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "Rate Limit-regel met de naam '{0}' bestaat niet."
accessLimitRuleAlreadyExistsExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat al."
accessLimitRuleDoesNotExistExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat niet."
+ invalidPodeStateFormatExceptionMessage = 'Plik PodeState "{0}" ma nieprawidłowy format. Oczekiwano struktury podobnej do słownika (ConcurrentDictionary, Hashtable lub OrderedDictionary), ale znaleziono [{1}]. Sprawdź zawartość pliku lub ponownie zainicjalizuj stan.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Onbekend woordenboek-/collectietype in JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'De opgegeven gegevens vertegenwoordigen geen geldige Pode-status.'
+ podeStateVersionMismatchExceptionMessage = 'De opgegeven statusgegevens zijn afkomstig van een nieuwere versie van Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'De opgegeven statusgegevens behoren tot een andere applicatie: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1
index cf5dd507b..5292410af 100644
--- a/src/Locales/pl/Pode.psd1
+++ b/src/Locales/pl/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "Reguła limitu szybkości o nazwie '{0}' nie istnieje."
accessLimitRuleAlreadyExistsExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' już istnieje."
accessLimitRuleDoesNotExistExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' nie istnieje."
+ invalidPodeStateFormatExceptionMessage = 'Plik PodeState "{0}" ma nieprawidłowy format. Oczekiwano struktury podobnej do słownika (ConcurrentDictionary, Hashtable lub OrderedDictionary), ale znaleziono [{1}]. Sprawdź zawartość pliku lub ponownie zainicjalizuj stan.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Nieznany typ słownika/kolekcji w JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'Podane dane nie są poprawnym stanem Pode.'
+ podeStateVersionMismatchExceptionMessage = 'Podane dane stanu pochodzą z nowszej wersji Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'Podane dane stanu należą do innej aplikacji: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1
index b8e1e4cd1..af35ad6de 100644
--- a/src/Locales/pt/Pode.psd1
+++ b/src/Locales/pt/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = "A regra de limite de taxa com o nome '{0}' não existe."
accessLimitRuleAlreadyExistsExceptionMessage = "A regra de limite de acesso com o nome '{0}' já existe."
accessLimitRuleDoesNotExistExceptionMessage = "A regra de limite de acesso com o nome '{0}' não existe."
+ invalidPodeStateFormatExceptionMessage = 'O arquivo PodeState "{0}" contém um formato inválido. Era esperado uma estrutura semelhante a um dicionário (ConcurrentDictionary, Hashtable ou OrderedDictionary), mas foi encontrado [{1}]. Verifique o conteúdo do arquivo ou reinicialize o estado.'
+ unknownJsonDictionaryTypeExceptionMessage = 'Tipo de dicionário/coleção desconhecido no JSON: {0}'
+ invalidPodeStateDataExceptionMessage = 'Os dados fornecidos não representam um estado válido do Pode.'
+ podeStateVersionMismatchExceptionMessage = 'Os dados de estado fornecidos são de uma versão mais recente do Pode: {0}.'
+ podeStateApplicationMismatchExceptionMessage = 'Os dados de estado fornecidos pertencem a outra aplicação: {0}.'
}
\ No newline at end of file
diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1
index 3653dc151..2cb1bd87f 100644
--- a/src/Locales/zh/Pode.psd1
+++ b/src/Locales/zh/Pode.psd1
@@ -326,4 +326,9 @@
rateLimitRuleDoesNotExistExceptionMessage = '速率限制规则不存在: {0}'
accessLimitRuleAlreadyExistsExceptionMessage = '访问限制规则已存在: {0}'
accessLimitRuleDoesNotExistExceptionMessage = '访问限制规则不存在: {0}'
+ invalidPodeStateFormatExceptionMessage = 'PodeState 文件 "{0}" 的格式无效。预期为类似字典的结构 (ConcurrentDictionary、Hashtable 或 OrderedDictionary),但发现 [{1}]。请验证文件内容或重新初始化状态。'
+ unknownJsonDictionaryTypeExceptionMessage = 'JSON 中的未知字典/集合类型: {0}'
+ invalidPodeStateDataExceptionMessage = '提供的数据不是有效的 Pode 状态数据。'
+ podeStateVersionMismatchExceptionMessage = '提供的状态数据来自较新版本的 Pode:{0}。'
+ podeStateApplicationMismatchExceptionMessage = '提供的状态数据属于另一个应用程序:{0}。'
}
\ No newline at end of file
diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1
index a4f4f9d98..70b8b6072 100644
--- a/src/Private/Context.ps1
+++ b/src/Private/Context.ps1
@@ -97,6 +97,7 @@ function New-PodeContext {
$ctx.Server.PodeModule = (Get-PodeModuleInfo)
$ctx.Server.Console = $Console
$ctx.Server.ComputerName = [System.Net.DNS]::GetHostName()
+ $ctx.Server.ApplicationName = (Get-PodeApplicationName)
# list of created listeners/receivers
$ctx.Listeners = @()
@@ -343,7 +344,7 @@ function New-PodeContext {
$ctx.Server.InbuiltDrives = @{}
# shared state between runspaces
- $ctx.Server.State = @{}
+ $ctx.Server.State = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
# setup caching
$ctx.Server.Cache = @{
diff --git a/src/Private/Convert.ps1 b/src/Private/Convert.ps1
new file mode 100644
index 000000000..3e944aa6c
--- /dev/null
+++ b/src/Private/Convert.ps1
@@ -0,0 +1,455 @@
+
+<#
+.SYNOPSIS
+ Deserializes JSON from ConvertTo-PodeCustomDictionaryJson (nested) back into
+ the original dictionary/collection type (Hashtable, ConcurrentDictionary, OrderedDictionary,
+ ConcurrentBag, ConcurrentQueue, ConcurrentStack, or PSCustomObject with PsTypeName).
+
+.DESCRIPTION
+ Recursively reads the JSON, checks the "Type" property, and reconstructs
+ the corresponding dictionary/collection. Also handles arrays, PSCustomObjects, and primitive types.
+
+.PARAMETER Json
+ A JSON string containing "Type" and "Items" at each dictionary/collection level.
+
+.OUTPUTS
+ - [Hashtable]
+ - [System.Collections.Concurrent.ConcurrentDictionary[string, object]]
+ - [System.Collections.Specialized.OrderedDictionary]
+ - [System.Collections.Concurrent.ConcurrentBag[object]]
+ - [System.Collections.Concurrent.ConcurrentQueue[object]]
+ - [System.Collections.Concurrent.ConcurrentStack[object]]
+ - [PSCustomObject] (when applicable, with preserved PsTypeName)
+ - Arrays, primitives, or other structures.
+
+.NOTES
+ This function is for internal Pode usage and may be subject to change.
+#>
+function ConvertFrom-PodeCustomDictionaryJson {
+ [CmdletBinding()]
+ [OutputType([hashtable])]
+ param(
+ [Parameter(Mandatory)]
+ [string]$Json
+ )
+
+ function Construct {
+ param([object]$obj)
+
+ <# Handle Null Values #>
+ if ($null -eq $obj) {
+ return $null
+ }
+
+ <# Handle Arrays/Lists #>
+ if ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) {
+ $resultList = @()
+ foreach ($item in $obj) {
+ $resultList += Construct $item
+ }
+ return $resultList
+ }
+
+ <# Handle PSCustomObject (Check for "Type" Property) #>
+ if ($obj -is [PSCustomObject]) {
+ if ($obj.PSObject.Properties.Name -contains 'Type') {
+ # Reconstruct Dictionaries & Collections
+ switch ($obj.Type) {
+ 'Hashtable' {
+ $dict = @{}
+ foreach ($pair in $obj.Items) {
+ $dict[$pair.Key] = (Construct -obj $pair.Value)
+ }
+ return $dict
+ }
+ 'ConcurrentDictionary' {
+ $dict = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ foreach ($pair in $obj.Items) {
+ $null = $dict.TryAdd($pair.Key, (Construct -obj $pair.Value))
+ }
+ return $dict
+ }
+ 'OrderedDictionary' {
+ $dict = [ordered]@{}
+ foreach ($pair in $obj.Items) {
+ $dict[$pair.Key] = (Construct -obj $pair.Value)
+ }
+ return $dict
+ }
+ 'ConcurrentBag' {
+ # Rebuild a ConcurrentBag[object]
+ $bag = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
+ foreach ($item in $obj.Items) {
+ $bag.Add((Construct -obj $item))
+ }
+ return , $bag # Prepend with a comma to return it as an object, not Object[]
+ }
+ 'ConcurrentQueue' {
+ # Rebuild a ConcurrentQueue[object]
+ $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
+ foreach ($item in $obj.Items) {
+ $queue.Enqueue((Construct -obj $item))
+ }
+ return $queue
+ }
+ 'ConcurrentStack' {
+ # Rebuild a ConcurrentStack[object]
+ $stack = [System.Collections.Concurrent.ConcurrentStack[object]]::new()
+ foreach ($item in $obj.Items) {
+ $stack.Push((Construct -obj $item))
+ }
+ return $stack
+ }
+ default {
+ throw ($PodeLocale.unknownJsonDictionaryTypeExceptionMessage -f $obj.Type)
+ }
+ }
+ }
+ else {
+ <# Preserve PSCustomObject Instead of Converting to Hashtable #>
+ $restoredObject = [PSCustomObject]@{}
+
+ <# Add Other Properties #>
+ $properties = $obj | Get-Member -MemberType NoteProperty, AliasProperty, ScriptProperty
+ foreach ($prop in $properties) {
+ if ($prop.Name -ne '__PsTypeName__') {
+ $restoredObject | Add-Member -MemberType NoteProperty -Name $prop.Name -Value (Construct($obj.$($prop.Name))) -Force
+ }
+ else {
+ if ( $obj.$($prop.Name) -ne 'System.Management.Automation.PSCustomObject') {
+ $restoredObject.PSTypeNames.Insert(0, $obj.$($prop.Name))
+ }
+ }
+ }
+
+ return $restoredObject
+ }
+ }
+
+ <# Return Primitive Values as-is #>
+ return $obj
+ }
+
+
+ # Parse the top-level JSON into a PSObject/Array
+ $parsed = $Json | ConvertFrom-Json
+ if ($parsed.Metadata) {
+ if ($parsed.Metadata.Product -ne 'Pode') {
+ # 'The provided data does not represent a valid Pode state.'
+ throw $PodeLocale.invalidPodeStateDataExceptionMessage
+ }
+ $podeVersion = (Get-PodeVersion -Raw)
+ if (!($podeVersion -eq '[dev]' -or ( ([System.Version]$parsed.Metadata) -le ([System.Version]$podeVersion))) ) {
+ # The provided state data originates from a newer Pode version:
+ throw ($PodeLocale.podeStateVersionMismatchExceptionMessage -f $parsed.Metadata)
+ }
+ if ($parsed.Metadata.Application -ne ($PodeContext.Server.ApplicationName)) {
+ # The provided state data belongs to a different application
+ throw ($PodeLocale.podeStateApplicationMismatchExceptionMessage -f $parsed.Metadata.Application)
+ }
+
+ <# Rebuild the Full Structure from JSON #>
+ return Construct -obj $parsed.Data
+ }
+ else {
+ return ConvertTo-PodeHashtable -InputObject $parsed
+ }
+}
+
+
+<#
+.SYNOPSIS
+ Serializes specialized PowerShell/Concurrent collections to JSON, preserving type info.
+
+.DESCRIPTION
+ This function checks the .NET type of the supplied object. If it's a [Hashtable],
+ [OrderedDictionary], [ConcurrentDictionary], [ConcurrentBag], [ConcurrentQueue], or [ConcurrentStack],
+ it serializes the data in a structured format with a "Type" property. Arrays and custom objects
+ are similarly processed recursively.
+
+.PARAMETER Dictionary
+ The object/collection to serialize.
+
+.PARAMETER Depth
+ Specifies how many levels of contained objects should be included in the JSON. Default is 10.
+
+.PARAMETER Compress
+ If supplied, the output JSON will be condensed (no extra whitespace).
+
+.OUTPUTS
+ [string] (JSON)
+
+.NOTES
+ This function is for internal Pode usage and may be subject to change.
+#>
+function ConvertTo-PodeCustomDictionaryJson {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [object]$Dictionary,
+
+ [Parameter()]
+ [int16]
+ $Depth = 20,
+
+ [switch]
+ $Compress
+ )
+
+ # Nested helper to recursively deconstruct objects
+ function Deconstruct {
+ param([Parameter()] $Object)
+
+ if ($null -eq $Object) {
+ return $null # Return null without modification
+ }
+
+ # Return common primitives directly
+ if ($Object.PSObject.BaseObject.GetType().IsPrimitive -or
+ $Object -is [string] -or
+ $Object -is [datetime]) {
+ return $Object
+ }
+
+ <# Handle PSCustomObject (Preserve PsTypeName) #>
+ if ($Object -is [PSCustomObject]) {
+ $serializedObject = [ordered]@{}
+ if ($Object.PSTypeNames.Count -gt 0) {
+ $serializedObject['__PsTypeName__'] = $Object.PSTypeNames[0] # Preserve the first PsTypeName
+ }
+ foreach ($prop in $Object.PSObject.Properties) {
+ $serializedObject[$prop.Name] = Deconstruct($prop.Value)
+ }
+ return $serializedObject
+ }
+
+ # Handle OrderedDictionary
+ if ($Object -is [System.Collections.Specialized.OrderedDictionary]) {
+ $wrapper = [PSCustomObject]@{ Type = 'OrderedDictionary'; Items = @() }
+ foreach ($key in $Object.Keys) {
+ $wrapper.Items += [PSCustomObject]@{
+ Key = $key
+ Value = Deconstruct($Object[$key])
+ }
+ }
+ return $wrapper
+ }
+
+ # Handle Hashtable
+ if ($Object -is [System.Collections.Hashtable]) {
+ $wrapper = [PSCustomObject]@{ Type = 'Hashtable'; Items = @() }
+ foreach ($key in $Object.Keys) {
+ $wrapper.Items += [PSCustomObject]@{
+ Key = $key
+ Value = Deconstruct($Object[$key])
+ }
+ }
+ return $wrapper
+ }
+
+ # Handle ConcurrentDictionary
+ if ($Object -is [System.Collections.Concurrent.ConcurrentDictionary[string, object]]) {
+ $wrapper = [PSCustomObject]@{ Type = 'ConcurrentDictionary'; Items = @() }
+ foreach ($key in $Object.Keys) {
+ $wrapper.Items += [PSCustomObject]@{
+ Key = $key
+ Value = Deconstruct($Object[$key])
+ }
+ }
+ return $wrapper
+ }
+
+ # Handle ConcurrentBag[object]
+ if ($Object -is [System.Collections.Concurrent.ConcurrentBag[object]]) {
+ $wrapper = [PSCustomObject]@{ Type = 'ConcurrentBag'; Items = @() }
+ foreach ($item in $Object) {
+ $wrapper.Items += Deconstruct($item)
+ }
+ return $wrapper
+ }
+
+ # Handle ConcurrentQueue[object]
+ if ($Object -is [System.Collections.Concurrent.ConcurrentQueue[object]]) {
+ $wrapper = [PSCustomObject]@{ Type = 'ConcurrentQueue'; Items = @() }
+ foreach ($item in $Object) {
+ $wrapper.Items += Deconstruct($item)
+ }
+ return $wrapper
+ }
+
+ # Handle ConcurrentStack[object]
+ if ($Object -is [System.Collections.Concurrent.ConcurrentStack[object]]) {
+ $wrapper = [PSCustomObject]@{ Type = 'ConcurrentStack'; Items = @() }
+ foreach ($item in $Object) {
+ $wrapper.Items += Deconstruct($item)
+ }
+ return $wrapper
+ }
+
+ # If it's a list/array, process each item but return as array
+ if ($Object -is [System.Collections.IEnumerable] -and $Object -isnot [string]) {
+ if ($Object.Count -eq 0) {
+ return , @()
+ }
+ $convertedArray = @()
+ foreach ($item in $Object) {
+ $convertedArray += Deconstruct($item)
+ }
+ return $convertedArray
+ }
+
+ # If it's a PSCustomObject, process each property individually
+ if ($Object -is [PSCustomObject]) {
+ $newObj = [ordered]@{}
+ $properties = $Object | Get-Member -MemberType NoteProperty, AliasProperty, ScriptProperty
+ foreach ($prop in $properties) {
+ $newObj[$prop.Name] = Deconstruct($Object.$($prop.Name))
+ }
+ return $newObj
+ }
+
+ # Fallback: Return object as-is (any other primitive or type)
+ return $Object
+ }
+
+ $converted = [ordered]@{
+ Metadata = [ordered]@{
+ Product = 'Pode'
+ Version = Get-PodeVersion
+ Timestamp = Get-PodeUtcNow
+ Application = $PodeContext.Server.ApplicationName
+ }
+ Data = @{}
+ }
+ # If top-level is null, treat as an empty dictionary
+ if ($null -ne $Dictionary) {
+ # Recursively convert any nested structures
+ $converted.Data = Deconstruct -Object $Dictionary
+ }
+
+ # Finally convert to JSON
+ return $converted | ConvertTo-Json -Depth $Depth -Compress:$Compress
+}
+
+<#
+.SYNOPSIS
+ Converts a PSCustomObject or nested object structure into a hashtable.
+
+.DESCRIPTION
+ The `ConvertTo-PodeHashtable` function recursively converts a PowerShell `PSCustomObject`
+ into a hashtable while preserving the original data structure. It ensures that objects,
+ arrays, and collections are properly transformed, while primitive types such as numbers,
+ booleans, and strings remain unchanged. Optionally, it can create an ordered hashtable.
+
+.PARAMETER InputObject
+ Specifies the input object to convert. The function can accept:
+ - A `PSCustomObject`, which will be transformed into a hashtable.
+ - A collection (`Array`, `List`), which will be processed recursively.
+ - A primitive type (`String`, `Number`, `Boolean`), which will remain unchanged.
+
+.PARAMETER Ordered
+ If specified, the resulting hashtable will be an ordered dictionary (`[ordered]@{}`),
+ preserving the order of properties as they appear in the original object.
+
+.OUTPUTS
+ [hashtable]
+ Returns a hashtable representation of the provided PSCustomObject.
+
+.EXAMPLE
+ $psCustomObject = [PSCustomObject]@{
+ Name = "Pode"
+ Version = 2.0
+ Active = $true
+ Metadata = [PSCustomObject]@{
+ Author = "Pode Team"
+ Created = "2025-02-03"
+ Stats = [PSCustomObject]@{
+ Users = 150
+ Servers = 5
+ }
+ }
+ Features = @("Fast", "Lightweight", "Modular")
+ }
+
+ $hashtable = ConvertTo-PodeHashtable -InputObject $psCustomObject
+ $hashtable
+
+.EXAMPLE
+ # Convert a list of PSCustomObjects to an array of hashtables
+ $users = @(
+ [PSCustomObject]@{ ID = 1; Name = "Alice" }
+ [PSCustomObject]@{ ID = 2; Name = "Bob" }
+ )
+
+ $hashtableList = ConvertTo-PodeHashtable -InputObject $users
+ $hashtableList
+
+.EXAMPLE
+ # Using pipeline input
+ $users | ConvertTo-PodeHashtable
+
+.EXAMPLE
+ # Convert a PSCustomObject to an ordered hashtable
+ $orderedHashtable = ConvertTo-PodeHashtable -InputObject $psCustomObject -Ordered
+ $orderedHashtable
+
+.NOTES
+ - This function ensures deep conversion of nested PSCustomObjects while leaving primitive values intact.
+ - Collections (e.g., Arrays, Lists) are processed recursively, preserving structure.
+ - The `Ordered` switch allows for property order preservation in the resulting hashtable.
+ - This function is for internal Pode usage and may be subject to change.
+#>
+function ConvertTo-PodeHashtable {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [PSCustomObject]$InputObject,
+ [switch]
+ $Ordered
+ )
+ begin {
+ # Define a recursive function within the process block
+ function Convert-ObjectRecursively {
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.Object]
+ $InputObject
+ )
+ # Initialize an ordered dictionary
+ $hashtable = if ($Ordered) { [ordered]@{} }else { @{} }
+
+ # Loop through each property of the PSCustomObject
+ foreach ($property in $InputObject.PSObject.Properties) {
+ # Check if the property value is a PSCustomObject
+ if ($property.Value -is [PSCustomObject]) {
+ # Recursively convert the nested PSCustomObject
+ $hashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value
+ }
+ elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) {
+ # If the value is a collection, check each element
+ $convertedCollection = @()
+ foreach ($item in $property.Value) {
+ if ($item -is [PSCustomObject]) {
+ $convertedCollection += Convert-ObjectRecursively -InputObject $item
+ }
+ else {
+ $convertedCollection += $item
+ }
+ }
+ $hashtable[$property.Name] = $convertedCollection
+ }
+ else {
+ # Add the property name and value to the ordered hashtable
+ $hashtable[$property.Name] = $property.Value
+ }
+ }
+
+ # Return the resulting ordered hashtable
+ return $hashtable
+ }
+ }
+ process {
+ # Call the recursive helper function for each input object
+ Convert-ObjectRecursively -InputObject $InputObject
+ }
+}
\ No newline at end of file
diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1
index b2a32e66c..15925c209 100644
--- a/src/Private/Helpers.ps1
+++ b/src/Private/Helpers.ps1
@@ -3942,6 +3942,77 @@ function Test-PodeIsISEHost {
return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name))
}
+<#
+.SYNOPSIS
+ Retrieves the name of the main Pode application script.
+
+.DESCRIPTION
+ The `Get-PodeApplicationName` function determines the name of the primary script (`.ps1`)
+ that started execution. It does this by examining the PowerShell call stack and
+ extracting the first script file that appears.
+
+ If no script file is found in the call stack, the function returns `"NoName"`.
+
+.OUTPUTS
+ [string]
+ Returns the filename of the main application script, or `"NoName"` if no script is found.
+
+.EXAMPLE
+ Get-PodeApplicationName
+
+ This retrieves the name of the main script that launched the Pode application.
+
+.EXAMPLE
+ $AppName = Get-PodeApplicationName
+ Write-Host "Application Name: $AppName"
+
+ This stores the retrieved application name in a variable and prints it.
+
+.NOTES
+ - This function relies on `Get-PSCallStack`, meaning it must be run within a script execution context.
+ - If called interactively or if no `.ps1` script is in the call stack, it will return `"NoName"`.
+ - This is an internal function and may change in future releases of Pode.
+#>
+function Get-PodeApplicationName {
+ $scriptFrame = (Get-PSCallStack | Where-Object { $_.Command -match '\.ps1$' } | Select-Object -First 1)
+ if ($scriptFrame) {
+ return [System.IO.Path]::GetFileName($scriptFrame.Command)
+ }
+ else {
+ return 'NoName'
+ }
+}
+
+
+<#
+.SYNOPSIS
+ Returns the current date and time in UTC format.
+
+.DESCRIPTION
+ This function retrieves the current date and time in Coordinated Universal Time (UTC), ensuring consistency across different time zones.
+
+.OUTPUTS
+ [DateTime] - The current UTC date and time.
+
+.EXAMPLE
+ Get-PodeUtcNow
+
+ Returns the current UTC datetime.
+
+.NOTES
+ - This function is required to allow Pester test to mock it
+ - This function is for internal Pode usage and may be subject to change.
+#>
+function Get-PodeUtcNow {
+ [CmdletBinding()]
+ [OutputType([System.DateTime])]
+ param ()
+
+ process {
+ return [System.DateTime]::UtcNow
+ }
+}
+
<#
.SYNOPSIS
Creates aliases for Pode OpenAPI functions to support legacy naming conventions.
@@ -3952,7 +4023,7 @@ function Test-PodeIsISEHost {
- Enable-PodeOA as an alias for Enable-PodeOpenApi.
- Get-PodeOpenApiDefinition as an alias for Get-PodeOADefinition.
The function helps maintain backward compatibility and simplifies calling Pode OpenAPI functions.
-
+
.PARAMETER None
This function does not accept any parameters.
.OUTPUTS
diff --git a/src/Private/ScopedVariables.ps1 b/src/Private/ScopedVariables.ps1
index 2991e13e3..cfcf32e80 100644
--- a/src/Private/ScopedVariables.ps1
+++ b/src/Private/ScopedVariables.ps1
@@ -76,7 +76,7 @@ function Add-PodeScopedVariableInbuiltSession {
function Add-PodeScopedVariableInbuiltState {
Add-PodeScopedVariable -Name 'state' `
-SetReplace "Set-PodeState -Name '{{name}}' -Value " `
- -GetReplace "`$PodeContext.Server.State.'{{name}}'.Value"
+ -GetReplace "`$PodeContext.Server.State['{{name}}'].Value"
}
function Add-PodeScopedVariableInbuiltUsing {
diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1
index 5d7054bd7..1c013f539 100644
--- a/src/Public/Core.ps1
+++ b/src/Public/Core.ps1
@@ -273,7 +273,7 @@ function Start-PodeServer {
# Create main context object
$PodeContext = New-PodeContext @ContextParams
-
+
# Define parameter values with comments explaining each one
$ConfigParameters = @{
DisableTermination = $DisableTermination # Disable termination of the Pode server from the console
diff --git a/src/Public/State.ps1 b/src/Public/State.ps1
index 1c841e586..b4cef555e 100644
--- a/src/Public/State.ps1
+++ b/src/Public/State.ps1
@@ -1,40 +1,67 @@
<#
.SYNOPSIS
-Sets an object within the shared state.
+ Sets an object within the shared state.
.DESCRIPTION
-Sets an object within the shared state.
+ Sets an object within the shared state, allowing for the creation of different collection types, such as a Hashtable, ConcurrentDictionary, or other concurrent collections.
.PARAMETER Name
-The name of the state object.
+ The name of the state object.
.PARAMETER Value
-The value to set in the state.
+ The value to set in the state. If a collection type is specified using `-NewCollectionType`, this value is ignored.
.PARAMETER Scope
-An optional Scope for the state object, used when saving the state.
+ An optional scope for the state object, used when saving the state.
+
+.PARAMETER NewCollectionType
+ Specifies the type of collection to create. Supported options include:
+ - Hashtable
+ - ConcurrentDictionary
+ - OrderedDictionary
+ - ConcurrentBag
+ - ConcurrentQueue
+ - ConcurrentStack
+
+ If this parameter is used, the state object will be initialized as the specified collection type.
+
+.EXAMPLE
+ Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
+
+.EXAMPLE
+ Set-PodeState -Name 'Users' -Value @('user1', 'user2') -Scope General, Users
.EXAMPLE
-Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
+ Set-PodeState -Name 'Cache' -NewCollectionType 'ConcurrentDictionary'
.EXAMPLE
-Set-PodeState -Name 'Users' -Value @('user1', 'user2') -Scope General, Users
+ Set-PodeState -Name 'Tasks' -NewCollectionType 'ConcurrentQueue'
+
+.NOTES
+ - `NewCollectionType` and `Value` are mutually exclusive; only one can be used at a time.
+ - The function ensures thread safety when using concurrent collections.
+ - Pode must be initialized before calling this function.
#>
function Set-PodeState {
- [CmdletBinding()]
+ [CmdletBinding(DefaultParameterSetName = 'Value')]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
- [Parameter(ValueFromPipeline = $true, Position = 0)]
+ [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Value')]
[object]
$Value,
[Parameter()]
[string[]]
- $Scope
+ $Scope,
+
+ [Parameter(Mandatory = $true, ParameterSetName = 'Collection')]
+ [ValidateSet('Hashtable', 'ConcurrentDictionary', 'OrderedDictionary', 'ConcurrentBag', 'ConcurrentQueue', 'ConcurrentStack')]
+ [string]
+ $NewCollectionType
)
begin {
@@ -52,25 +79,38 @@ function Set-PodeState {
}
process {
- # Add the current piped-in value to the array
+ # Collect piped-in values
$pipelineValue += $_
}
end {
- # Set Value to the array of values
+ # If multiple values were piped in, store them as an array
if ($pipelineValue.Count -gt 1) {
$Value = $pipelineValue
}
- $PodeContext.Server.State[$Name] = @{
- Value = $Value
- Scope = $Scope
+ # Initialize the state as a case-insensitive ConcurrentDictionary
+ $PodeContext.Server.State[$Name] = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+
+ # Create the specified collection type, or use the provided value
+ $PodeContext.Server.State[$Name].Value = switch ($NewCollectionType) {
+ 'Hashtable' { @{} }
+ 'ConcurrentDictionary' { [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
+ 'OrderedDictionary' { [ordered]@{} }
+ 'ConcurrentBag' { [System.Collections.Concurrent.ConcurrentBag[object]]::new() }
+ 'ConcurrentQueue' { [System.Collections.Concurrent.ConcurrentQueue[object]]::new() }
+ 'ConcurrentStack' { [System.Collections.Concurrent.ConcurrentStack[object]]::new() }
+ default { $Value } # If no collection type is specified, use the provided value
}
- return $Value
+ # Store the scope for the state object
+ $PodeContext.Server.State[$Name].Scope = $Scope
+
+ return $PodeContext.Server.State[$Name].Value
}
}
+
<#
.SYNOPSIS
Retrieves some state object from the shared state.
@@ -144,7 +184,6 @@ function Get-PodeStateNames {
)
if ($null -eq $PodeContext.Server.State) {
- # Pode has not been initialized
throw ($PodeLocale.podeNotInitializedExceptionMessage)
}
@@ -152,13 +191,16 @@ function Get-PodeStateNames {
$Scope = @()
}
- $tempState = $PodeContext.Server.State.Clone()
- $keys = $tempState.Keys
+ # Directly retrieve the keys from the ConcurrentDictionary
+ $keys = $PodeContext.Server.State.Keys
if ($Scope.Length -gt 0) {
$keys = @(foreach ($key in $keys) {
- if ($tempState[$key].Scope -iin $Scope) {
- $key
+ if ($PodeContext.Server.State.ContainsKey($key)) {
+ $scopeValue = $PodeContext.Server.State[$key]['Scope']
+ if ($scopeValue -is [string] -and ($scopeValue -iin $Scope)) {
+ $key
+ }
}
})
}
@@ -174,6 +216,7 @@ function Get-PodeStateNames {
return $keys
}
+
<#
.SYNOPSIS
Removes some state object from the shared state.
@@ -189,56 +232,81 @@ Remove-PodeState -Name 'Data'
#>
function Remove-PodeState {
[CmdletBinding()]
- [OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
- if ($null -eq $PodeContext.Server.State) {
+ if ($null -eq $PodeContext -or $null -eq $PodeContext.Server -or $null -eq $PodeContext.Server.State) {
# Pode has not been initialized
throw ($PodeLocale.podeNotInitializedExceptionMessage)
}
- $value = $PodeContext.Server.State[$Name].Value
- $null = $PodeContext.Server.State.Remove($Name)
- return $value
+ # ConcurrentDictionary requires TryRemove to remove and retrieve the value
+ $removedValue = $null
+ $removed = $PodeContext.Server.State.TryRemove($Name, [ref]$removedValue)
+
+ if ($removed) {
+ return $removedValue.Value
+ }
+
+ # If not removed (key didn't exist), return $null
+ return $null
}
<#
.SYNOPSIS
-Saves the current shared state to a supplied JSON file.
+ Saves the current Pode server state to a JSON file.
.DESCRIPTION
-Saves the current shared state to a supplied JSON file. When using this function, it's recommended to wrap it in a Lock-PodeObject block.
+ This function serializes the Pode state into a JSON file while preserving the structure
+ of dictionaries (`ConcurrentDictionary`, `Hashtable`, `OrderedDictionary`). It allows
+ filtering the saved state by scope, inclusion, or exclusion of specific keys.
+
+ For thread safety, it is recommended to wrap this function inside a `Lock-PodeObject` block.
.PARAMETER Path
-The path to a JSON file which the current state will be saved to.
+ Specifies the file path where the state should be saved.
.PARAMETER Scope
-An optional array of scopes for state objects that should be saved. (This has a lower precedence than Exclude/Include)
+ Filters the state objects to be saved based on their scope.
+ Only state objects within the specified scope(s) will be included.
+ This filter has **lower precedence** than Exclude and Include.
.PARAMETER Exclude
-An optional array of state object names to exclude from being saved. (This has a higher precedence than Include)
+ Specifies state object names to **exclude** from being saved.
+ This filter has **higher precedence** than Include.
.PARAMETER Include
-An optional array of state object names to only include when being saved.
+ Specifies state object names to **only** include in the saved state.
+ This filter has **lower precedence** than Exclude.
.PARAMETER Depth
-Saved JSON maximum depth. Will be passed to ConvertTo-JSON's -Depth parameter. Default is 10.
+ Defines the maximum depth for JSON serialization.
+ This value is passed to `ConvertTo-PodeCustomDictionaryJson`. Default is **20**.
.PARAMETER Compress
-If supplied, the saved JSON will be compressed.
+ If specified, the JSON output will be minified (no extra whitespace).
.EXAMPLE
-Save-PodeState -Path './state.json'
+ Save-PodeState -Path './state.json'
+ Saves the entire Pode state to `state.json`.
.EXAMPLE
-Save-PodeState -Path './state.json' -Exclude Name1, Name2
+ Save-PodeState -Path './state.json' -Exclude 'SessionData', 'UserCache'
+ Saves the Pode state but **excludes** the specified state keys.
.EXAMPLE
-Save-PodeState -Path './state.json' -Scope Users
+ Save-PodeState -Path './state.json' -Scope 'Users'
+ Saves **only** state objects that belong to the `"Users"` scope.
+
+.OUTPUTS
+ [System.Void] - This function does not return an output. The state is saved to a file.
+
+.NOTES
+ - This function is intended for internal Pode usage and may be subject to changes.
+ - For more information, refer to: https://github.com/Badgerati/Pode/tree/develop
#>
function Save-PodeState {
[CmdletBinding()]
@@ -261,158 +329,190 @@ function Save-PodeState {
[Parameter()]
[int16]
- $Depth = 10,
+ $Depth = 20,
[switch]
$Compress
)
- # error if attempting to use outside of the pode server
- if ($null -eq $PodeContext.Server.State) {
- # Pode has not been initialized
+
+ # Validate Pode Server Context
+
+ if ($null -eq $PodeContext -or
+ $null -eq $PodeContext.Server -or
+ $null -eq $PodeContext.Server.State) {
throw ($PodeLocale.podeNotInitializedExceptionMessage)
}
- # get the full path to save the state
+ # Convert relative path to absolute
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
- # contruct the state to save (excludes, etc)
- $state = $PodeContext.Server.State.Clone()
- # scopes
- if (($null -ne $Scope) -and ($Scope.Length -gt 0)) {
- foreach ($_key in $state.Clone().Keys) {
- # remove if no scope
- if (($null -eq $state[$_key].Scope) -or ($state[$_key].Scope.Length -eq 0)) {
- $null = $state.Remove($_key)
- continue
- }
+ # Create a Shallow Copy of the Current State
+
+ # A new ConcurrentDictionary is created to store a snapshot of the current state,
+ # preventing modifications while the state is being serialized.
+ $state = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ foreach ($kvp in $PodeContext.Server.State.GetEnumerator()) {
+ $null = $state.TryAdd($kvp.Key, $kvp.Value)
+ }
- # check scopes (only remove if none match)
- $found = $false
- foreach ($_scope in $state[$_key].Scope) {
- if ($Scope -icontains $_scope) {
- $found = $true
- break
+ # Filter State by Scope
+
+ if (($null -ne $Scope) -and ($Scope.Length -gt 0)) {
+ $keys = $state.Keys
+ foreach ($key in $keys) {
+ if ($state.ContainsKey($key)) {
+ $value = $state[$key]
+
+ # Remove state objects that lack a scope
+ if (($null -eq $value.Scope) -or ($value.Scope.Count -eq 0)) {
+ $null = $state.TryRemove($key, [ref]$null)
+ continue
}
- }
- if ($found) {
- continue
- }
+ # Remove objects that do not match the specified scope(s)
+ $found = $false
+ foreach ($item in $value.Scope) {
+ if ($Scope -icontains $item) {
+ $found = $true
+ break
+ }
+ }
- # none matched, remove
- $null = $state.Remove($_key)
+ if (!$found) {
+ $null = $state.TryRemove($key, [ref]$null)
+ }
+ }
}
}
- # include keys
+
+ # If Include is defined, only keep the specified keys
if (($null -ne $Include) -and ($Include.Length -gt 0)) {
- foreach ($_key in $state.Clone().Keys) {
- if ($Include -inotcontains $_key) {
- $null = $state.Remove($_key)
+ $keys = $state.Keys
+ foreach ($key in $keys) {
+ if ($Include -inotcontains $key) {
+ $null = $state.TryRemove($key, [ref]$null)
}
}
}
- # exclude keys
+ # If Exclude is defined, remove the specified keys from the state
if (($null -ne $Exclude) -and ($Exclude.Length -gt 0)) {
- foreach ($_key in $state.Clone().Keys) {
- if ($Exclude -icontains $_key) {
- $null = $state.Remove($_key)
+ $keys = $state.Keys
+ foreach ($key in $keys) {
+ if ($Exclude -icontains $key) {
+ $null = $state.TryRemove($key, [ref]$null)
}
}
}
- # save the state
- $null = ConvertTo-Json -InputObject $state -Depth $Depth -Compress:$Compress | Out-File -FilePath $Path -Force
+ # The state is converted to JSON while preserving dictionary types (Hashtable,
+ # OrderedDictionary, ConcurrentDictionary). The Compress flag minifies output.
+ $json = ConvertTo-PodeCustomDictionaryJson -Dictionary $state -Depth $Depth -Compress:$Compress
+ $json | Out-File -FilePath $Path -Force
}
<#
.SYNOPSIS
-Restores the shared state from some JSON file.
+ Restores the Pode shared state from a JSON file.
.DESCRIPTION
-Restores the shared state from some JSON file.
+ This function reads a JSON file and restores the Pode server state.
+ It preserves dictionary types (ConcurrentDictionary, Hashtable, OrderedDictionary)
+ and ensures state integrity. If the file does not exist, the function exits silently.
+
+ The function supports **merging** the restored state with the current Pode state or
+ **overwriting** it entirely.
.PARAMETER Path
-The path to a JSON file that contains the state information.
+ Specifies the JSON file path containing the saved state.
.PARAMETER Merge
-If supplied, the state loaded from the JSON file will be merged with the current state, instead of overwriting it.
+ If specified, the loaded state will be merged with the existing Pode state instead
+ of replacing it.
.PARAMETER Depth
-Saved JSON maximum depth. Will be passed to ConvertFrom-JSON's -Depth parameter (Powershell >=6). Default is 10.
+ Defines the maximum depth for JSON deserialization.
+ This value is passed to `ConvertFrom-PodeCustomDictionaryJson`. Default is **20**.
+
+.EXAMPLE
+ Restore-PodeState -Path './state.json'
+ Restores the Pode state from `state.json`, replacing the current state.
.EXAMPLE
-Restore-PodeState -Path './state.json'
+ Restore-PodeState -Path './state.json' -Merge
+ Merges the loaded state with the existing Pode state.
+
+.OUTPUTS
+ [System.Void] - The function updates `$PodeContext.Server.State` but does not return a value.
+
+.NOTES
+ - This function is intended for internal Pode usage and may be subject to changes.
+ - For more details, refer to: https://github.com/Badgerati/Pode/tree/develop
#>
function Restore-PodeState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
- [string]
- $Path,
+ [string]$Path,
- [switch]
- $Merge,
+ [switch]$Merge,
- [int16]
- $Depth = 10
+ [int16]$Depth = 20
)
- # error if attempting to use outside of the pode server
- if ($null -eq $PodeContext.Server.State) {
- # Pode has not been initialized
+ <# Validate Pode Server Context #>
+ if ($null -eq $PodeContext -or
+ $null -eq $PodeContext.Server -or
+ $null -eq $PodeContext.Server.State) {
throw ($PodeLocale.podeNotInitializedExceptionMessage)
}
- # get the full path to the state
+ <# Resolve File Path and Check Existence #>
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
if (!(Test-Path $Path)) {
- return
+ return # Exit silently if the file does not exist
}
- # restore the state from file
- $state = @{}
-
- if (Test-PodeIsPSCore) {
- $state = (Get-Content $Path -Force | ConvertFrom-Json -AsHashtable -Depth $Depth)
+ <# Read and Deserialize JSON #>
+ $json = Get-Content -Path $Path -Raw -Force
+ if (![string]::IsNullOrWhiteSpace($json)) {
+ # Deserialize the JSON, preserving dictionary structures
+ $state = ConvertFrom-PodeCustomDictionaryJson -Json $json
}
else {
- $props = (Get-Content $Path -Force | ConvertFrom-Json).psobject.properties
- foreach ($prop in $props) {
- $state[$prop.Name] = $prop.Value
- }
+ return # Exit if the file is empty
}
- # check for no scopes, and add for backwards compat
- $convert = $false
- foreach ($_key in $state.Clone().Keys) {
- if ($null -eq $state[$_key].Scope) {
- $convert = $true
- break
- }
- }
+ <# Ensure Backward Compatibility for Missing Scopes #>
+ # Older versions of Pode may not include scope properties in state objects.
+ foreach ($_key in $state.Keys) {
+ if ($_key) {
+ if ($null -eq $state[$_key].Scope) {
+ $state[$_key].Scope = @()
- if ($convert) {
- foreach ($_key in $state.Clone().Keys) {
- $state[$_key] = @{
- Value = $state[$_key]
- Scope = @()
}
}
}
- # set the scope to the main context
- if ($Merge) {
- foreach ($_key in $state.Clone().Keys) {
- $PodeContext.Server.State[$_key] = $state[$_key]
+
+ <# Validate and Apply the Restored State #>
+ if ($state -is [System.Collections.IDictionary]) {
+ if (! $Merge) {
+ # If not merging, clear the existing state before applying the restored data
+ $PodeContext.Server.State.Clear()
+ }
+ # Merge or replace each key in the state
+ foreach ($key in $state.Keys) {
+ $null = $PodeContext.Server.State.TryAdd($key, $state[$key])
}
}
else {
- $PodeContext.Server.State = $state.Clone()
+ # Raise an error if the file format is invalid
+ throw ($PodeLocale.invalidPodeStateFormatExceptionMessage -f $Path, $state.GetType().FullName)
}
}
@@ -444,4 +544,4 @@ function Test-PodeState {
}
return $PodeContext.Server.State.ContainsKey($Name)
-}
\ No newline at end of file
+}
diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1
index 64d21b677..fdc942f3f 100644
--- a/src/Public/Utilities.ps1
+++ b/src/Public/Utilities.ps1
@@ -1357,12 +1357,19 @@ function New-PodeCron {
<#
.SYNOPSIS
Retrieves the version of the Pode module.
+ Retrieves the version of the Pode module.
.DESCRIPTION
The `Get-PodeVersion` function checks the version of the Pode module as specified in the module manifest.
If the module version is **not** the placeholder value (`'$version$'`), it returns the actual version prefixed with `'v'`.
If the module version **is** the placeholder value, indicating the development branch, it returns `"[dev]"`.
+ The `Get-PodeVersion` function checks the version of the Pode module as specified in the module manifest.
+ If the module version is **not** the placeholder value (`'$version$'`), it returns the actual version prefixed with `'v'`.
+ If the module version **is** the placeholder value, indicating the development branch, it returns `"[dev]"`.
+.PARAMETER Raw
+ If specified, the function returns only the raw module version without the `'v'` prefix.
+ By default, the function formats the version as `'vX.Y.Z'` unless the module is in development mode.
.PARAMETER Raw
If specified, the function returns only the raw module version without the `'v'` prefix.
By default, the function formats the version as `'vX.Y.Z'` unless the module is in development mode.
@@ -1372,17 +1379,26 @@ function New-PodeCron {
Returns a string representing the Pode module version in one of the following formats:
- `"vX.Y.Z"` for a release version (e.g., `"v1.2.3"`).
- `"[dev]"` for development versions.
+ System.String
+ Returns a string representing the Pode module version in one of the following formats:
+ - `"vX.Y.Z"` for a release version (e.g., `"v1.2.3"`).
+ - `"[dev]"` for development versions.
.EXAMPLE
PS> Get-PodeVersion
Returns the Pode module version, e.g., `'v1.2.3'` for release versions or `"[dev]"` if in development.
+ PS> Get-PodeVersion
+ Returns the Pode module version, e.g., `'v1.2.3'` for release versions or `"[dev]"` if in development.
.EXAMPLE
PS> Get-PodeVersion -Raw
Returns the raw version number, e.g., `'1.2.3'`, without the `'v'` prefix.
+ PS> Get-PodeVersion -Raw
+ Returns the raw version number, e.g., `'1.2.3'`, without the `'v'` prefix.
.NOTES
- If the module version is a placeholder (`'$version$'`), the function assumes it's running from the development branch.
+ - If the module version is a placeholder (`'$version$'`), the function assumes it's running from the development branch.
#>
function Get-PodeVersion {
param (
diff --git a/tests/shared/TestHelper.ps1 b/tests/shared/TestHelper.ps1
index 38ccacbe2..2d4bc638b 100644
--- a/tests/shared/TestHelper.ps1
+++ b/tests/shared/TestHelper.ps1
@@ -47,3 +47,145 @@ function Import-PodeAssembly {
Add-Type -LiteralPath (Join-Path -Path $netFolder -ChildPath 'Pode.dll') -ErrorAction Stop
}
}
+
+
+function Compare-Hashtable {
+ param (
+ [object]$Hashtable1,
+ [object]$Hashtable2
+ )
+
+ # Function to compare two hashtable values
+ function Compare-Value($value1, $value2) {
+ # Check if both values are hashtables
+ if ((($value1 -is [hashtable] -or $value1 -is [System.Collections.Specialized.OrderedDictionary]) -and
+ ($value2 -is [hashtable] -or $value2 -is [System.Collections.Specialized.OrderedDictionary]))) {
+ return Compare-Hashtable -Hashtable1 $value1 -Hashtable2 $value2
+ }
+ # Check if both values are arrays
+ elseif (($value1 -is [Object[]]) -and ($value2 -is [Object[]])) {
+ if ($value1.Count -ne $value2.Count) {
+ return $false
+ }
+ for ($i = 0; $i -lt $value1.Count; $i++) {
+ $found = $false
+ for ($j = 0; $j -lt $value2.Count; $j++) {
+ if ( Compare-Value $value1[$i] $value2[$j]) {
+ $found = $true
+ }
+ }
+ if ($found -eq $false) {
+ return $false
+ }
+ }
+ return $true
+ }
+ else {
+ if ($value1 -is [string] -and $value2 -is [string]) {
+ return Compare-StringRnLn $value1 $value2
+ }
+ # Check if the values are equal
+ return $value1 -eq $value2
+ }
+ }
+
+ $keys1 = $Hashtable1.Keys
+ $keys2 = $Hashtable2.Keys
+
+ # Check if both hashtables have the same keys
+ if ($keys1.Count -ne $keys2.Count) {
+ return $false
+ }
+
+ foreach ($key in $keys1) {
+ if (! ($Hashtable2.Keys -contains $key)) {
+ return $false
+ }
+
+ if ($Hashtable2[$key] -is [hashtable] -or $Hashtable2[$key] -is [System.Collections.Specialized.OrderedDictionary]) {
+ if (! (Compare-Hashtable -Hashtable1 $Hashtable1[$key] -Hashtable2 $Hashtable2[$key])) {
+ return $false
+ }
+ }
+ elseif (!(Compare-Value $Hashtable1[$key] $Hashtable2[$key])) {
+ return $false
+ }
+ }
+
+ return $true
+}
+
+
+function Compare-StringRnLn {
+ param (
+ [string]$InputString1,
+ [string]$InputString2
+ )
+ return ($InputString1.Trim() -replace "`r`n|`n|`r", "`n") -eq ($InputString2.Trim() -replace "`r`n|`n|`r", "`n")
+}
+
+function Convert-PsCustomObjectToOrderedHashtable {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [PSCustomObject]$InputObject
+ )
+ begin {
+ # Define a recursive function within the process block
+ function Convert-ObjectRecursively {
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.Object]
+ $InputObject
+ )
+
+ # Initialize an ordered dictionary
+ $orderedHashtable = [ordered]@{}
+
+ # Loop through each property of the PSCustomObject
+ foreach ($property in $InputObject.PSObject.Properties) {
+ # Check if the property value is a PSCustomObject
+ if ($property.Value -is [PSCustomObject]) {
+ # Recursively convert the nested PSCustomObject
+ $orderedHashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value
+ }
+ elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) {
+ # If the value is a collection, check each element
+ $convertedCollection = @()
+ foreach ($item in $property.Value) {
+ if ($item -is [PSCustomObject]) {
+ $convertedCollection += Convert-ObjectRecursively -InputObject $item
+ }
+ else {
+ $convertedCollection += $item
+ }
+ }
+ $orderedHashtable[$property.Name] = $convertedCollection
+ }
+ else {
+ # Add the property name and value to the ordered hashtable
+ $orderedHashtable[$property.Name] = $property.Value
+ }
+ }
+
+ # Return the resulting ordered hashtable
+ return $orderedHashtable
+ }
+ }
+ process {
+ # Call the recursive helper function for each input object
+ Convert-ObjectRecursively -InputObject $InputObject
+ }
+}
+
+function Get-PodeModuleManifest {
+ param(
+ [string]$Src
+ )
+ # Construct the path to the module manifest (.psd1 file)
+ $moduleManifestPath = Join-Path -Path $Src -ChildPath 'Pode.psd1'
+
+ # Import the module manifest to access its properties
+ $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath
+ return $moduleManifest
+}
\ No newline at end of file
diff --git a/tests/unit/Convert.Tests.ps1 b/tests/unit/Convert.Tests.ps1
new file mode 100644
index 000000000..f51c60162
--- /dev/null
+++ b/tests/unit/Convert.Tests.ps1
@@ -0,0 +1,130 @@
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
+param()
+BeforeAll {
+ Add-Type -AssemblyName 'System.Net.Http' -ErrorAction SilentlyContinue
+ $path = $PSCommandPath
+ $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/'
+ Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ }
+ Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode'
+
+ $helperPath = (Split-Path -Parent -Path $path) -ireplace 'unit', 'shared'
+ . "$helperPath/TestHelper.ps1"
+
+ # Import the module manifest to access its properties
+ $PodeManifest = Get-PodeModuleManifest -Src $src
+}
+
+Describe 'ConvertFrom-PodeCustomDictionaryJson' {
+ BeforeAll {
+ $PodeContext = @{Server = @{ ApplicationName = 'Pester' } }
+ }
+ It 'Should correctly deserialize a Hashtable' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"Foo","Value":"Bar"},{"Key":"Baz","Value":42}]}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result | Should -BeOfType Hashtable
+ $result['Foo'] | Should -Be 'Bar'
+ $result['Baz'] | Should -Be 42
+ }
+
+ It 'Should correctly deserialize a ConcurrentDictionary' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Type":"ConcurrentDictionary","Items":[{"Key":"Key1","Value":123},{"Key":"Key2","Value":"Test"}]}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result | Should -BeOfType 'System.Collections.Concurrent.ConcurrentDictionary[string, object]'
+ $result.ContainsKey('Key1') | Should -BeTrue
+ $result['Key1'] | Should -Be 123
+ $result['Key2'] | Should -Be 'Test'
+ }
+
+ It 'Should correctly deserialize an OrderedDictionary' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Type":"OrderedDictionary","Items":[{"Key":"First","Value":1},{"Key":"Second","Value":2}]}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary'
+ $result['First'] | Should -Be 1
+ $result['Second'] | Should -Be 2
+ }
+
+ It 'Should correctly deserialize a ConcurrentBag' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Type":"ConcurrentBag","Items":["Item1","Item2","Item3"]}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result.GetType().Name | Should -Be 'ConcurrentBag`1'
+ $result.Count | Should -Be 3
+ }
+
+ It 'Should correctly deserialize a PSCustomObject' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Name":"John","Age":30,"__PsTypeName__":"CustomType"}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result | Should -BeOfType PSCustomObject
+ $result.Name | Should -Be 'John'
+ $result.Age | Should -Be 30
+ $result.PSTypeNames[0] | Should -Be 'CustomType'
+ }
+
+ It 'Should correctly deserialize a recursively nested dictionary' {
+ $json = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:30:24.6971206Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"Level1","Value":{"Type":"OrderedDictionary","Items":[{"Key":"Level2","Value":{"Type":"Hashtable","Items":[{"Key":"Final","Value":"Reached"}]}}]}}]}}'
+ $result = ConvertFrom-PodeCustomDictionaryJson -Json $json
+
+ $result | Should -BeOfType Hashtable
+ $result['Level1'] | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary'
+ $result['Level1']['Level2'] | Should -BeOfType Hashtable
+ $result['Level1']['Level2']['Final'] | Should -Be 'Reached'
+ }
+}
+
+
+Describe 'ConvertTo-PodeCustomDictionaryJson' {
+ BeforeAll {
+ $PodeContext = @{Server = @{ ApplicationName = 'Pester' } }
+ mock Get-PodeUtcNow { '2025-02-04T01:54:30.6400033Z' }
+ }
+ It 'Should correctly serialize a recursively nested dictionary' {
+ $dictionary = @{ 'Level1' = @{ 'Level2' = @{ 'Final' = 'Reached' } } }
+ $json = ConvertTo-PodeCustomDictionaryJson -Dictionary $dictionary | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+ $expected = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:54:30.6400033Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"Level1","Value":{"Type":"Hashtable","Items":[{"Key":"Level2","Value":{"Type":"Hashtable","Items":[{"Key":"Final","Value":"Reached"}]}}]}}]}}' |
+ ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+ Compare-Hashtable $json $expected | Should -BeTrue
+ }
+
+ It 'Should correctly serialize a dictionary with multiple types' {
+ $dictionary = @{ 'String' = 'Test'; 'Number' = 123; 'Boolean' = $true; 'Array' = @(1, 2, 3) }
+ $json = ConvertTo-PodeCustomDictionaryJson -Dictionary $dictionary | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+ $expected = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:54:30.6400033Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"Array","Value":[1,2,3]},{"Key":"Boolean","Value":true},{"Key":"Number","Value":123},{"Key":"String","Value":"Test"}]}}' |
+ ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+
+ Compare-Hashtable $json $expected | Should -BeTrue
+ }
+
+ It 'Should correctly serialize nested dictionaries and collections' {
+ $dictionary = @{ 'Dict' = @{ 'SubDict' = @{ 'Key' = 'Value' } }; 'List' = @(1, 2, @{ 'Nested' = 'Yes' }) }
+ $json = ConvertTo-PodeCustomDictionaryJson -Dictionary $dictionary | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+ $expected = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:54:30.6400033Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"List","Value":[1,2,{"Type":"Hashtable","Items":[{"Key":"Nested","Value":"Yes"}]}]},{"Key":"Dict","Value":{"Type":"Hashtable","Items":[{"Key":"SubDict","Value":{"Type":"Hashtable","Items":[{"Key":"Key","Value":"Value"}]}}]}}]}}' |
+ ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+
+ Compare-Hashtable $json $expected | Should -BeTrue
+ }
+
+ It 'Should correctly serialize thread-safe collections' {
+ $concurrentDictionary = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ $concurrentDictionary['Key1'] = 'Value1'
+ $concurrentDictionary['Key2'] = 42
+
+ $concurrentBag = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
+ $concurrentBag.Add('Item1')
+ $concurrentBag.Add('Item2')
+
+ $dictionary = @{ 'ConcurrentDict' = $concurrentDictionary; 'ConcurrentBag' = $concurrentBag }
+ $json = ConvertTo-PodeCustomDictionaryJson -Dictionary $dictionary | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+ $expected = '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:54:30.6400033Z","Application":"Pester"},"Data":{"Type":"Hashtable","Items":[{"Key":"ConcurrentBag","Value":{"Type":"ConcurrentBag","Items":["Item2","Item1"]}},{"Key":"ConcurrentDict","Value":{"Type":"ConcurrentDictionary","Items":[{"Key":"Key1","Value":"Value1"},{"Key":"Key2","Value":42}]}}]}}' |
+ ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable
+
+ Compare-Hashtable $json $expected | Should -BeTrue
+ }
+}
+
+
+
diff --git a/tests/unit/State.Tests.ps1 b/tests/unit/State.Tests.ps1
index f8c2ff61b..b3b7440b5 100644
--- a/tests/unit/State.Tests.ps1
+++ b/tests/unit/State.Tests.ps1
@@ -7,6 +7,12 @@ BeforeAll {
Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ }
Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode'
+ $helperPath = (Split-Path -Parent -Path $path) -ireplace 'unit', 'shared'
+ . "$helperPath/TestHelper.ps1"
+
+ # Import the module manifest to access its properties
+ $PodeManifest = Get-PodeModuleManifest -Src $src
+
$PodeContext = @{ 'Server' = $null; }
}
@@ -17,7 +23,7 @@ Describe 'Set-PodeState' {
}
It 'Sets and returns an object' {
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
$result = Set-PodeState -Name 'test' -Value 7
$result | Should -Be 7
@@ -26,11 +32,11 @@ Describe 'Set-PodeState' {
}
It 'Sets by pipe and returns an object array' {
- $PodeContext.Server = @{ 'State' = @{} }
- $result = @(7,3,4)|Set-PodeState -Name 'test'
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
+ $result = @(7, 3, 4) | Set-PodeState -Name 'test'
- $result | Should -Be @(7,3,4)
- $PodeContext.Server.State['test'].Value | Should -Be @(7,3,4)
+ $result | Should -Be @(7, 3, 4)
+ $PodeContext.Server.State['test'].Value | Should -Be @(7, 3, 4)
$PodeContext.Server.State['test'].Scope | Should -Be @()
}
}
@@ -42,7 +48,7 @@ Describe 'Get-PodeState' {
}
It 'Gets an object from the state' {
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Get-PodeState -Name 'test' | Should -Be 8
}
@@ -55,7 +61,7 @@ Describe 'Remove-PodeState' {
}
It 'Removes an object from the state' {
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Remove-PodeState -Name 'test' | Should -Be 8
$PodeContext.Server.State['test'] | Should -Be $null
@@ -72,7 +78,7 @@ Describe 'Save-PodeState' {
Mock Get-PodeRelativePath { return $Path }
Mock Out-File {}
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Save-PodeState -Path './state.json'
@@ -83,7 +89,7 @@ Describe 'Save-PodeState' {
Mock Get-PodeRelativePath { return $Path }
Mock Out-File {}
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Save-PodeState -Path './state.json' -Include 'test'
@@ -94,7 +100,7 @@ Describe 'Save-PodeState' {
Mock Get-PodeRelativePath { return $Path }
Mock Out-File {}
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Save-PodeState -Path './state.json' -Exclude 'test'
@@ -111,9 +117,9 @@ Describe 'Restore-PodeState' {
It 'Restores the state from file' {
Mock Get-PodeRelativePath { return $Path }
Mock Test-Path { return $true }
- Mock Get-Content { return '{ "Name": "Morty" }' }
+ Mock Get-Content { return '{"Metadata":{"Product":"Pode","Version":"[dev]","Timestamp":"2025-02-04T01:54:30.6400033Z","Application":"Pester"},"Data":{"Type":"ConcurrentDictionary","Items":[{"Key":"Name","Value":{"Type":"ConcurrentDictionary","Items":[{"Key":"Value","Value":"Morty"},{"Key":"Scope","Value":[]}]}}]}}' }
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase); ApplicationName = 'Pester' }
Restore-PodeState -Path './state.json'
Get-PodeState -Name 'Name' | Should -Be 'Morty'
}
@@ -126,14 +132,101 @@ Describe 'Test-PodeState' {
}
It 'Returns true for an object being in the state' {
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Test-PodeState -Name 'test' | Should -Be $true
}
It 'Returns false for an object not being in the state' {
- $PodeContext.Server = @{ 'State' = @{} }
+ $PodeContext.Server = @{ 'State' = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) }
Set-PodeState -Name 'test' -Value 8
Test-PodeState -Name 'tests' | Should -Be $false
}
-}
\ No newline at end of file
+}
+
+# Get-PodeStateNames.Tests.ps1
+# Pester 5 test script for Get-PodeStateNames
+
+# If your function is in a separate file, dot-source it. Adjust the path as needed:
+# . "$PSScriptRoot\..\Functions\Get-PodeStateNames.ps1"
+
+Describe 'Get-PodeStateNames' -Tags 'Unit', 'Pode' {
+ BeforeAll {
+ # Mocking up $PodeLocale and $PodeContext to simulate Pode's environment.
+ $PodeLocale = @{
+ podeNotInitializedExceptionMessage = 'Pode has not been initialized.'
+ }
+
+ $PodeContext = @{
+ Server = @{
+ State = $null
+ }
+ }
+
+ }
+
+ Context 'When PodeContext.Server.State is $null' {
+ It 'Throws an exception if state is null' {
+ { Get-PodeStateNames } | Should -Throw 'Pode has not been initialized.'
+ }
+ }
+
+ Context 'When PodeContext.Server.State is a valid ConcurrentDictionary' {
+ BeforeEach {
+ # Initialize the thread-safe dictionary before each test
+ $PodeContext.Server.State = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+
+ # For each key, store another ConcurrentDictionary with "Scope" and "Data"
+ # Key1 -> { Scope = 'Test1'; Data = 'Value1' }
+ # Key2 -> { Scope = 'Test2'; Data = 'Value2' }
+ # SpecialKey -> { Scope = 'Test1'; Data = 'SpecialValue' }
+
+ $cd1 = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ $cd1['Scope'] = 'Test1'
+ $cd1['Data'] = 'Value1'
+ $null = $PodeContext.Server.State.TryAdd('Key1', $cd1)
+
+ $cd2 = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ $cd2['Scope'] = 'Test2'
+ $cd2['Data'] = 'Value2'
+ $null = $PodeContext.Server.State.TryAdd('Key2', $cd2)
+
+ $cd3 = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ $cd3['Scope'] = 'Test1'
+ $cd3['Data'] = 'SpecialValue'
+ $null = $PodeContext.Server.State.TryAdd('SpecialKey', $cd3)
+ }
+
+ It 'Returns all keys if no scope or pattern is specified' {
+ $keys = Get-PodeStateNames
+ $keys | Should -Contain 'Key1'
+ $keys | Should -Contain 'Key2'
+ $keys | Should -Contain 'SpecialKey'
+ $keys.Count | Should -Be 3
+ }
+
+ It 'Filters by scope correctly' {
+ $keys = Get-PodeStateNames -Scope 'Test1'
+ $keys.Count | Should -Be 2
+ $keys | Should -Contain 'Key1'
+ $keys | Should -Contain 'SpecialKey'
+ $keys | Should -Not -Contain 'Key2'
+ }
+
+ It 'Filters by pattern correctly' {
+ # Pattern to match "Key\d" (e.g. Key1, Key2)
+ $keys = Get-PodeStateNames -Pattern 'Key\d'
+ $keys.Count | Should -Be 2
+ $keys | Should -Contain 'Key1'
+ $keys | Should -Contain 'Key2'
+ $keys | Should -Not -Contain 'SpecialKey'
+ }
+
+ It 'Filters by both scope and pattern' {
+ # e.g. Scope = 'Test1', Pattern = 'Special'
+ $keys = Get-PodeStateNames -Scope 'Test1' -Pattern 'Special'
+ $keys.Count | Should -Be 1
+ $keys | Should -Contain 'SpecialKey'
+ }
+ }
+}
From 019bf82aec7fbede8ff5058aa0aedcf9ca6ef868 Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:32:44 -0800
Subject: [PATCH 06/12] Create Version.json
---
Version.json | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 Version.json
diff --git a/Version.json b/Version.json
new file mode 100644
index 000000000..13efd672d
--- /dev/null
+++ b/Version.json
@@ -0,0 +1,4 @@
+{
+ "Version": "2.13.0",
+ "Prerelease": "alpha.3"
+}
\ No newline at end of file
From 182df9838e2c2dc7968053ad9734d485206cfa35 Mon Sep 17 00:00:00 2001
From: mdaneri
Date: Mon, 3 Mar 2025 07:43:08 -0800
Subject: [PATCH 07/12] Squashed commit of the following:
commit f057eff21ddd1e9e9cfac651c28a6616d5679615
Author: mdaneri
Date: Sun Mar 2 10:07:28 2025 -0800
Folder reorganization
commit d91b7a5bfb2091c8472968df58f3f8f8bddade69
Author: mdaneri
Date: Sat Mar 1 14:13:58 2025 -0800
fix powershell support message
commit 5d7db3723080ed6d6b888a9bf57555bf41206ae7
Author: mdaneri
Date: Sat Mar 1 04:47:58 2025 -0800
fix tests
commit e1f4e244a4a994705e67a35193686e7fbc2e40e4
Author: mdaneri
Date: Sat Mar 1 03:12:49 2025 -0800
fix tests for 5.1
commit c54d0a61669dfd62a15861f10245d95593411ca6
Author: mdaneri
Date: Fri Feb 28 07:41:21 2025 -0800
new Digest client module with documentation
commit 044070de08cdd593f2b1a1f1cbfebc74faae9bc6
Author: mdaneri
Date: Fri Feb 28 06:39:02 2025 -0800
update
commit 11a574cf13e8034ef2c5b069495d8fd702184cea
Author: mdaneri
Date: Fri Feb 28 06:18:33 2025 -0800
new Invoke-WebRequestDigest
commit ea89876bca3e79ea906c4e5f436567d2ec00e815
Author: mdaneri
Date: Thu Feb 27 12:09:18 2025 -0800
fix Export-Certificate
commit 3789b335cb8e6ecd7acce77ddb8791cb7af719d2
Author: mdaneri
Date: Thu Feb 27 10:58:50 2025 -0800
Update Cryptography.ps1
commit 1ef78ac34eaaee502ced748986835989e498204e
Author: mdaneri
Date: Thu Feb 27 10:53:35 2025 -0800
change parameter Import-PodeCertificate -FilePath to -Path
commit 6a428339566852d7ecc03d1bb0ee2abd6cc8baee
Author: mdaneri
Date: Thu Feb 27 09:20:13 2025 -0800
added digest check for $QualityOfProtection 'auth-int'
commit 9f83eccbbbde354676557cbbf7cc4ae6279d7277
Author: mdaneri
Date: Wed Feb 26 10:16:53 2025 -0800
Update DigestAuthentication.Tests.ps1
commit ec6fb18a8497efca96dcc7f5409719b062545af0
Author: mdaneri
Date: Wed Feb 26 10:15:40 2025 -0800
added digest tests
commit 6116794b543c42cf5c7e2c1d1480ea9056be6e59
Author: mdaneri
Date: Tue Feb 25 18:41:45 2025 -0800
test update and fixes
commit 76bab58e6613e78ab84b143da56c0ccd42180147
Author: mdaneri
Date: Tue Feb 25 15:35:06 2025 -0800
Fix Test and build
commit 57588200ca38d9386aff5775dadfaf3f8cd49b11
Author: mdaneri
Date: Tue Feb 25 10:50:05 2025 -0800
update tests
commit f0ca68ed64978624d4bb6470aeec6aaa97c34205
Author: mdaneri
Date: Tue Feb 25 08:45:44 2025 -0800
Update JWTAuthentication.Tests.ps1
commit 2df6dfe8da270c0cf31ad5e34993499ad77ea68f
Author: mdaneri
Date: Sun Feb 23 21:23:58 2025 -0800
fix mac os issue with ssl
commit 72674fcfe945f7e9ec99a5f9ec0c1baebf9a3a1f
Author: mdaneri
Date: Sun Feb 23 18:14:49 2025 -0800
update macOS
commit 2f4e5ce26ad7a6d31ee4c0e327081eb513725ec1
Author: MDaneri
Date: Sun Feb 23 17:18:29 2025 -0800
fix linux mac SSL detection issue
commit c3c24d816d479abc1cd43745cc9e405161273d14
Author: mdaneri
Date: Sun Feb 23 10:13:51 2025 -0800
update Test-PodeCertificate and SSL documentation
commit c42e497469cf53559aa1293b65895a013846cd36
Merge: 6dd871d9 67505f56
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sun Feb 23 07:30:47 2025 -0800
Merge branch 'develop' into RFC-7616-Compliance
commit 6dd871d9e61078b53ff6fc7925037786e1669bda
Author: mdaneri
Date: Sat Feb 22 18:54:09 2025 -0800
added certificate test
commit 04d76a6ce599cc622b736084dce527c1f9cc025b
Merge: 7db8ff72 cbdc62fe
Author: mdaneri
Date: Sat Feb 22 09:17:52 2025 -0800
Merge remote-tracking branch 'upstream/develop' into RFC-7616-Compliance
commit 7db8ff724b3a22885e5572afb0c7b85730602f69
Author: mdaneri
Date: Sat Feb 22 09:00:11 2025 -0800
added certificate documentation
commit ecc2729e59bef6ce6a1a55337410c5398298e4fd
Merge: 908cdafb b40d4e84
Author: mdaneri
Date: Sat Feb 22 07:32:52 2025 -0800
Merge branch 'RFC-7616-Compliance' of https://github.com/mdaneri/Pode into RFC-7616-Compliance
commit 908cdafbe7b03d95f300fea469cdecb7f6a79b35
Author: mdaneri
Date: Sat Feb 22 07:32:50 2025 -0800
Fix MacOS EphemeralKeySet issue
commit b40d4e8458de02c3b84e29405859bb03de13c314
Merge: 5fc7a002 fbf6ecfb
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Sat Feb 22 06:32:32 2025 -0800
Merge branch 'develop' into RFC-7616-Compliance
commit 5fc7a002f764410750d73dbd40577c01fad48b05
Author: mdaneri
Date: Fri Feb 21 16:04:02 2025 -0800
fixex
commit 4b0d09cd3ef7499b4c19d1339621c243a027b799
Author: mdaneri
Date: Fri Feb 21 13:42:52 2025 -0800
fix issue with ephemeral
commit 7f06e5095bb563844bdd9e40d132fb3ade156708
Author: mdaneri
Date: Fri Feb 21 10:07:28 2025 -0800
revert pode.build
commit 5bbd552a5df0762f8dfa48f2656c950bf272159c
Author: mdaneri
Date: Fri Feb 21 08:57:00 2025 -0800
added certificate management functions
commit 6add82dee252c99df1d87863fc546fea94216bce
Author: mdaneri
Date: Wed Feb 19 08:41:14 2025 -0800
added JWT lifecycle
commit 601c1cbf9620a84df882865420a946bec9bf1a72
Author: mdaneri
Date: Wed Feb 19 08:05:10 2025 -0800
update JWT lifecycle
commit 9fd8589ce25cc202721b0295ecdceba682a050ad
Author: mdaneri
Date: Tue Feb 18 20:23:19 2025 -0800
update build for 5.1
commit 9a35551bae9742d1348c22c3997309b2fbb2e896
Author: mdaneri
Date: Tue Feb 18 10:05:39 2025 -0800
Added Update-PodeJWT and support for bearer Body token
commit 393998337ae318c9d7abb091c4c9a2f15fbfaf97
Author: mdaneri
Date: Sun Feb 16 18:58:13 2025 -0800
moved Authentication example inside Authentication folder
commit fc4ed64c09937ec7be324882e4b62626f83d0e96
Author: mdaneri
Date: Sun Feb 16 18:46:34 2025 -0800
Update Cryptography.ps1
commit 74584e947f7f02cf3e48cc08a80d766a9353808f
Author: MDaneri
Date: Sun Feb 16 18:45:19 2025 -0800
added header to Get-PodeJwtSigningAlgorithm
commit cf107b2c95ff32bcbe395d2ba70a68eacf05ef25
Author: MDaneri
Date: Sun Feb 16 18:17:22 2025 -0800
added workaround for .Net Linux issue
commit 6f54b3f7ff259b2049e501268ce153e35c8d82e0
Author: mdaneri
Date: Sun Feb 16 11:12:37 2025 -0800
macos fixes
commit 852d8c06fb539441ef01b78bdd0d814a8f3dc575
Author: mdaneri
Date: Sun Feb 16 10:58:52 2025 -0800
fixed tests
commit 0f0c2528cc604fc9bf76b4fc9496ce01f05ed3b2
Author: mdaneri
Date: Sun Feb 16 10:29:30 2025 -0800
updated comments and documentation
commit 73685484d8051d4da52a27fbe10b36588b40191d
Author: mdaneri
Date: Sun Feb 16 09:43:52 2025 -0800
updated to adhere the Pode standard for the certificate
commit feb7a2dfe73225f191ce5c53ebae884ebd4ba0f2
Merge: 363168b3 a76741bc
Author: mdaneri
Date: Sun Feb 16 07:06:14 2025 -0800
Merge remote-tracking branch 'upstream/develop' into RFC-7616-Compliance
commit 363168b34d02aa59b32883ffbac101c15cd05355
Author: mdaneri
Date: Sat Feb 15 16:51:30 2025 -0800
Change from PEM cert to PFX and adding 5.1 support
commit 84bda14066918b7e45a54ba27aa4debe838002e4
Author: mdaneri
Date: Thu Feb 13 09:57:07 2025 -0800
Update Cryptography.Tests.ps1
commit 61072c2f18fdf8c48cf1be0ce1c7c429bef3ef3f
Author: mdaneri
Date: Thu Feb 13 09:44:17 2025 -0800
Update Cryptography.Tests.ps1
commit 881b72cb89fb7010935e44eafdf7dbc0f5fe9643
Author: mdaneri
Date: Thu Feb 13 09:28:45 2025 -0800
doc update
commit 0e3ae54f4efc15400f237d7603b579bf3f4c8484
Author: mdaneri
Date: Thu Feb 13 08:22:59 2025 -0800
fix Desktop tests
commit 5a2f77f7b7f1722bccab0e6e1cd6ecf394deb0c9
Author: mdaneri
Date: Thu Feb 13 07:26:32 2025 -0800
Update pode.build.ps1
commit 2bc4d848663b6087bbb7fe49861defd9ef101819
Author: MDaneri
Date: Tue Feb 11 19:32:47 2025 -0800
fix not windows issue
commit e98748ae0810b18780b2cbfbba892e378fecfa2f
Merge: 119d479d a236a1a0
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Tue Feb 11 18:47:14 2025 -0800
Merge branch 'develop' into RFC-7616-Compliance
commit 119d479d9560049f86e82e311fc6ab68ac13e4ee
Author: mdaneri
Date: Tue Feb 11 18:19:20 2025 -0800
working version
commit a148489d3cc0f935f12709371ae4078d86a9d8b1
Author: mdaneri
Date: Tue Feb 11 10:00:16 2025 -0800
work in progress
commit 71315d5ed95f1eacee3fe29ecfe4c5a666d0fe2b
Merge: d5fface5 e9e73340
Author: mdaneri
Date: Mon Feb 10 06:47:34 2025 -0800
Merge remote-tracking branch 'upstream/develop' into RFC-7616-Compliance
commit d5fface5f8544fcdfe9ab5e5afe71c645472891b
Author: mdaneri
Date: Mon Feb 10 06:47:15 2025 -0800
added missing header and language entries
commit 93fb8ea007e20615ddd63252ce06a73db52f2034
Author: mdaneri
Date: Sun Feb 9 21:00:34 2025 -0800
fixes
commit 7a8f61a1d820c96ed82d59f0a87209b41a16987f
Merge: 615d4ab8 75e29626
Author: mdaneri
Date: Sun Feb 9 07:21:04 2025 -0800
Merge remote-tracking branch 'upstream/develop' into RFC-7616-Compliance
commit 615d4ab8d4fe3c738ca1a2e12a438b8a269d95a1
Author: mdaneri
Date: Sat Feb 8 11:23:24 2025 -0800
fix tests and
remove Invoke-PodeJWTSign
merge Invoke-PodeJWTSign in New-PodeJwtSignature
commit 815ca109792384ee7d12b1efe0fca78fedda7a14
Author: mdaneri
Date: Sat Feb 8 10:16:44 2025 -0800
Enhance Authentication: RFC Compliance, JWT Algorithms, and Bearer Query Support
- Added full support for RFC 7518 JWT algorithms: NONE, HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512.
- Introduced `-PrivateKey` parameter in `New-PodeAuthScheme` for RSA and ECDSA JWT signature verification.
- Ensured JWT signature validation follows RFC 7518 standards.
- Improved JWT validation for `exp` (expiration) and `nbf` (not before) claims.
- Added support for passing Bearer tokens via query parameters (`-BearerLocation Query`) as per RFC 6750.
- Updated `WWW-Authenticate` handling to correctly return headers on authentication failures for all authentication methods.
- Ensured Pode authentication mechanisms align with industry security standards.
- Updated documentation to reflect these enhancements.
commit 2af345c840425a4ecea4af084d4ab7bfafc6b7f5
Merge: 23889e8e 6b23fc33
Author: mdaneri <17148649+mdaneri@users.noreply.github.com>
Date: Wed Feb 5 13:13:29 2025 -0800
Merge branch 'develop' into RFC-7616-Compliance
commit 23889e8e9d637b44e258155feb29a9d02b4c070b
Author: mdaneri
Date: Fri Jan 31 06:52:55 2025 -0800
Fix markdown
commit 4690effd3ed851155e4ee34bf767530958cf59fc
Author: mdaneri
Date: Fri Jan 31 06:34:56 2025 -0800
update documentation
commit b53f07d6b93b8bcb89372ef498f3c7c7bbe2f47b
Author: mdaneri
Date: Thu Jan 30 10:31:14 2025 -0800
Update Cryptography.ps1
commit 3088db2184d044e4ec98cf2683c1f195d23dfded
Author: mdaneri
Date: Thu Jan 30 10:11:08 2025 -0800
Fixed tests
commit 9090b07942525acaffdc85940879192a82524867
Author: mdaneri
Date: Thu Jan 30 09:30:23 2025 -0800
add rawdata
commit 71d4747d6501036a9ca38be866bf1f3401c2784f
Author: mdaneri
Date: Thu Jan 30 07:50:04 2025 -0800
Adding auth-int
commit ed3a0f5f6b98f8f3a507fb0427b0364b2ebf6fa3
Author: mdaneri
Date: Wed Jan 29 18:05:34 2025 -0800
fixes
commit 062cc2d60f9651e99f3034ba7be7a9fe93968b95
Author: mdaneri
Date: Wed Jan 29 09:01:17 2025 -0800
first drop
---
.gitignore | 6 +-
.vscode/settings.json | 8 +
README.md | 65 +-
.../Authentication/Methods/ApiKey.md | 4 +-
.../Authentication/Methods/Bearer.md | 195 +-
.../Authentication/Methods/Digest.md | 180 +-
docs/Tutorials/Authentication/Methods/JWT.md | 307 ++-
docs/Tutorials/Basics.md | 5 +
docs/Tutorials/Certificates.md | 310 ++-
docs/Tutorials/Ssl.md | 85 +
docs/index.md | 61 +-
.../Authentication/Modules/Invoke-Digest.psm1 | 662 ++++++
.../Web-AuthApiKey.ps1} | 4 +-
.../{ => Authentication}/Web-AuthBasic.ps1 | 6 +-
.../Web-AuthBasicAccess.ps1 | 4 +-
.../Web-AuthBasicAdhoc.ps1 | 4 +-
.../Web-AuthBasicAnon.ps1 | 4 +-
.../Web-AuthBasicBearer.ps1 | 14 +-
.../Web-AuthBasicClientcert.ps1 | 4 +-
.../Web-AuthBasicHeader.ps1 | 13 +-
examples/Authentication/Web-AuthBearerJWT.ps1 | 248 +++
.../Web-AuthBearerJWTLifecycle.ps1 | 274 +++
examples/Authentication/Web-AuthDigest.ps1 | 215 ++
.../{ => Authentication}/Web-AuthForm.ps1 | 6 +-
.../Web-AuthFormAccess.ps1 | 4 +-
.../{ => Authentication}/Web-AuthFormAd.ps1 | 4 +-
.../{ => Authentication}/Web-AuthFormAnon.ps1 | 4 +-
.../Web-AuthFormCreds.ps1 | 4 +-
.../{ => Authentication}/Web-AuthFormFile.ps1 | 4 +-
.../Web-AuthFormLocal.ps1 | 4 +-
.../Web-AuthFormMerged.ps1 | 4 +-
.../Web-AuthFormSessionAuth.ps1 | 4 +-
.../{ => Authentication}/Web-AuthMerged.ps1 | 6 +-
.../Web-AuthNegotiate.ps1 | 4 +-
.../{ => Authentication}/Web-AuthOauth2.ps1 | 4 +-
.../Web-AuthOauth2Form.ps1 | 4 +-
.../Web-AuthOauth2Oidc.ps1 | 6 +-
.../{ => Authentication}/Web-UsePodeAuth.ps1 | 4 +-
.../WebAuth-ApikeyJWT.ps1} | 4 +-
.../{ => Authentication}/auth/SampleAuth.ps1 | 0
.../Authentication/client/New-JwtKeyPair.ps1 | 150 ++
.../client/Test-BearerClient.ps1 | 146 ++
examples/Authentication/outfile.json | 1 +
examples/OpenApi-TuttiFrutti.ps1 | 21 +-
examples/Web-AuthDigest.ps1 | 80 -
examples/certs/cert.pem | 21 -
examples/certs/cert_nodes.pem | 21 -
examples/certs/key.pem | 30 -
examples/certs/key_nodes.pem | 28 -
examples/certs/pode-cert.cer | Bin 796 -> 0 bytes
examples/certs/pode-cert.pfx | Bin 2597 -> 0 bytes
pode.build.ps1 | 14 +
src/Locales/ar/Pode.psd1 | 33 +-
src/Locales/de/Pode.psd1 | 32 +-
src/Locales/en-us/Pode.psd1 | 32 +-
src/Locales/en/Pode.psd1 | 33 +-
src/Locales/es/Pode.psd1 | 32 +-
src/Locales/fr/Pode.psd1 | 34 +-
src/Locales/it/Pode.psd1 | 32 +-
src/Locales/ja/Pode.psd1 | 32 +-
src/Locales/ko/Pode.psd1 | 32 +-
src/Locales/nl/Pode.psd1 | 32 +-
src/Locales/pl/Pode.psd1 | 32 +-
src/Locales/pt/Pode.psd1 | 32 +-
src/Locales/zh/Pode.psd1 | 32 +-
src/Pode.psd1 | 19 +-
src/Private/ADAuthentication.ps1 | 749 +++++++
src/Private/Authentication.ps1 | 1841 +++++++++--------
src/Private/Certificate.ps1 | 425 ++++
src/Private/Context.ps1 | 14 +-
src/Private/Cryptography.ps1 | 258 +--
src/Private/Helpers.ps1 | 303 ++-
src/Private/Jwt.ps1 | 918 ++++++++
src/Private/Middleware.ps1 | 3 +-
src/Private/Security.ps1 | 250 ++-
src/Public/Authentication.ps1 | 626 +++---
src/Public/Certificate.ps1 | 1052 ++++++++++
src/Public/Core.ps1 | 16 +-
src/Public/Endpoint.ps1 | 28 +-
src/Public/Jwt.ps1 | 979 +++++++++
src/Public/Routes.ps1 | 8 +
tests/integration/Authentication.Tests.ps1 | 4 +-
.../DigestAuthentication.Tests.ps1 | 444 ++++
tests/integration/JWTAuthentication.Tests.ps1 | 600 ++++++
tests/shared/TestHelper.ps1 | 2 +
tests/unit/Certificate.Tests.ps1 | 381 ++++
tests/unit/Cryptography.Tests.ps1 | 7 +-
tests/unit/Jwt.Tests.ps1 | 132 ++
88 files changed, 10736 insertions(+), 2003 deletions(-)
create mode 100644 docs/Tutorials/Ssl.md
create mode 100644 examples/Authentication/Modules/Invoke-Digest.psm1
rename examples/{WebAuth-ApikeyJWT.ps1 => Authentication/Web-AuthApiKey.ps1} (94%)
rename examples/{ => Authentication}/Web-AuthBasic.ps1 (93%)
rename examples/{ => Authentication}/Web-AuthBasicAccess.ps1 (96%)
rename examples/{ => Authentication}/Web-AuthBasicAdhoc.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthBasicAnon.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthBasicBearer.ps1 (80%)
rename examples/{ => Authentication}/Web-AuthBasicClientcert.ps1 (92%)
rename examples/{ => Authentication}/Web-AuthBasicHeader.ps1 (84%)
create mode 100644 examples/Authentication/Web-AuthBearerJWT.ps1
create mode 100644 examples/Authentication/Web-AuthBearerJWTLifecycle.ps1
create mode 100644 examples/Authentication/Web-AuthDigest.ps1
rename examples/{ => Authentication}/Web-AuthForm.ps1 (92%)
rename examples/{ => Authentication}/Web-AuthFormAccess.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthFormAd.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthFormAnon.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthFormCreds.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthFormFile.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthFormLocal.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthFormMerged.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthFormSessionAuth.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthMerged.ps1 (95%)
rename examples/{ => Authentication}/Web-AuthNegotiate.ps1 (89%)
rename examples/{ => Authentication}/Web-AuthOauth2.ps1 (93%)
rename examples/{ => Authentication}/Web-AuthOauth2Form.ps1 (94%)
rename examples/{ => Authentication}/Web-AuthOauth2Oidc.ps1 (93%)
rename examples/{ => Authentication}/Web-UsePodeAuth.ps1 (93%)
rename examples/{Web-AuthApiKey.ps1 => Authentication/WebAuth-ApikeyJWT.ps1} (93%)
rename examples/{ => Authentication}/auth/SampleAuth.ps1 (100%)
create mode 100644 examples/Authentication/client/New-JwtKeyPair.ps1
create mode 100644 examples/Authentication/client/Test-BearerClient.ps1
create mode 100644 examples/Authentication/outfile.json
delete mode 100644 examples/Web-AuthDigest.ps1
delete mode 100644 examples/certs/cert.pem
delete mode 100644 examples/certs/cert_nodes.pem
delete mode 100644 examples/certs/key.pem
delete mode 100644 examples/certs/key_nodes.pem
delete mode 100644 examples/certs/pode-cert.cer
delete mode 100644 examples/certs/pode-cert.pfx
create mode 100644 src/Private/ADAuthentication.ps1
create mode 100644 src/Private/Certificate.ps1
create mode 100644 src/Private/Jwt.ps1
create mode 100644 src/Public/Certificate.ps1
create mode 100644 src/Public/Jwt.ps1
create mode 100644 tests/integration/DigestAuthentication.Tests.ps1
create mode 100644 tests/integration/JWTAuthentication.Tests.ps1
create mode 100644 tests/unit/Certificate.Tests.ps1
create mode 100644 tests/unit/Jwt.Tests.ps1
diff --git a/.gitignore b/.gitignore
index 99de26566..937948c82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -270,4 +270,8 @@ docs/Getting-Started/Samples.md
# Dump Folder
Dump
-
+examples/certs/*-public.pem
+examples/certs/*-private.pem
+tests/certs/*
+/examples/certs
+examples/Authentication/certs/*
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b5be5ef8e..4dd3cd280 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -38,5 +38,13 @@
"javascript.format.insertSpaceBeforeFunctionParenthesis": false,
"[yaml]": {
"editor.tabSize": 2
+ },
+ "markdownlint.config": {
+ "default": true,
+ "MD045": false,
+ "MD033": false,
+ "MD026": {
+ "punctuation": ".,;:"
+ }
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3ffdf9f6f..30dcb52fe 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
-
+
[](https://raw.githubusercontent.com/Badgerati/Pode/master/LICENSE.txt)
[](https://badgerati.github.io/Pode)
@@ -53,36 +53,37 @@ Then navigate to `http://127.0.0.1:8000` in your browser.
## 🚀 Features
-* Cross-platform using PowerShell Core (with support for PS5)
-* Docker support, including images for ARM/Raspberry Pi
-* Azure Functions, AWS Lambda, and IIS support
-* OpenAPI specification version 3.0.x and 3.1.0
-* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
-* Listen on a single or multiple IP(v4/v6) address/hostnames
-* Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
-* Host REST APIs, Web Pages, and Static Content (with caching)
-* Support for custom error pages
-* Request and Response compression using GZip/Deflate
-* Multi-thread support for incoming requests
-* Inbuilt template engine, with support for third-parties
-* Async timers for short-running repeatable processes
-* Async scheduled tasks using cron expressions for short/long-running processes
-* Supports logging to CLI, Files, and custom logic for other services like LogStash
-* Cross-state variable access across multiple runspaces
-* Restart the server via file monitoring, or defined periods/times
-* Ability to allow/deny requests from certain IP addresses and subnets
-* Basic rate limiting for IP addresses and subnets
-* Middleware and Sessions on web servers, with Flash message and CSRF support
-* Authentication on requests, such as Basic, Windows and Azure AD
-* Authorisation support on requests, using Roles, Groups, Scopes, etc.
-* Support for dynamically building Routes from Functions and Modules
-* Generate/bind self-signed certificates
-* Secret management support to load secrets from vaults
-* Support for File Watchers
-* In-memory caching, with optional support for external providers (such as Redis)
-* (Windows) Open the hosted server as a desktop application
-* FileBrowsing support
-* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese
+- ✅ Cross-platform using PowerShell Core (with support for PS5)
+- ✅ Docker support, including images for ARM/Raspberry Pi
+- ✅ Azure Functions, AWS Lambda, and IIS support
+- ✅ OpenAPI specification version 3.0.x and 3.1.0
+- ✅ OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
+- ✅ Listen on a single or multiple IP(v4/v6) addresses/hostnames
+- ✅ Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
+- ✅ Host REST APIs, Web Pages, and Static Content (with caching)
+- ✅ Support for custom error pages
+- ✅ Request and Response compression using GZip/Deflate
+- ✅ Multi-thread support for incoming requests
+- ✅ Inbuilt template engine, with support for third-parties
+- ✅ Async timers for short-running repeatable processes
+- ✅ Async scheduled tasks using cron expressions for short/long-running processes
+- ✅ Supports logging to CLI, Files, and custom logic for other services like LogStash
+- ✅ Cross-state variable access across multiple runspaces
+- ✅ Restart the server via file monitoring, or defined periods/times
+- ✅ Ability to allow/deny requests from certain IP addresses and subnets
+- ✅ Basic rate limiting for IP addresses and subnets
+- ✅ Middleware and Sessions on web servers, with Flash message and CSRF support
+- ✅ Authentication on requests, such as Basic, Windows and Azure AD
+- ✅ Authorisation support on requests, using Roles, Groups, Scopes, etc.
+- ✅ Enhanced authentication support, including Basic, Bearer (with JWT), Certificate, Digest, Form, OAuth2, and ApiKey (with JWT).
+- ✅ Support for dynamically building Routes from Functions and Modules
+- ✅ Generate/bind self-signed certificates
+- ✅ Secret management support to load secrets from vaults
+- ✅ Support for File Watchers
+- ✅ In-memory caching, with optional support for external providers (such as Redis)
+- ✅ (Windows) Open the hosted server as a desktop application
+- ✅ FileBrowsing support
+- ✅ Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese,Dutch and Chinese
## 📦 Install
diff --git a/docs/Tutorials/Authentication/Methods/ApiKey.md b/docs/Tutorials/Authentication/Methods/ApiKey.md
index 2167867cf..6d754daa0 100644
--- a/docs/Tutorials/Authentication/Methods/ApiKey.md
+++ b/docs/Tutorials/Authentication/Methods/ApiKey.md
@@ -26,13 +26,13 @@ Start-PodeServer {
}
```
-By default, Pode will look for an `X-API-KEY` header in the request. You can change this to Cookie or Query by using the `-Location` parameter. To change the name of what Pode looks for, you can use `-LocationName`.
+By default, Pode will look for an `X-API-KEY` header in the request. You can change this to Cookie or Query by using the `-ApiKeyLocation` parameter. To change the name of what Pode looks for, you can use `-LocationName`.
For example, to look for an `appId` query value:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -ApiKey -Location Query -LocationName 'appId' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthScheme -ApiKey -ApiKeyLocation Query -LocationName 'appId' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($key)
# check if the key is valid, and get user
diff --git a/docs/Tutorials/Authentication/Methods/Bearer.md b/docs/Tutorials/Authentication/Methods/Bearer.md
index 77f92abbe..be388f8ae 100644
--- a/docs/Tutorials/Authentication/Methods/Bearer.md
+++ b/docs/Tutorials/Authentication/Methods/Bearer.md
@@ -6,13 +6,30 @@ Bearer authentication lets you authenticate a user based on a token, with option
Authorization: Bearer
```
+!!! note
+ **`New-PodeAuthScheme -Bearer` is deprecated.** Please use **`New-PodeAuthBearerScheme`**.
+
## Setup
-To start using Bearer authentication in Pode you can use `New-PodeAuthScheme -Bearer`, and then pipe the returned object into [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameter supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock is the `$token` from the Authorization token:
+To start using Bearer authentication in Pode, call **`New-PodeAuthBearerScheme`**, and then pipe the returned object into [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameter supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's **ScriptBlock** is the `$token` from the Authorization header.
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # check if the token is valid, and get user
+
+ return @{ User = $user }
+ }
+}
+```
+
+By default, Pode will look for a token in the **`Authorization`** header, verifying that it starts with the **`Bearer`** tag. You can customize this tag via **`-HeaderTag`**. You can also change the token extraction location to the **query string** using **`-Location Query`**. For the **`-Location query`** the standard tag is **`access_token`**:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthBearerScheme -Location Query | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($token)
# check if the token is valid, and get user
@@ -22,13 +39,129 @@ Start-PodeServer {
}
```
-By default, Pode will check if the request's header contains an `Authorization` key, and whether the value of that key starts with `Bearer` tag. The `New-PodeAuthScheme -Bearer` function can be supplied parameters to customise the tag using `-HeaderTag`.
+**Note:** Per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750), using the Authorization header is recommended for sending bearer tokens. Query parameters should only be used when headers are not feasible, as query strings may be logged in URLs, potentially exposing sensitive information.
+
+## JWT Support
+
+`New-PodeAuthBearerScheme` supports **JWT authentication** with various security levels and algorithms. Set **`-AsJWT`** to enable JWT validation. Depending on the chosen algorithm, you can specify:
+
+- **HMAC**-based secret keys (`-Secret`)
+- **Certificate**-based parameters (`-Certificate`, `-CertificateThumbprint`, `-CertificateName`, `-X509Certificate`, `-SelfSigned`)
+- The **RSA padding scheme** (`-RsaPaddingScheme`)
+- The **JWT verification mode** (`-JwtVerificationMode`)
+
+### JwtVerificationMode
+
+Defines how aggressively JWT claims should be checked:
+
+- **Strict**: Requires all standard claims:
+ - `exp` (Expiration Time)
+ - `nbf` (Not Before)
+ - `iat` (Issued At)
+ - `iss` (Issuer)
+ - `aud` (Audience)
+ - `jti` (JWT ID)
+
+- **Moderate**: Allows missing `iss` (Issuer) and `aud` (Audience) but still checks expiration (`exp`).
+- **Lenient**: Ignores missing `iss` and `aud`, only verifies expiration (`exp`), not-before (`nbf`), and issued-at (`iat`).
+
+### HMAC Example
+
+Here’s an example using **HMAC** (HS256) JWT validation:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -Algorithm 'HS256' `
+ -Secret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) `
+ -JwtVerificationMode 'Strict' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate and decode JWT, then extract user details
+
+ return @{ User = $user }
+ }
+}
+```
+
+### Certificate-Based Example
+
+For **RSA/ECDSA** JWT validation, you can specify a **certificate** or **thumbprint** instead of a secret key. Pode will infer the appropriate signing algorithms (e.g., RS256, ES256) from the certificate. For instance, using a local **PFX** certificate file:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -Algorithm 'RS256' `
+ -Certificate "C:\path\to\cert.pfx" `
+ -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) `
+ -JwtVerificationMode 'Moderate' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+### Self-Signed Certificate Example
+
+For testing purposes or internal deployments, you can use the **`-SelfSigned`** parameter, which automatically generates an **ephemeral self-signed ECDSA certificate** (ES384) for JWT signing. This avoids the need to manually create and manage certificate files.
+
+#### Example:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -SelfSigned |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+This is equivalent to manually generating a self-signed ECDSA certificate and passing it via `-X509Certificate`:
+
+```powershell
+Start-PodeServer {
+ $x509Certificate = New-PodeSelfSignedCertificate `
+ -CommonName 'JWT Signing Certificate' `
+ -KeyType ECDSA `
+ -KeyLength 384 `
+ -CertificatePurpose CodeSigning `
+ -Ephemeral
+
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -X509Certificate $x509Certificate |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+Using `-SelfSigned` simplifies setup by automatically handling certificate creation and disposal, making it a convenient choice for local development and testing scenarios.
+
+## Scope Validation
-You can also optionally return a `Scope` property alongside the `User`. If you specify any scopes with [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) then it will be validated in the Bearer's post validator - a 403 will be returned if the scope is invalid.
+You can optionally include `-Scope` when creating the scheme. Pode will validate any returned `Scope` from your auth **ScriptBlock** against the scheme’s required scopes. If the scope is invalid, Pode will return 403 (Forbidden).
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Bearer -Scope 'write' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthBearerScheme -Scope 'write' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($token)
# check if the token is valid, and get user
@@ -38,11 +171,13 @@ Start-PodeServer {
}
```
+
+
## Middleware
-Once configured you can start using Bearer authentication to validate incoming requests. You can either configure the validation to happen on every Route as global Middleware, or as custom Route Middleware.
+Once configured, you can instruct Pode to validate every request with Bearer authentication by using **Global Middleware**, or you can require it on individual Routes.
-The following will use Bearer authentication to validate every request on every Route:
+**Global Middleware Example** – Validate **every** incoming request:
```powershell
Start-PodeServer {
@@ -50,7 +185,7 @@ Start-PodeServer {
}
```
-Whereas the following example will use Bearer authentication to only validate requests on specific a Route:
+**Route-Specific Example** – Validate only on a certain Route:
```powershell
Start-PodeServer {
@@ -60,46 +195,42 @@ Start-PodeServer {
}
```
-## JWT
-
-You can supply a JWT using Bearer authentication, for more details [see here](../JWT).
-
## Full Example
-The following full example of Bearer authentication will setup and configure authentication, validate the token, and then validate on a specific Route:
+Below is a complete example demonstrating Bearer authentication with JWT. It configures a server, sets up JWT validation with a shared secret, and validates requests on one route (`/cpu`) while leaving another (`/memory`) open:
```powershell
Start-PodeServer {
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
- # setup bearer authentication to validate a user
- New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
- param($token)
-
- # here you'd check a real storage, this is just for example
- if ($token -eq 'test-token') {
- return @{
- User = @{
- 'ID' ='M0R7Y302'
- 'Name' = 'Morty'
- 'Type' = 'Human'
+ # Setup Bearer authentication to validate a user via JWT
+ New-PodeAuthBearerScheme -AsJWT -Algorithm 'HS256' -Secret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) -JwtVerificationMode 'Lenient' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Example: in real usage, you would decode/verify the JWT fully
+ if ($token -eq 'test-token') {
+ return @{
+ User = @{
+ 'ID' = 'M0R7Y302'
+ 'Name' = 'Morty'
+ 'Type' = 'Human'
+ }
+ # Scope = 'read'
}
- # Scope = 'read'
}
- }
- # authentication failed
- return $null
- }
+ # authentication failed
+ return $null
+ }
- # check the request on this route against the authentication
+ # Validate against the authentication on this route
Add-PodeRoute -Method Get -Path '/cpu' -Authentication 'Authenticate' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'cpu' = 82 }
}
- # this route will not be validated against the authentication
+ # Open route, no auth required
Add-PodeRoute -Method Get -Path '/memory' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'memory' = 14 }
}
}
-```
diff --git a/docs/Tutorials/Authentication/Methods/Digest.md b/docs/Tutorials/Authentication/Methods/Digest.md
index 95e1e9570..c9cfca84e 100644
--- a/docs/Tutorials/Authentication/Methods/Digest.md
+++ b/docs/Tutorials/Authentication/Methods/Digest.md
@@ -1,14 +1,16 @@
# Digest
-Digest authentication lets you authenticate a user without actually sending the password to the server. Instead the a request is made to the server, and a challenge issued back for credentials. The authentication is then done by comparing hashes generated by the client and server using the user's password as a secret key.
+Digest authentication allows secure user authentication without sending the password to the server. Instead, the client receives a challenge from the server and responds with a hash-based authentication response. The server then verifies the hash using the stored password as a secret key.
+
+**Pode's Digest Authentication is compliant with [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616)**, ensuring compatibility with standard authentication mechanisms.
## Setup
-To setup and start using Digest authentication in Pode you use the `New-PodeAuthScheme -Digest` function, and then pipe this into the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function. The parameters supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock are the `$username`, and a HashTable containing the parameters from the Authorization header:
+To configure Digest authentication in Pode, use the `New-PodeAuthScheme -Digest` function and pass it to [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameters supplied to the `Add-PodeAuth` function's ScriptBlock include the `$username` and a hashtable containing the authentication parameters extracted from the `Authorization` header:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthScheme -Digest -Algorithm "SHA-256" -QualityOfProtection "auth-int" | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($username, $params)
# check if the user is valid
@@ -18,28 +20,70 @@ Start-PodeServer {
}
```
-Unlike other forms of authentication where you only need return the User on success. Digest requires you to also return the Password of the user as a separate property. This password is what is used as the secret key to generate the client's response hash, and allows the server to re-generate the hash for validation. (Not returning the password will result in an HTTP 401 challenge response).
+Unlike other authentication methods, where only a user object is returned on success, Digest authentication **requires returning the password** (or hash) as a separate property. The password acts as the secret key to regenerate the client’s hash response for verification. Not returning the password results in an HTTP `401 Unauthorized` challenge response.
+
+### RFC 7616 Compliance
+
+Pode’s Digest authentication implementation adheres to **RFC 7616**, ensuring:
+
+- Use of **nonce-based challenge-response authentication**
+- Support for **multiple hashing algorithms** beyond MD5
+- Support for **Quality of Protection (QoP)**, including `auth` and `auth-int`
+- Correct formatting of **WWW-Authenticate** headers on authentication failure
+
+!!! note
+ SHA-384 is **not** part of RFC 7616 but has been added for consistency with other modern cryptographic algorithms and to provide additional security options.
+
+### Supported Algorithms
+
+Pode now supports multiple algorithms for Digest authentication. The `-Algorithm` parameter allows selecting one or more of the following:
+
+- `MD5`
+- `SHA-1`
+- `SHA-256`
+- `SHA-384`
+- `SHA-512`
+- `SHA-512/256`
+
+Pode automatically includes all supported algorithms in the `WWW-Authenticate` challenge header, allowing clients to select the strongest available option.
+
+### Quality of Protection (QoP)
+
+The `-QualityOfProtection` parameter (`-qop`) allows choosing between:
+
+- `"auth"` (authentication only)
+- `"auth-int"` (authentication with message integrity protection)
+
+If `auth-int` is used, the client includes a hash of the request body in the authentication response, ensuring the request content has not been altered.
-By default, Pode will check if the Request's header contains an `Authorization` key, and whether the value of that key starts with `Digest` tag. The `New-PodeAuthScheme -Digest` function can be supplied parameters to customise the tag using `-HeaderTag`. Pode will also gather the rest of the parameters in the header such as the Nonce, NonceCount, etc. An HTTP 401 challenge will be sent back if the Authorization header is invalid.
+## Handling Authentication Requests
-The HashTable of parameters sent to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock are the following:
+By default, Pode checks if the request contains an `Authorization` header with the `Digest` scheme. The `New-PodeAuthScheme -Digest` function can be customized using the `-HeaderTag` parameter to modify the tag used in the request header. Pode also extracts all required parameters from the header, including the nonce, nonce count, and QoP options.
-| Parameter | Description |
-| --------- | ----------- |
-| cnonce | A nonce value generated by the client |
-| nc | The count of time the client has used the server nonce |
-| nonce | A nonce value generated by the server |
-| qop | Fixed to 'auth' |
-| realm | The realm description from the server's HTTP 401 challenge |
-| response | The generated hash value of all these parameters from the client |
-| uri | The URI path that needs authentication |
-| username | The username of the user that needs authenticating |
+If the `Authorization` header is missing or invalid, Pode returns an HTTP `401 Unauthorized` response with a `WWW-Authenticate` challenge.
+
+### Digest Authentication Parameters
+
+The hashtable of parameters passed to the `Add-PodeAuth` function’s ScriptBlock includes the following:
+
+| Parameter | Description |
+|------------|--------------|
+| `cnonce` | A nonce value generated by the client. |
+| `nc` | The count of times the client has used the server nonce. |
+| `nonce` | A nonce value generated by the server. |
+| `qop` | The quality of protection requested (`auth` or `auth-int`). |
+| `realm` | The authentication realm from the server's challenge. |
+| `response` | The hash generated by the client for authentication. |
+| `uri` | The URI path that requires authentication. |
+| `username` | The username provided for authentication. |
## Middleware
-Once configured you can start using Digest authentication to validate incoming Requests. You can either configure the validation to happen on every Route as global Middleware, or as custom Route Middleware.
+Digest authentication can be applied globally to all requests using `Add-PodeAuthMiddleware` or to specific routes via the `-Authentication` parameter.
-The following will use Digest authentication to validate every request on every Route:
+### Global Middleware
+
+To apply Digest authentication globally to all routes:
```powershell
Start-PodeServer {
@@ -47,7 +91,9 @@ Start-PodeServer {
}
```
-Whereas the following example will use Digest authentication to only validate requests on specific a Route:
+### Per-Route Middleware
+
+To enforce Digest authentication only on specific routes:
```powershell
Start-PodeServer {
@@ -59,40 +105,116 @@ Start-PodeServer {
## Full Example
-The following full example of Digest authentication will setup and configure authentication, validate that a user's username is valid, and then validate on a specific Route:
+The following example sets up Digest authentication with SHA-256 and `auth-int`, validates a user, and applies authentication to a specific route:
```powershell
Start-PodeServer {
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
- # setup digest authentication to validate a user
- New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ # Setup Digest authentication with SHA-256 and auth-int
+ New-PodeAuthScheme -Digest -Algorithm "SHA-256" -QualityOfProtection "auth-int" | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($username, $params)
- # here you'd check a real user storage, this is just for example
+ # Example user validation
if ($username -eq 'morty') {
return @{
User = @{
- 'ID' ='M0R7Y302'
- 'Name' = 'Morty';
- 'Type' = 'Human';
+ 'ID' = 'M0R7Y302'
+ 'Name' = 'Morty'
+ 'Type' = 'Human'
}
Password = 'pickle'
}
}
- # authentication failed
+ # Authentication failed
return $null
}
- # check the request on this route against the authentication
+ # Protect the /cpu route with Digest authentication
Add-PodeRoute -Method Get -Path '/cpu' -Authentication 'Authenticate' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'cpu' = 82 }
}
- # this route will not be validated against the authentication
+ # The /memory route is accessible without authentication
Add-PodeRoute -Method Get -Path '/memory' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'memory' = 14 }
}
}
```
+
+### **Windows-Specific Limitations and the Pode Client Module**
+
+Windows' built-in Digest authentication has several **critical limitations** that restrict its compatibility with modern security practices:
+
+- **Limited to MD5:** Windows does not support stronger hashing algorithms like SHA-256 or SHA-512.
+- **No Support for `auth-int`:** Integrity protection (`auth-int`) is not available, making it less secure.
+- **Fails with Multiple Algorithms:** If the `WWW-Authenticate` header lists multiple algorithms, Windows' built-in implementation fails to negotiate properly.
+- **Lack of Algorithm Negotiation:** Windows cannot automatically select the strongest supported algorithm from a list.
+
+### **Overcoming Windows Limitations with the Pode Client Module**
+
+To bypass these **Windows client limitations**, Pode provides a custom **client module** that supports full RFC 7616-compliant Digest authentication. This module allows PowerShell scripts to authenticate using modern algorithms, multiple QoP modes, and cross-platform compatibility.
+
+The **client module** is available under:
+
+```powershell
+Import-Module ./examples/Authentication/Modules/Invoke-Digest.psm1
+```
+
+By using this module, you can perform **secure Digest authentication** in PowerShell, even on Windows, without being restricted to **MD5-only authentication**.
+
+The module includes the following functions:
+
+### **Invoke-WebRequestDigest**
+
+A replacement for `Invoke-WebRequest` that supports Digest authentication.
+
+#### **Example Usage**
+
+```powershell
+Import-Module './examples/Authentication/Modules/Invoke-Digest.psm1'
+
+# Define the URI and credentials
+$uri = 'http://localhost:8081/users'
+$username = 'morty'
+$password = 'pickle'
+
+# Convert the password to a SecureString and create a credential object
+$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+$credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+# Make a GET request using Digest authentication
+$response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+
+# Display response headers and content
+$response.Headers | Format-List
+Write-Output $response.Content
+```
+
+---
+
+### **Invoke-RestMethodDigest**
+
+A replacement for `Invoke-RestMethod` that supports Digest authentication.
+
+#### **Example Usage**
+
+```powershell
+Import-Module './examples/Authentication/Modules/Invoke-Digest.psm1'
+
+# Define the URI and credentials
+$uri = 'http://localhost:8081/users'
+$username = 'morty'
+$password = 'pickle'
+
+# Convert the password to a SecureString and create a credential object
+$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+$credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+# Make a GET request and automatically parse JSON response
+$response = Invoke-RestMethodDigest -Uri $uri -Method 'GET' -Credential $credential
+
+# Output the parsed response
+$response
+```
\ No newline at end of file
diff --git a/docs/Tutorials/Authentication/Methods/JWT.md b/docs/Tutorials/Authentication/Methods/JWT.md
index 9a24416ff..3e2453921 100644
--- a/docs/Tutorials/Authentication/Methods/JWT.md
+++ b/docs/Tutorials/Authentication/Methods/JWT.md
@@ -1,112 +1,299 @@
-# JWT
+# Create a JWT
-Pode has inbuilt JWT parsing for either [Bearer](../Bearer) or [API Key](../ApiKey) authentications. Pode will attempt to validate and parse the token/key as a JWT, and if successful the JWT's payload will be passed as the parameter to [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth), instead of the token/key.
+Pode provides a [`ConvertTo-PodeJwt`](../../../../Functions/Authentication/ConvertTo-PodeJwt) command that builds and signs a JWT for you. You can provide:
-For more information on JWTs, see the [official website](https://jwt.io).
+- **`-Header`**: A hashtable defining fields like `alg`, `typ`, etc.
+- **`-Payload`**: A hashtable for JWT claims (e.g., `sub`, `exp`, `nbf`, and other custom claims).
+- **`-Secret`**/**`-Certificate`**/**`-CertificateThumbprint`**, etc.: If you want to sign the JWT (for HS*, RS*, ES*, PS* algorithms).
+- **`-IgnoreSignature`**: If you want a token with no signature (alg = none).
+- **`-Authentication`**: To reference an existing named authentication scheme, automatically pulling its parameters (algorithm, secret, certificate, etc.) so the generated JWT is recognized by that scheme.
-## Setup
+## Customizing the Header/Payload
-To start using JWT authentication, you can supply the `-AsJWT` switch with either the `-Bearer` or `-ApiKey` switch on [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme). You can also supply an optional `-Secret` that the JWT signature uses so Pode can validate the JWT:
+When generating a JWT using **`ConvertTo-PodeJwt`**, you can specify parameters that either:
+
+1. **Manually** define the header/payload using `-Header` and `-Payload`, or
+2. **Automatically** set standard claims via shortcut parameters like `-Expiration`, `-Issuer`, `-Audience`, etc.
+
+You can also combine these approaches—Pode merges everything into the final token unless you use **`-NoStandardClaims`** to disable automatic claims.
+
+Below are the **primary parameters** you can pass to **`ConvertTo-PodeJwt`**:
+
+### Header and Payload
+
+- **`-Header`**: A hashtable for JWT header fields (e.g., `alg`, `typ`).
+- **`-Payload`**: A hashtable for arbitrary/custom claims (e.g., `role`, `scope`, etc.).
+- **`-NoStandardClaims`**: If specified, **no** standard claims are auto-generated (e.g., no `exp`, `nbf`, `iat`, etc.). This is useful if you want full control over claims in `-Payload`.
+
+#### Standard Claims Parameters
+
+These automatically populate or override common JWT claims:
+
+- **`-Expiration`** (`int`, default 3600)
+ - Sets the `exp` (expiration time) to the current time + `Expiration` (in seconds).
+ - For example, **3600** means `exp` = now + 1 hour.
+
+- **`-NotBefore`** (`int`, default 0)
+ - Sets the `nbf` (not-before) to current time + `NotBefore` (in seconds).
+ - **0** = immediate validity; **60** = valid 1 minute from now, etc.
+
+- **`-IssuedAt`** (`int`, default 0)
+ - Sets the `iat` (issued-at) time.
+ - **0** means “use current time.” Any other integer is added to the current time as seconds.
+
+- **`-Issuer`** (`string`)
+ - Sets the `iss` (issuer) claim, e.g. `"auth.example.com"`.
+
+- **`-Subject`** (`string`)
+ - Sets the `sub` (subject) claim, e.g. `"user123"`.
+
+- **`-Audience`** (`string`)
+ - Sets the `aud` (audience) claim, e.g. `"myapi.example.com"`.
+
+- **`-JwtId`** (`string`)
+ - Sets the `jti` (JWT ID) claim, a unique identifier for the token.
+
+If you **also** supply the same claims in your `-Payload` hashtable, Pode typically defers to your explicit claim unless **`-NoStandardClaims`** is omitted, in which case these parameters can overwrite the payload-based claims.
+
+---
+
+### Example Usage
+
+Below is an example that **automatically** sets standard claims for expiration (1 hour from now), not-before (starts immediately), and an issuer, while also providing a custom header/payload:
```powershell
-# jwt with no signature:
-New-PodeAuthScheme -Bearer -AsJWT | Add-PodeAuth -Name 'Example' -Sessionless -ScriptBlock {
- param($payload)
+$header = @{
+ alg = 'HS256'
+ typ = 'JWT'
}
-# jwt with signature, signed with secret "abc":
-New-PodeAuthScheme -ApiKey -AsJWT -Secret 'abc' | Add-PodeAuth -Name 'Example' -Sessionless -ScriptBlock {
- param($payload)
+$payload = @{
+ role = 'admin'
+ customClaim = 'someValue'
}
+
+$jwt = ConvertTo-PodeJwt `
+ -Header $header `
+ -Payload $payload `
+ -Secret 'SuperSecretKey' `
+ -Expiration 3600 `
+ -NotBefore 0 `
+ -Issuer 'auth.example.com' `
+ -Subject 'user123' `
+ -Audience 'myapi.example.com' `
+ -JwtId 'unique-token-id'
+
+Write-PodeJsonResponse -Value @{ token = $jwt }
```
-The `$payload` will be a PSCustomObject of the converted JSON payload. For example, sending the following unsigned JWT in a request:
+This produces a JWT that includes:
-```plain
-eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.
-```
+- A header with `alg = HS256`, `typ = JWT`.
+- Standard claims: `exp`, `nbf`, `iat`, `iss`, `sub`, `aud`, `jti`.
+- Custom claims: `role`, `customClaim`.
-would produce a payload of:
+If you **don’t** want Pode to generate any standard claims at all (perhaps you want to define everything in `-Payload` yourself), include **`-NoStandardClaims`**:
-```plain
-sub: 123
-username: morty
+```powershell
+$jwt = ConvertTo-PodeJwt -NoStandardClaims -Payload @{ sub='user123'; customKey='abc' } -Secret 'SuperSecretKey'
```
-### Algorithms
+No `exp`, `nbf`, or `iat` will be automatically added.
-Pode supports the following algorithms for JWT signatures:
+Similarly, if you have a named scheme:
-* None
-* HS256
-* HS384
-* HS512
+```powershell
+New-PodeAuthBearerScheme -AsJWT -Algorithm 'RS256' -Certificate 'C:\cert.pfx' -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) |
+ Add-PodeAuth -Name 'ExampleApiKeyCert'
-For `none`, Pode expects there to be no signature with the JWT. For other algorithms, a `-Secret` is required, and a signature must be supplied with the JWT in requests.
+Add-PodeRoute -Method Post -Path '/login' -ScriptBlock {
+ $jwt = ConvertTo-PodeJwt `
+ -Authentication 'ExampleApiKeyCert' `
+ -Issuer 'auth.example.com' `
+ -Expiration 3600 `
+ -Subject 'user123'
-### Payload
+ Write-PodeJsonResponse -Value @{ token = $jwt }
+}
+```
-If the payload of the JWT contains a expiry (`exp`) or a not before (`nbf`) timestamp, Pode will validate it and return a 400 if the JWT is expired/not started.
+Here, Pode automatically applies the RS256 certificate from **`ExampleApiKeyCert`** and merges your standard-claims parameters, producing a token recognized by that same scheme upon verification.
-## Usage
+## Using `-Authentication`
-To send the JWT in a request, the JWT should be sent in place of where the usual bearer token/API key would have been. For example, for bearer it would be in the Authorization header:
+If you have already set up an authentication scheme, for instance:
-```plain
-Authorization: Bearer
+```powershell
+New-PodeAuthBearerScheme -AsJWT -Algorithm 'RS256' -Certificate 'C:\path\to\cert.pfx' -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) |
+ Add-PodeAuth -Name 'ExampleApiKeyCert'
```
-and for API keys, it would be in the location defined (header, cookie, or query string). For example, in the X-API-KEY header:
+then you can **reuse** this scheme’s configuration when creating a token by calling:
-```plain
-X-API-KEY:
-```
+```powershell
+$jwt = ConvertTo-PodeJwt -Authentication 'ExampleApiKeyCert'
-## Create JWT
+# e.g., return the new JWT to a client
+Write-PodeJsonResponse -StatusCode 200 -Value @{ jwt_token = $jwt }
+```
-Pode has a simple [`ConvertTo-PodeJwt`](../../../../Functions/Authentication/ConvertTo-PodeJwt) that will build a JWT for you. It accepts a hashtable for `-Header` and `-Payload`, as well as an optional `-Secret`.
+Pode automatically looks up the **`ExampleApiKeyCert`** auth scheme, retrieves its signing algorithm and key/certificate, and uses those to generate a valid JWT. This ensures that the JWT you create can later be **decoded and verified** by the same auth scheme without having to re-specify all parameters (secret, certificate, etc.).
-The function will run some simple validation, and them build the JWT for you.
+### Example
-For example:
+Below is a short example of how you might implement a **login** route that returns a signed JWT:
```powershell
-$header = @{
- alg = 'hs256'
- typ = 'JWT'
+Add-PodeRoute -Method Post -Path '/user/login' -ScriptBlock {
+ param()
+
+ # In a real scenario, you'd validate the incoming credentials from $WebEvent.data
+ $username = $WebEvent.Data['username']
+ $password = $WebEvent.Data['password']
+
+ # If valid, generate a JWT that matches the 'ExampleApiKeyCert' scheme
+ $jwt = ConvertTo-PodeJwt -Authentication 'ExampleApiKeyCert'
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{ jwt_token = $jwt }
}
+```
-$payload = @{
- sub = '123'
- name = 'John Doe'
- exp = ([System.DateTimeOffset]::Now.AddDays(1).ToUnixTimeSeconds())
+In this example, the **`-Authentication`** parameter ensures Pode uses the RS256 certificate-based configuration already defined by the `ExampleApiKeyCert` auth scheme, producing a token that is verifiable by that same scheme on future requests.
+
+
+Below is an **updated JWT Lifecycle guide** for Pode, clarifying that **Pode automatically validates the token** when you attach `-Authentication` to a route, and that **`ConvertFrom-PodeJwt`** is generally used for **inspecting** or **debugging** token contents.
+
+
+## Managing the JWT Lifecycle in Pode
+
+In many scenarios, you need more than just generating JWTs—you also need endpoints or logic for **renewing** and **inspecting** tokens. Pode’s built-in commands and authentication features enable these patterns quickly:
+
+1. **Creating a JWT**: Use [`ConvertTo-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/ConvertTo-PodeJwt.ps1) to build and sign a JWT.
+2. **Automatic Validation**: Rely on Pode’s bearer auth if a route uses `-Authentication 'YourBearerScheme'`.
+3. **Decoding/Inspecting a JWT**: Use `ConvertFrom-PodeJwt` if you want to explicitly decode the JWT for debugging or extracting claims.
+4. **Renewing/Extending a JWT**: Use `Update-PodeJwt` to reissue a token with a new expiration.
+
+## 1. Creating a JWT
+
+See the [“Create a JWT” guide](#create-a-jwt) for details on using `ConvertTo-PodeJwt`. You can:
+
+- Define a scheme in Pode (e.g., `Bearer_JWT_ES512`) that holds your algorithm and certificates/secrets.
+- Generate tokens by referencing `-Authentication 'Bearer_JWT_ES512'`.
+- Optionally set custom claims, expiration, issuer, etc.
+
+This creation step often happens inside a **login** route, as shown in the example below:
+
+```powershell
+function Test-User {
+ param($username, $password)
+ if ($username -eq 'morty' -and $password -eq 'pickle') {
+ return @{
+ Id = 'M0R7Y302'
+ Username = 'morty.smith'
+ Name = 'Morty Smith'
+ Groups = 'Domain Users'
+ }
+ }
+ throw 'Invalid credentials'
}
-ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'abc'
+Add-PodeRoute -Method Post -Path '/auth/login' -ScriptBlock {
+ try {
+ $username = $WebEvent.Data.username
+ $password = $WebEvent.Data.password
+ $user = Test-User $username $password # Validate credentials in some real store
+
+ $payload = @{
+ sub = $user.Id
+ name = $user.Name
+ # ... more custom claims ...
+ }
+
+ # Generate JWT recognized by the scheme 'Bearer_JWT_ES512'
+ $jwt = ConvertTo-PodeJwt -Payload $payload -Authentication 'Bearer_JWT_ES512' -Expiration 600
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ success = $true
+ user = $user
+ jwt = $jwt
+ }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid credentials' }
+ }
+}
```
-This return the following JWT:
+## 2. Automatic Validation
+
+Once you have a named bearer scheme (e.g., `Bearer_JWT_ES512`), **any** route that includes `-Authentication 'Bearer_JWT_ES512'` is automatically protected. Pode will:
+
+- Extract the JWT from the HTTP `Authorization` header (or another location if specified).
+- Decode and verify the signature based on the scheme’s configuration.
+- Reject the request if invalid; otherwise, set `$WebEvent.Auth.User` with any relevant user/claims data.
-```plain
-eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY
+```powershell
+Add-PodeRoute -Method Get -Path '/secure' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ # If we get here, the token is valid
+ $user = $WebEvent.Auth.User
+ Write-PodeJsonResponse -Value @{ user = $user; message = 'Welcome!' }
+}
```
-## Parse JWT
+No need to manually call `ConvertFrom-PodeJwt`—Pode handles validation behind the scenes.
-Pode has a [`ConvertFrom-PodeJwt`](../../../../Functions/Authentication/ConvertFrom-PodeJwt) that can be used to parse a valid JWT. Only the algorithms at the top of this page are supported for verifying the signature. You can skip signature verification by passing `-IgnoreSignature`. On success, the payload of the JWT is returned.
+## 3. Decoding/Inspecting a JWT
-For example, if the created JWT was supplied:
+Sometimes you want to **inspect** a token or decode it for debugging. That’s where `ConvertFrom-PodeJwt` is handy. For example, you might have a route that **also** includes `-Authentication 'Bearer_JWT_ES512'` (so the user needs a valid token to get in), but within the route you call `ConvertFrom-PodeJwt` to see the raw contents or claims:
```powershell
-ConvertFrom-PodeJwt -Token 'eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY' -Secret 'abc'
+Add-PodeRoute -Method Post -Path '/auth/bearer/jwt/info' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ try {
+ # Although Pode already validated the token, we can decode it ourselves for debugging
+ $decoded = ConvertFrom-PodeJwt -Outputs 'Header,Payload,Signature' -HumanReadable
+ Write-PodeJsonResponse -Value $decoded
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+}
```
-then the following would be returned:
+This route returns the **header, payload, and signature** in JSON, with timestamps (like `exp`, `nbf`, `iat`) converted to human-readable dates.
+
+## 4. Renewing/Extending a JWT with `Update-PodeJwt`
+
+Use `Update-PodeJwt` to **extend** an existing token’s lifetime. Typically, you create a `/renew` endpoint:
```powershell
-@{
- sub = '123'
- name = 'John Doe'
- exp = 1636657408
+Add-PodeRoute -Method Post -Path '/auth/bearer/jwt/renew' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ try {
+ # Reads the current valid JWT, reissues it with a fresh 'exp' claim
+ $newToken = Update-PodeJwt
+ Write-PodeJsonResponse -StatusCode 200 -Value @{ success = $true; jwt = $newToken }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
}
```
+
+Pode fetches the token from `$WebEvent`, checks the original scheme (here, `Bearer_JWT_ES512`), and re-signs with updated expiration. The rest of the claims stay the same. The client can then discard the old token and use the newly returned token moving forward.
+
+---
+
+## Full Lifecycle Example
+
+**1.** **Login** (create token)
+**2.** **Make Authenticated Requests** (Pode automatically validates)
+**3.** **Renew** (use `Update-PodeJwt` if needed)
+**4.** **Debug** (optionally decode token with `ConvertFrom-PodeJwt`)
+
+This covers a typical JWT flow in Pode:
+
+- The user logs in at `/auth/login`, gets a JWT.
+- They pass that JWT in subsequent requests, which are auto-validated by `-Authentication 'Bearer_JWT_ES512'`.
+- If the token is about to expire, they can call `/auth/bearer/jwt/renew` to get a fresh one.
+- If you need to debug claims, you can build an endpoint that calls `ConvertFrom-PodeJwt` or look at `$WebEvent.Auth.User`.
+
+For more details, see the [Pode GitHub examples](https://github.com/Badgerati/Pode/tree/develop/examples/Authentication) or the relevant [`ConvertTo-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/ConvertTo-PodeJwt.ps1) and [`Update-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/Update-PodeJwt.ps1) source files.
\ No newline at end of file
diff --git a/docs/Tutorials/Basics.md b/docs/Tutorials/Basics.md
index 137aa8b29..f887da2e5 100644
--- a/docs/Tutorials/Basics.md
+++ b/docs/Tutorials/Basics.md
@@ -1,4 +1,5 @@
# Basics
+
!!! Warning
You can initiate only one server per PowerShell instance. To run multiple servers, start additional PowerShell, or pwsh, sessions. Each session can run its own server. This is fundamental to how Pode operates, so consider it when designing your scripts and infrastructure.
@@ -59,6 +60,7 @@ When you call [`Start-PodeServer`](../../Functions/Core/Start-PodeServer) direct
For example, the following is a file that contains the same scriptblock for the server at the top of this page. Following that are the two ways to run the server - the first is via another script, and the second is directly from the CLI:
* File.ps1
+
```powershell
{
# attach to port 8080 for http
@@ -70,12 +72,15 @@ For example, the following is a file that contains the same scriptblock for the
```
* Server.ps1 (start via script)
+
```powershell
Start-PodeServer -FilePath './File.ps1'
```
+
then use `./Server.ps1` on the CLI.
* CLI (start from CLI)
+
```powershell
PS> Start-PodeServer -FilePath './File.ps1'
```
diff --git a/docs/Tutorials/Certificates.md b/docs/Tutorials/Certificates.md
index be3a9e606..2aff12dae 100644
--- a/docs/Tutorials/Certificates.md
+++ b/docs/Tutorials/Certificates.md
@@ -1,121 +1,313 @@
# Certificates
-Pode has the ability to generate and bind self-signed certificates (for dev/testing), as well as the ability to bind existing certificates for HTTPS.
+Pode has the ability to generate and bind self-signed certificates (for dev/testing), as well as the ability to bind existing certificates for HTTPS or JWT.
-There are 8 ways to setup HTTPS on [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint):
+## Setting Up HTTPS in Pode
-1. Supplying just the `-Certificate`, which is the path to files such as a `.cer` or `.pem` file.
-2. Supplying both the `-Certificate` and `-CertificatePassword`, which is the path to a `.pfx` file and its password.
-3. Supplying both the `-Certificate` and `-CertificateKey`, which is the paths to certificate/key PEM file pairs.
-4. Supplying all of `-Certificate`, `-CertificateKey`, and `-CertificatePassword`, which is the paths to certificate/key PEM file pairs and the password for an encrypted key.
-5. Supplying a `-CertificateThumbprint` for a certificate installed at `Cert:\CurrentUser\My` on Windows.
-6. Supplying a `-CertificateName` for a certificate installed at `Cert:\CurrentUser\My` on Windows.
-7. Supplying `-X509Certificate` of type `X509Certificate`.
-8. Supplying the `-SelfSigned` switch, to generate a quick self-signed `X509Certificate`.
+Pode provides multiple ways to configure HTTPS on [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint):
-Note: for 5. and 6. you can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+- **File-based certificates:**
+ - `-Certificate`: Path to a `.cer` or `.pem` file.
+ - `-Certificate` with `-CertificatePassword`: Path to a `.pfx` file and its password.
+ - `-Certificate` with `-CertificateKey`: Paths to a certificate/key PEM file pair.
+ - `-Certificate`, `-CertificateKey`, and `-CertificatePassword`: Paths to an encrypted PEM file pair and its password.
-## Usage
+- **Windows Certificate Store:**
+ - `-CertificateThumbprint`: Uses a certificate installed at `Cert:\CurrentUser\My`.
+ - `-CertificateName`: Uses a certificate installed at `Cert:\CurrentUser\My` by name.
-### File
+- **X.509 Certificates:**
+ - `-X509Certificate`: Provides a certificate object of type `X509Certificate2`.
+ - `-SelfSigned`: Generates a quick self-signed `X509Certificate` for development.
-#### PFX
+- **Custom Certificate Management:**
+ - Pode’s built-in functions allow better control over certificate creation, import, and export.
-To bind a certificate PFX file, you use the `-Certificate` parameter, along with the `-CertificatePassword` parameter for the PFX certificate. The following example supplies the path to some `.pfx` to enable HTTPS support:
+## Generating a Self-Signed Certificate
+
+Pode provides the **`New-PodeSelfSignedCertificate`** function for creating self-signed X.509 certificates for development and testing purposes.
+
+### Features of `New-PodeSelfSignedCertificate`
+
+- ✅ Creates a **self-signed certificate** for HTTPS, JWT, or other use cases.
+- ✅ Supports **RSA** and **ECDSA** keys with configurable key sizes.
+- ✅ Can include **multiple Subject Alternative Names (SANs)** (e.g., `localhost`, IP addresses).
+- ✅ Allows setting **certificate purposes (ServerAuth, ClientAuth, etc.).**
+- ✅ Provides **ephemeral certificates** (in-memory only, not stored on disk).
+- ✅ Supports **exportable certificates** that can be saved for later use.
+
+### Usage Examples
+
+#### 1️⃣ Generate a Self-Signed Certificate for HTTPS
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pfx' -CertificatePassword 'Hunter2'
-}
+$cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
```
-#### PEM
+- Creates a **self-signed RSA certificate** for `example.com`.
+- The certificate is valid for HTTPS (`ServerAuth`).
-Pode has support for binding certificate/key PEM file pairs, on PowerShell 7+ this works out-of-the-box. However, for PowerShell 5/6 you are required to have OpenSSL installed.
+#### 2️⃣ Generate a Self-Signed Certificate for Local Development
-To bind a certificate/key PEM file pairs generated via LetsEncrypt or OpenSSL, you supply their paths to the `-Certificate` and `-CertificateKey` parameters.
+```powershell
+$cert = New-PodeSelfSignedCertificate -Loopback
+```
+
+- Automatically includes common loopback addresses:
+ - `127.0.0.1`
+ - `::1`
+ - `localhost`
+ - The machine’s hostname
+
+#### 3️⃣ Generate an ECDSA Certificate
-For example, if you generate the certificate/key using the following:
-```bash
-openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "test.local" -KeyType "ECDSA" -KeyLength 384
```
-Then your endpoint would be created as:
+- Creates a **self-signed ECDSA certificate** with a **384-bit** key.
+
+#### 4️⃣ Generate a Certificate That Exists Only in Memory (Ephemeral)
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "temp.local" -Ephemeral
+```
+
+- The private key is **not stored on disk**, and the certificate only exists **in-memory**.
+
+#### 5️⃣ Generate an Exportable Certificate
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "secureapp.local" -Exportable
+```
+
+- The certificate is **exportable** and can be saved as a `.pfx` or `.pem` file later.
+
+#### 6️⃣ Bind a Self-Signed Certificate to an HTTPS Endpoint
+
```powershell
Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pem' -CertificateKey './key.pem'
+ $cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
+ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
}
```
-However, if you generate the certificate/key and encrypt the key with a passphrase:
-```bash
-openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
+- Creates an HTTPS endpoint using a self-signed certificate.
+
+---
+
+## Generating a Certificate Signing Request (CSR)
+
+To generate a Certificate Signing Request (CSR) along with a private key, use the **`New-PodeCertificateRequest`** function:
+
+```powershell
+$csr = New-PodeCertificateRequest -DnsName "example.com" -CommonName "example.com" -KeyType "RSA" -KeyLength 2048
```
-Then the endpoint is created as follows:
+This will create a CSR file and a private key file in the current directory. You can specify additional parameters such as organization details and certificate purposes.
+
+### Using a CSR to Obtain a Certificate
+
+Once you have generated a CSR, you need to submit it to a **Certificate Authority (CA)** (such as Let's Encrypt, DigiCert, or a private CA) to receive a signed certificate. The process typically involves:
+
+1. Uploading or providing the `.csr` file to the CA.
+2. Completing domain validation steps (if required).
+3. Receiving the signed certificate (`.cer`, `.pem`, or `.pfx`) from the CA.
+4. Importing the signed certificate into Pode for use.
+
+Example: Importing the signed certificate after receiving it from the CA:
+
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pem' -CertificateKey './key.pem' -CertificatePassword ''
+$cert = Import-PodeCertificate -Path "C:\Certs\signed-cert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+if (-not (Test-PodeCertificate -Certificate $cert -ErrorAction Stop)) {
+ throw 'Certificate not valid'
}
```
-Depending on how you generated the certificate, especially if you used the above openssl, you might have to install the certificate to your local certificate store for it to be trusted. If you're using `Invoke-WebRequest` or `Invoke-RestMethod` on PowerShell 6+ you can supply the `-SkipCertificateCheck` switch.
+Alternatively, you can use:
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ErrorAction Stop | Out-Null
+```
+
+to force an exception if the certificate fails validation.
+**Refer to the `Test-PodeCertificate` documentation for any parameter details.**
+
+---
+
+## Exporting a Certificate
+
+Pode allows exporting certificates in various formats such as PFX and PEM. To export a certificate:
+
+```powershell
+Export-PodeCertificate -Certificate $cert -FilePath "C:\Certs\mycert" -Format "PFX" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+```
+
+or as a PEM file with a separate private key:
-### Thumbprint
+```powershell
+Export-PodeCertificate -Certificate $cert -FilePath "C:\Certs\mycert" -Format "PEM" -IncludePrivateKey
+```
+
+## Checking a Certificate’s Purpose
+
+A certificate's **purpose** is defined by its **Enhanced Key Usage (EKU)** attributes, which specify what the certificate is allowed to be used for. Common EKU values include:
-On Windows only, you can use a certificate that is installed at `Cert:\CurrentUser\My` using its thumbprint:
+- `ServerAuth` – Used for server authentication in HTTPS.
+- `ClientAuth` – Used for client authentication in mutual TLS setups.
+- `CodeSigning` – Used for digitally signing software and scripts.
+- `EmailSecurity` – Used for securing email communication.
+
+Pode can extract the EKU of a certificate to determine its intended purposes:
```powershell
+$purposes = Get-PodeCertificatePurpose -Certificate $cert
+$purposes
+```
+
+### Enforcing Certificate Purpose
+
+When Pode validates a certificate, it ensures that the certificate’s EKU matches the expected usage. If a certificate is used for an endpoint but lacks the required EKU (e.g., using a `CodeSigning` certificate for `ServerAuth`), Pode will reject the certificate and fail to bind it to the endpoint.
+
+For example, if an HTTPS endpoint is created, the certificate **must** include `ServerAuth`:
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
+
Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -CertificateThumbprint '2A623A8DC46ED42A13B27DD045BFC91FDDAEB957'
+ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
}
```
-Note: You can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+If the certificate lacks the correct EKU, Pode will return an error when attempting to bind it.
-### Name
+## Importing an Existing Certificate
-On Windows only, you can use a certificate that is installed at `Cert:\CurrentUser\My` using its subject name:
+To import a certificate from a file or the Windows certificate store:
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -CertificateName '*.example.com'
+$cert = Import-PodeCertificate -Path "C:\Certs\mycert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+```
+
+If you import a certificate without validating it, you should then call **`Test-PodeCertificate`** to verify that the certificate is valid:
+
+```powershell
+if (-not (Test-PodeCertificate -Certificate $cert -ErrorAction Stop)) {
+ throw 'Certificate not valid'
}
```
-Note: You can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+Alternatively, you can force an exception by piping the output:
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ErrorAction Stop | Out-Null
+```
+
+Refer to the **Test-PodeCertificate** section below for more details on all available parameters.
+
+---
+
+## Testing a Certificate’s Validity
+
+Pode provides the **`Test-PodeCertificate`** function to validate an **X.509 certificate** and ensure it meets security and usage requirements.
-### X509
+### Features of `Test-PodeCertificate`
-The following will instead create an X509Certificate, and pass that to the endpoint instead:
+- ✅ Checks if the certificate is **within its validity period** (`NotBefore` and `NotAfter`).
+- ✅ **Builds the certificate chain** to verify its trust.
+- ✅ Supports **online and offline revocation checking** (OCSP/CRL).
+- ✅ Allows **optional enforcement of strong cryptographic algorithms**.
+- ✅ Provides an option to **reject self-signed certificates**.
+- ✅ Optionally checks that the certificate’s Enhanced Key Usage (EKU) matches an **ExpectedPurpose** (with an optional **Strict** mode).
+
+### Usage Examples
+
+#### Basic Certificate Validation
```powershell
-$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new('./certs/example.cer')
-Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
+Test-PodeCertificate -Certificate $cert
```
-### Self-Signed
+- Checks if the certificate is currently valid.
+- Does **not** check revocation status.
-If you are developing/testing a site on HTTPS then Pode can generate and bind quick self-signed certificates. To do this you can pass the `-SelfSigned` switch:
+#### Validate Certificate with Online Revocation Checking
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned
-}
+Test-PodeCertificate -Certificate $cert -CheckRevocation
+```
+
+- Uses **OCSP/CRL lookup** to check if the certificate is revoked.
+
+#### Validate Certificate with Offline (Cached CRL) Revocation Check
+
+```powershell
+Test-PodeCertificate -Certificate $cert -CheckRevocation -OfflineRevocation
```
-You might get a warning in the browser about the certificate, and this is fine. If you're using `Invoke-WebRequest` or `Invoke-RestMethod` on PowerShell 6+ you can supply the `-SkipCertificateCheck` switch.
+- Uses **only locally cached CRLs**, making it suitable for air-gapped environments.
-## SSL Protocols
+#### Allow Certificates with Weak Algorithms
-The default allowed SSL protocols are SSL3 and TLS1.2 (or just TLS1.2 on MacOS), but you can change these to any of: SSL2, SSL3, TLS, TLS11, TLS12, TLS13. This is specified in your `server.psd1` configuration file:
+```powershell
+Test-PodeCertificate -Certificate $cert -AllowWeakAlgorithms
+```
+
+- Allows the use of certificates with **SHA1, MD5, or RSA-1024**.
+
+#### Reject Self-Signed Certificates
+
+```powershell
+Test-PodeCertificate -Certificate $cert -DenySelfSigned
+```
+
+- Fails validation if the certificate **is self-signed**.
+
+#### Enforce Expected Certificate Purpose
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ExpectedPurpose CodeSigning -Strict
+```
+
+- Validates that the certificate is explicitly authorized for **CodeSigning**.
+- In strict mode, if any unknown EKUs are present, the validation fails.
+
+---
+
+## Using Certificates for JWT Authentication
+
+Pode supports using X.509 certificates for JWT authentication. You can specify a certificate for signing and verifying JWTs by providing `-X509Certificate` when creating a bearer authentication scheme:
```powershell
-@{
- Server = @{
- Ssl= @{
- Protocols = @('TLS', 'TLS11', 'TLS12')
- }
+$cert = Import-PodeCertificate -Path "C:\Certs\jwt-signing-cert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -X509Certificate $cert |
+ Add-PodeAuth -Name 'JWTAuth' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Validate and extract user details
+ return @{ User = $user }
}
}
```
+
+Alternatively, you can use a self-signed certificate for development and testing:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -SelfSigned |
+ Add-PodeAuth -Name 'JWTAuth' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Validate and extract user details
+ return @{ User = $user }
+ }
+}
+```
+
+Using certificates for JWT authentication provides enhanced security by enabling asymmetric signing (RSA/ECDSA) rather than using a shared secret.
diff --git a/docs/Tutorials/Ssl.md b/docs/Tutorials/Ssl.md
new file mode 100644
index 000000000..1d65ad098
--- /dev/null
+++ b/docs/Tutorials/Ssl.md
@@ -0,0 +1,85 @@
+# SSL Protocols
+
+By default, the server chooses the allowed SSL/TLS protocols based on the operating system’s native support.
+
+For example, on Windows 11 and Windows Server 2022 only TLS 1.2 and TLS 1.3 are enabled, while older systems (such as Windows Vista/Server 2008) allow SSL 2.0 and SSL 3.0. This behavior follows the table below:
+
+| Operating System | SSL 2.0 | SSL 3.0 | TLS 1.0 | TLS 1.1 | TLS 1.2 | TLS 1.3 |
+|------------------------------------|---------------------|---------------------|---------------------|---------------------|--------------------|--------------------|
+| Windows Vista / Server 2008 | Enabled | Enabled | Not Supported | Not Supported | Not Supported | Not Supported |
+| Windows 7 / Server 2008 R2 | Enabled | Enabled | Disabled | Disabled | Disabled | Not Supported |
+| Windows 8 / Server 2012 | Disabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Not Supported |
+| Windows 10 (Build 20170 and later) | No | Disabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Enabled by Default |
+| Windows 11 / Server 2022 | No | Disabled by Default | Disabled by Default | Disabled by Default | Enabled by Default | Enabled by Default |
+| macOS 10.8 - 10.10 | No | Yes | Yes | Yes | Yes | No |
+| macOS 10.11 | No | No | Yes | Yes | Yes | No |
+| macOS 10.13 and later | No | No | Yes | Yes | Yes | Yes |
+| Linux (OpenSSL 1.0.1 - 1.0.1f) | No | Yes | Yes | Yes | Yes | No |
+| Linux (OpenSSL 1.0.1g and later) | No | No | Yes | Yes | Yes | No |
+| Linux (OpenSSL 1.1.1 and later) | No | No | Yes | Yes | Yes | Yes |
+
+**Notes:**
+
+- **Windows Operating Systems:**
+ - TLS 1.3 is supported starting from Windows 10 Build 20170 and Windows Server 2022.
+ - Earlier versions (like Windows 7 and Windows Server 2008 R2) support up to TLS 1.2, but may require manual configuration to enable it.
+
+- **macOS:**
+ - TLS 1.3 support begins with macOS 10.13.
+
+- **Linux:**
+ - The supported SSL/TLS protocols on Linux systems depend on the version of OpenSSL installed:
+ - OpenSSL versions 1.0.1 to 1.0.1f support up to TLS 1.2, with SSL 3.0 enabled by default.
+ - OpenSSL version 1.0.1g and later disable SSL 3.0 by default.
+ - OpenSSL version 1.1.1 and later add support for TLS 1.3.
+
+## Override the Default Values
+
+If you wish to override the defaults, you can customize the allowed protocols in your `server.psd1` configuration file. For example, if you want to allow only TLS protocols (excluding the deprecated SSL versions), you can configure it as follows:
+
+```powershell
+@{
+ Server = @{
+ Ssl = @{
+ Protocols = @('Tls', 'Tls11', 'Tls12')
+ }
+ }
+}
+```
+
+Or, to include TLS 1.3 where supported:
+
+```powershell
+@{
+ Server = @{
+ Ssl = @{
+ Protocols = @('Tls', 'Tls11', 'Tls12', 'Tls13')
+ }
+ }
+}
+```
+
+This configuration allows you to explicitly set the protocols from the following list of supported values: `'Ssl2'`, `'Ssl3'`, `'Tls'`, `'Tls11'`, `'Tls12'`, and `'Tls13'`.
+
+> **Important:** Overriding these default values in your configuration file does **not** automatically enable the corresponding protocols at the operating system level. The OS may block a protocol unless its native settings are also changed. In other words, even if you add `'Ssl3'` to your allowed protocols, Windows 11 will still reject SSLv3 connections unless you modify the OS settings.
+
+### Example: Enabling SSLv3 on Windows 11
+
+By default, Windows 11 disables SSLv3 in its Schannel settings. To enable SSLv3, you need to change the registry settings. **Proceed with caution** as enabling SSLv3 can expose your system to known vulnerabilities (such as the POODLE attack).
+
+You can enable SSLv3 on Windows 11 using PowerShell as follows:
+
+```powershell
+# Create the registry keys for SSL 3.0 if they don't already exist
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0" -Force | Out-Null
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client" -Force | Out-Null
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" -Force | Out-Null
+
+# Enable SSLv3 for both client and server by setting the Enabled DWORD to 1
+Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client" -Name "Enabled" -Value 1 -Type DWord
+Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" -Name "Enabled" -Value 1 -Type DWord
+
+Write-Output "SSLv3 has been enabled. A system restart may be required for the changes to take effect."
+```
+
+After making these changes, your Windows 11 system will accept SSLv3 connections. Remember that this registry modification is an OS-level change, and overriding the configuration in `server.psd1` alone will not suffice.
diff --git a/docs/index.md b/docs/index.md
index 25d0d9dd9..22b75db5a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -18,36 +18,37 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We
## 🚀 Features
-* Cross-platform using PowerShell Core (with support for PS5)
-* Docker support, including images for ARM/Raspberry Pi
-* Azure Functions, AWS Lambda, and IIS support
-* OpenAPI specification version 3.0.x and 3.1.0
-* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
-* Listen on a single or multiple IP(v4/v6) addresses/hostnames
-* Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
-* Host REST APIs, Web Pages, and Static Content (with caching)
-* Support for custom error pages
-* Request and Response compression using GZip/Deflate
-* Multi-thread support for incoming requests
-* Inbuilt template engine, with support for third-parties
-* Async timers for short-running repeatable processes
-* Async scheduled tasks using cron expressions for short/long-running processes
-* Supports logging to CLI, Files, and custom logic for other services like LogStash
-* Cross-state variable access across multiple runspaces
-* Restart the server via file monitoring, or defined periods/times
-* Ability to allow/deny requests from certain IP addresses and subnets
-* Basic rate limiting for IP addresses and subnets
-* Middleware and Sessions on web servers, with Flash message and CSRF support
-* Authentication on requests, such as Basic, Windows and Azure AD
-* Authorisation support on requests, using Roles, Groups, Scopes, etc.
-* Support for dynamically building Routes from Functions and Modules
-* Generate/bind self-signed certificates
-* Secret management support to load secrets from vaults
-* Support for File Watchers
-* In-memory caching, with optional support for external providers (such as Redis)
-* (Windows) Open the hosted server as a desktop application
-* FileBrowsing support
-* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese
+* ✅ Cross-platform using PowerShell Core (with support for PS5)
+* ✅ Docker support, including images for ARM/Raspberry Pi
+* ✅ Azure Functions, AWS Lambda, and IIS support
+* ✅ OpenAPI specification version 3.0.x and 3.1.0
+* ✅ OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
+* ✅ Listen on a single or multiple IP(v4/v6) addresses/hostnames
+* ✅ Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
+* ✅ Host REST APIs, Web Pages, and Static Content (with caching)
+* ✅ Support for custom error pages
+* ✅ Request and Response compression using GZip/Deflate
+* ✅ Multi-thread support for incoming requests
+* ✅ Inbuilt template engine, with support for third-parties
+* ✅ Async timers for short-running repeatable processes
+* ✅ Async scheduled tasks using cron expressions for short/long-running processes
+* ✅ Supports logging to CLI, Files, and custom logic for other services like LogStash
+* ✅ Cross-state variable access across multiple runspaces
+* ✅ Restart the server via file monitoring, or defined periods/times
+* ✅ Ability to allow/deny requests from certain IP addresses and subnets
+* ✅ Basic rate limiting for IP addresses and subnets
+* ✅ Middleware and Sessions on web servers, with Flash message and CSRF support
+* ✅ Authentication on requests, such as Basic, Windows and Azure AD
+* ✅ Authorisation support on requests, using Roles, Groups, Scopes, etc.
+* ✅ Enhanced authentication support, including Basic, Bearer (with JWT), Certificate, Digest, Form, OAuth2, and ApiKey (with JWT).
+* ✅ Support for dynamically building Routes from Functions and Modules
+* ✅ Generate/bind self-signed certificates
+* ✅ Secret management support to load secrets from vaults
+* ✅ Support for File Watchers
+* ✅ In-memory caching, with optional support for external providers (such as Redis)
+* ✅ (Windows) Open the hosted server as a desktop application
+* ✅ FileBrowsing support
+* ✅ Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese,Dutch and Chinese
## 🏢 Companies using Pode
diff --git a/examples/Authentication/Modules/Invoke-Digest.psm1 b/examples/Authentication/Modules/Invoke-Digest.psm1
new file mode 100644
index 000000000..07672527f
--- /dev/null
+++ b/examples/Authentication/Modules/Invoke-Digest.psm1
@@ -0,0 +1,662 @@
+function ConvertTo-Hash {
+ param (
+ [string]$Value,
+ [string]$Algorithm
+ )
+
+ $crypto = switch ($Algorithm) {
+ 'MD5' { [System.Security.Cryptography.MD5]::Create() }
+ 'SHA-1' { [System.Security.Cryptography.SHA1]::Create() }
+ 'SHA-256' { [System.Security.Cryptography.SHA256]::Create() }
+ 'SHA-384' { [System.Security.Cryptography.SHA384]::Create() }
+ 'SHA-512' { [System.Security.Cryptography.SHA512]::Create() }
+ 'SHA-512/256' {
+ # Compute SHA-512 and take first 32 bytes (256 bits)
+ $sha512 = [System.Security.Cryptography.SHA512]::Create()
+ $fullHash = $sha512.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))
+ return [System.BitConverter]::ToString($fullHash[0..31]).Replace('-', '').ToLowerInvariant()
+ }
+ }
+
+ return [System.BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))).Replace('-', '').ToLowerInvariant()
+}
+
+function ChallengeDigest {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
+ [string]$Method,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Uri
+ )
+ # Create an HTTP client
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ # Step 1: Send an initial request to get the challenge
+ $initialRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::$Method, $Uri)
+ $initialResponse = $httpClient.SendAsync($initialRequest).Result
+ if ($null -eq $initialResponse) {
+ Throw "Server $Uri is not responding"
+ }
+
+ # Extract WWW-Authenticate headers safely
+ $wwwAuthHeaders = $initialResponse.Headers.GetValues('WWW-Authenticate')
+ # Filter to get only the Digest authentication scheme
+ $wwwAuthHeader = $wwwAuthHeaders | Where-Object { $_ -match '^Digest' }
+
+ Write-Verbose 'Extracted WWW-Authenticate headers:'
+ $wwwAuthHeaders | ForEach-Object { Write-Verbose " - $_" }
+
+ if (-not $wwwAuthHeader) {
+ Throw 'Digest authentication not supported by server!'
+ }
+
+ # Extract Digest Authentication challenge values
+ $challenge = @{}
+
+ if ($wwwAuthHeader -match '^Digest ') {
+ $headerContent = $wwwAuthHeader -replace '^Digest ', ''
+ Write-Verbose "RAW HEADER: $headerContent"
+
+ # 1) CAPTURE supported algorithms
+ if ($headerContent -match 'algorithm=((?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5)(?:,\s*(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5))*)') {
+ $algorithms = ($matches[1] -split '\s*,\s*')
+ Write-Verbose "Supported Algorithms: $algorithms"
+ $challenge['algorithm'] = $algorithms
+ }
+
+ # 2) REMOVE algorithm parameter
+ $headerContent = $headerContent -replace 'algorithm=(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5)(?:,\s*(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5))*\s*,?', ''
+ # 3) CLEAN UP extra commas/whitespace
+ $headerContent = $headerContent -replace ',\s*,', ','
+ $headerContent = $headerContent -replace '^\s*,', ''
+
+ # Split remaining parameters safely
+ $headerContent -split ', ' | ForEach-Object {
+ $key, $value = $_ -split '=', 2
+ if ($key -and $value) {
+ $challenge[$key.Trim()] = $value.Trim('"')
+ }
+ }
+ }
+
+ Write-Verbose 'Extracted Digest Authentication Challenge:'
+ $challenge.GetEnumerator() | ForEach-Object { Write-Verbose "$($_.Key) = $($_.Value)" }
+
+ $realm = $challenge['realm']
+ $nonce = $challenge['nonce']
+ $qop = $challenge['qop']
+ $algorithm = $challenge['algorithm']
+
+ if (('Post', 'Put', 'Patch') -contains $Method) {
+ if ($qop -eq 'auth-int' -or $qop -eq 'auth,auth-int') {
+ $qop = 'auth-int'
+ }
+ else {
+ $qop = 'auth'
+ }
+ }
+ else {
+ if ($qop -eq 'auth' -or $qop -eq 'auth,auth-int') {
+ $qop = 'auth'
+ }
+ else {
+ throw "$Method doesn't support QualityOfProtection 'auth-int'"
+ }
+ }
+
+ Write-Verbose "Selected QOP: $qop"
+
+ $preferredAlgorithms = @('SHA-512/256', 'SHA-512', 'SHA-384', 'SHA-256', 'SHA-1', 'MD5')
+ if ($algorithm -isnot [System.Array]) {
+ $algorithm = @($algorithm)
+ }
+ $algorithm = ($preferredAlgorithms | Where-Object { $algorithm -contains $_ } | Select-Object -First 1)
+ if (-not $algorithm) {
+ Throw "No supported algorithms found! Server supports: $algorithm"
+ }
+ return [PSCustomObject]@{
+ realm = $realm
+ nonce = $nonce
+ qop = $qop
+ algorithm = $algorithm
+ wwwAuthHeader = $wwwAuthHeader
+ uri = $Uri
+ httpClient = $httpClient
+ method = $Method
+ }
+}
+
+<#
+.SYNOPSIS
+ Sends an HTTP request using Digest authentication and returns a web response.
+
+.DESCRIPTION
+ The Invoke-WebRequestDigest function performs an HTTP request with Digest authentication,
+ handling HTTP headers, authentication challenges, retries, and timeouts.
+ It returns a BasicHtmlWebResponseObject similar to Invoke-WebRequest.
+
+.PARAMETER Uri
+ The target URI for the request.
+
+.PARAMETER Method
+ The HTTP method to use for the request. Default is 'GET'.
+
+.PARAMETER Body
+ The request body, required for methods like POST, PUT, and PATCH.
+
+.PARAMETER Credential
+ The PSCredential object containing the username and password for Digest authentication.
+
+.PARAMETER Headers
+ A hashtable of additional headers to include in the request.
+
+.PARAMETER ContentType
+ The Content-Type of the request body. Default is 'application/json'.
+
+.PARAMETER OperationTimeoutSeconds
+ The maximum time in seconds before the request times out. Default is 100.
+
+.PARAMETER ConnectionTimeoutSeconds
+ The timeout in seconds for establishing a connection. Default is 100.
+
+.PARAMETER DisableKeepAlive
+ If specified, disables persistent connections by adding the 'Connection: close' header.
+
+.PARAMETER HttpVersion
+ The HTTP version to use, such as '1.1' or '2.0'. Default is '1.1'.
+
+.PARAMETER MaximumRetryCount
+ The number of times to retry the request in case of failure. Default is 1.
+
+.PARAMETER RetryIntervalSec
+ The interval in seconds between retry attempts. Default is 1.
+
+.PARAMETER OutFile
+ If specified, writes the response body to the specified file instead of returning content.
+
+.PARAMETER PassThru
+ If specified, returns the response object even if OutFile is used.
+
+.PARAMETER SkipCertificateCheck
+ If specified, disables SSL certificate validation (useful for self-signed certificates).
+
+.PARAMETER SslProtocol
+ Specifies the allowed SSL/TLS protocol(s) to use (e.g., 'Tls12').
+
+.PARAMETER TransferEncoding
+ The value for the 'Transfer-Encoding' header.
+
+.PARAMETER UserAgent
+ The User-Agent string to use in the request.
+
+.OUTPUTS
+ - Returns a [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject].
+ - If OutFile is specified, writes response data to the specified file.
+
+.EXAMPLE
+ $cred = Get-Credential
+ $response = Invoke-WebRequestDigest -Uri 'https://example.com/data' -Method 'GET' -Credential $cred
+ Write-Output $response.Content
+
+.EXAMPLE
+ $body = @{ "name" = "John Doe"; "email" = "john@example.com" }
+ $cred = Get-Credential
+ $response = Invoke-WebRequestDigest -Uri 'https://example.com/users' -Method 'POST' -Credential $cred -Body $body -ContentType 'application/json'
+ Write-Output $response.Content
+
+.EXAMPLE
+ # Download file
+ Invoke-WebRequestDigest -Uri 'https://example.com/file.zip' -Method 'GET' -Credential $cred -OutFile 'C:\Downloads\file.zip'
+
+.NOTES
+ - This function provides full control over HTTP requests with Digest authentication.
+ - Supports custom headers, connection options, timeouts, and retries.
+ - Unlike Invoke-RestMethodDigest, this function does not automatically parse JSON/XML.
+#>
+function Invoke-WebRequestDigest {
+ [CmdletBinding(DefaultParameterSetName = 'Uri')]
+ [OutputType([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject])]
+ param(
+ # URI of the request (required)
+ [Parameter(Mandatory = $true, Position = 0)]
+ [Uri]$Uri,
+
+ # HTTP method (default GET)
+ [Parameter(Position = 1)]
+ [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'PATCH', 'MERGE', 'CONNECT')]
+ [string]$Method = 'GET',
+
+ # Request body (for POST/PUT/PATCH, etc.)
+ [Parameter()]
+ $Body,
+
+ # Credential for Digest authentication (required)
+ [Parameter(Mandatory = $true)]
+ [System.Management.Automation.PSCredential]$Credential,
+
+ # Additional headers (as a hashtable)
+ [Parameter()]
+ [hashtable]$Headers,
+
+ # Content type for the request body (default application/json)
+ [Parameter()]
+ [string]$ContentType = 'application/json',
+
+ # Timeout (for the overall operation) in seconds
+ [Parameter()]
+ [int]$OperationTimeoutSeconds = 100,
+
+ # Connection timeout in seconds
+ [Parameter()]
+ [int]$ConnectionTimeoutSeconds = 100,
+
+ # Disable persistent connections (KeepAlive)
+ [Parameter()]
+ [switch]$DisableKeepAlive,
+
+ # Specify the HTTP version (e.g. '1.1' or '2.0')
+ [Parameter()]
+ [string]$HttpVersion = '1.1',
+
+ # Maximum number of retries (if request fails)
+ [Parameter()]
+ [int]$MaximumRetryCount = 1,
+
+ # Interval between retries (seconds)
+ [Parameter()]
+ [int]$RetryIntervalSec = 1,
+
+ # If provided, write response body to this file
+ [Parameter()]
+ [string]$OutFile,
+
+ # If specified, output the response object even if OutFile is used
+ [Parameter()]
+ [switch]$PassThru,
+
+ # Skip certificate validation (useful for self-signed certs)
+ [Parameter()]
+ [switch]$SkipCertificateCheck,
+
+ # Specify allowed SSL/TLS protocol(s) (e.g. 'Tls12')
+ [Parameter()]
+ [string]$SslProtocol,
+
+ # Transfer-Encoding header value to set on the request
+ [Parameter()]
+ [string]$TransferEncoding,
+
+ # User-Agent string to use on the request
+ [Parameter()]
+ [string]$UserAgent
+ )
+
+ # Validate that we have a credential
+ if (-not $Credential) {
+ Throw 'A credential is required for Digest authentication.'
+ }
+
+ # Use HttpClientHandler
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ if ($SkipCertificateCheck) {
+ $handler.ServerCertificateCustomValidationCallback = { return $true }
+ }
+ if ($SslProtocol) {
+ $handler.SslProtocols = [System.Enum]::Parse(
+ [System.Security.Authentication.SslProtocols], $SslProtocol)
+ }
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ $httpClient.Timeout = [TimeSpan]::FromSeconds($ConnectionTimeoutSeconds)
+
+ # If DisableKeepAlive is specified, add a header to close the connection.
+ if ($DisableKeepAlive) {
+ if (-not $Headers) { $Headers = @{} }
+ $Headers['Connection'] = 'close'
+ }
+
+ # Use the challenge function to get the digest details.
+ try {
+ $challenge = ChallengeDigest -Uri $Uri -Method $Method
+ }
+ catch {
+ Throw "Error retrieving Digest authentication challenge: $_"
+ }
+
+ try {
+ # If a body is provided and content type is JSON, convert it if necessary.
+ if ($Body -and ($ContentType -match 'application/json')) {
+ if ($Body -isnot [string]) {
+ $Body = $Body | ConvertTo-Json -Compress
+ }
+ }
+
+ # Build the digest response parameters.
+ $nc = '00000001'
+ $cnonce = (New-Guid).Guid.Substring(0, 8)
+ $Method = $challenge.Method.ToUpper()
+ Write-Verbose "Using method: $Method"
+ $uriPath = ([System.Uri]$challenge.uri).AbsolutePath
+
+ # Compute HA1
+ $HA1 = ConvertTo-Hash -Value "$($Credential.UserName):$($challenge.realm):$($Credential.GetNetworkCredential().Password)" -Algorithm $challenge.algorithm
+
+ if ($challenge.qop -eq 'auth-int') {
+ if (('Post', 'Put', 'Patch') -notcontains $Method) {
+ Throw "'auth-int' doesn't support $Method"
+ }
+ $requestBody = $Body | ConvertTo-Json
+ $entityBodyHash = ConvertTo-Hash -Value $requestBody -Algorithm $challenge.algorithm
+ $HA2 = ConvertTo-Hash -Value "$($Method):$($uriPath):$($entityBodyHash)" -Algorithm $challenge.algorithm
+ }
+ else {
+ $HA2 = ConvertTo-Hash -Value "$($Method):$($uriPath)" -Algorithm $challenge.algorithm
+ }
+
+ $responseHash = ConvertTo-Hash -Value "$($HA1):$($challenge.nonce):$($nc):$($cnonce):$($challenge.qop):$HA2" -Algorithm $challenge.algorithm
+
+ # Build the Authorization header using StringBuilder.
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.Append('Digest username="').Append($Credential.UserName).Append('"')
+ [void]$sb.Append(', realm="').Append($challenge.realm).Append('"')
+ [void]$sb.Append(', nonce="').Append($challenge.nonce).Append('"')
+ [void]$sb.Append(', uri="').Append($uriPath).Append('"')
+ [void]$sb.Append(', algorithm=').Append($challenge.algorithm)
+ [void]$sb.Append(', response="').Append($responseHash).Append('"')
+ [void]$sb.Append(', qop="').Append($challenge.qop).Append('"')
+ [void]$sb.Append(', nc=').Append($nc)
+ [void]$sb.Append(', cnonce="').Append($cnonce).Append('"')
+
+ # Create the HttpRequestMessage.
+ $authRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::$Method, $challenge.uri)
+ $authRequest.Headers.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Digest', $sb.ToString())
+
+ # Set the HTTP version if provided.
+ if ($HttpVersion) {
+ $authRequest.Version = [System.Version]$HttpVersion
+ }
+
+ # Add additional headers (if any) to the request.
+ if ($Headers) {
+ foreach ($key in $Headers.Keys) {
+ $authRequest.Headers.TryAddWithoutValidation($key, $Headers[$key]) | Out-Null
+ }
+ }
+
+ # Set Transfer-Encoding if provided.
+ if ($TransferEncoding) {
+ $authRequest.Headers.TryAddWithoutValidation('Transfer-Encoding', $TransferEncoding) | Out-Null
+ }
+
+ # Set User-Agent if provided.
+ if ($UserAgent) {
+ $authRequest.Headers.UserAgent.Clear()
+ $authRequest.Headers.UserAgent.ParseAdd($UserAgent)
+ }
+
+ if ($challenge.qop -eq 'auth-int') {
+ $authRequest.Content = [System.Net.Http.StringContent]::new($requestBody, [System.Text.Encoding]::UTF8, $ContentType)
+ }
+
+ # Implement a simple retry loop.
+ $retryCount = 0
+ do {
+ try {
+ $rawResponse = $challenge.httpClient.SendAsync($authRequest).Result
+ break
+ }
+ catch {
+ if (++$retryCount -ge $MaximumRetryCount) {
+ Throw "Error sending the authenticated request after $MaximumRetryCount attempts: $_"
+ }
+ else {
+ Write-Verbose "Retrying in $RetryIntervalSec seconds..."
+ Start-Sleep -Seconds $RetryIntervalSec
+ }
+ }
+ } while ($true)
+
+ # Optionally write response to file.
+ if ($OutFile) {
+ $mediaType = $rawResponse.Content.Headers.ContentType.MediaType
+ if ($mediaType -match '^(text|application/json|application/xml)') {
+ $contentString = $rawResponse.Content.ReadAsStringAsync().Result
+ Set-Content -Path $OutFile -Value $contentString -Encoding UTF8
+ }
+ else {
+ $rawResponse.Content.ReadAsByteArrayAsync().Result | Set-Content -Path $OutFile -Encoding Byte
+ }
+ if (-not $PassThru) { return }
+ }
+
+ # Wrap the response in a BasicHtmlWebResponseObject using the OperationTimeoutSeconds value.
+ $contentStream = $rawResponse.Content.ReadAsStream()
+ $timeout = [TimeSpan]::FromSeconds($OperationTimeoutSeconds)
+ $cancellationToken = [System.Threading.CancellationToken]::None
+ return [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]::new($rawResponse, $contentStream, $timeout, $cancellationToken)
+ }
+ catch {
+ Throw "Error sending Digest authenticated request: $_"
+ }
+}
+
+
+<#
+.SYNOPSIS
+ Sends an HTTP or REST request using Digest authentication and returns parsed data.
+
+.DESCRIPTION
+ The Invoke-RestMethodDigest function performs an HTTP request with Digest authentication,
+ leveraging Invoke-WebRequestDigest under the hood. It automatically parses the response
+ content into an object, supporting JSON and XML formats.
+
+.PARAMETER Uri
+ The target URI for the request.
+
+.PARAMETER Method
+ The HTTP method to use for the request. Default is 'GET'.
+
+.PARAMETER Body
+ The request body, required for methods like POST, PUT, and PATCH.
+
+.PARAMETER Credential
+ The PSCredential object containing the username and password for Digest authentication.
+
+.PARAMETER Headers
+ A hashtable of additional headers to include in the request.
+
+.PARAMETER ContentType
+ The Content-Type of the request body. Default is 'application/json'.
+
+.PARAMETER OperationTimeoutSeconds
+ The maximum time in seconds before the request times out. Default is 100.
+
+.PARAMETER ConnectionTimeoutSeconds
+ The timeout in seconds for establishing a connection. Default is 100.
+
+.PARAMETER DisableKeepAlive
+ If specified, disables persistent connections by adding the 'Connection: close' header.
+
+.PARAMETER HttpVersion
+ The HTTP version to use, such as '1.1' or '2.0'. Default is '1.1'.
+
+.PARAMETER MaximumRetryCount
+ The number of times to retry the request in case of failure. Default is 1.
+
+.PARAMETER RetryIntervalSec
+ The interval in seconds between retry attempts. Default is 1.
+
+.PARAMETER OutFile
+ If specified, writes the response body to the specified file instead of returning content.
+
+.PARAMETER PassThru
+ If specified, returns the response object even if OutFile is used.
+
+.PARAMETER SkipCertificateCheck
+ If specified, disables SSL certificate validation (useful for self-signed certificates).
+
+.PARAMETER SslProtocol
+ Specifies the allowed SSL/TLS protocol(s) to use (e.g., 'Tls12').
+
+.PARAMETER TransferEncoding
+ The value for the 'Transfer-Encoding' header.
+
+.PARAMETER UserAgent
+ The User-Agent string to use in the request.
+
+.OUTPUTS
+ - JSON responses are converted to PowerShell objects.
+ - XML responses are parsed into XML objects.
+ - Plain text or other data is returned as-is.
+
+.EXAMPLE
+ $cred = Get-Credential
+ $response = Invoke-RestMethodDigest -Uri 'https://example.com/api/data' -Method 'GET' -Credential $cred
+ Write-Output $response
+
+.EXAMPLE
+ $body = @{ "name" = "John Doe"; "email" = "john@example.com" }
+ $cred = Get-Credential
+ $response = Invoke-RestMethodDigest -Uri 'https://example.com/api/users' -Method 'POST' -Credential $cred -Body $body -ContentType 'application/json'
+ Write-Output $response
+
+.NOTES
+ - This function is a wrapper around Invoke-WebRequestDigest and provides an easier way
+ to work with REST APIs by automatically parsing the response content.
+ - Use Invoke-WebRequestDigest if you need full access to response headers and raw content.
+#>
+function Invoke-RestMethodDigest {
+ [CmdletBinding(DefaultParameterSetName = 'Uri')]
+ [OutputType([xml])]
+ [OutputType([psobject])]
+ param(
+ # URI of the request (required)
+ [Parameter(Mandatory = $true, Position = 0)]
+ [Uri]$Uri,
+
+ # HTTP method (default GET)
+ [Parameter(Position = 1)]
+ [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'PATCH', 'MERGE', 'CONNECT')]
+ [string]$Method = 'GET',
+
+ # Request body (for POST/PUT/PATCH, etc.)
+ [Parameter()]
+ $Body,
+
+ # Credential for Digest authentication (required)
+ [Parameter(Mandatory = $true)]
+ [System.Management.Automation.PSCredential]$Credential,
+
+ # Additional headers (as a hashtable)
+ [Parameter()]
+ [hashtable]$Headers,
+
+ # Content type for the request body (default application/json)
+ [Parameter()]
+ [string]$ContentType = 'application/json',
+
+ # Timeout (for the overall operation) in seconds
+ [Parameter()]
+ [int]$OperationTimeoutSeconds = 100,
+
+ # Connection timeout in seconds
+ [Parameter()]
+ [int]$ConnectionTimeoutSeconds = 100,
+
+ # Disable persistent connections (KeepAlive)
+ [Parameter()]
+ [switch]$DisableKeepAlive,
+
+ # Specify the HTTP version (e.g. '1.1' or '2.0')
+ [Parameter()]
+ [string]$HttpVersion = '1.1',
+
+ # Maximum number of retries (if request fails)
+ [Parameter()]
+ [int]$MaximumRetryCount = 1,
+
+ # Interval between retries (seconds)
+ [Parameter()]
+ [int]$RetryIntervalSec = 1,
+
+ # If provided, write response body to this file
+ [Parameter()]
+ [string]$OutFile,
+
+ # If specified, output the response object even if OutFile is used
+ [Parameter()]
+ [switch]$PassThru,
+
+ # Skip certificate validation (useful for self-signed certs)
+ [Parameter()]
+ [switch]$SkipCertificateCheck,
+
+ # Specify allowed SSL/TLS protocol(s) (e.g. 'Tls12')
+ [Parameter()]
+ [string]$SslProtocol,
+
+
+ # Transfer-Encoding header value
+ [Parameter()]
+ [string]$TransferEncoding,
+
+ # User-Agent string to use on the request
+ [Parameter()]
+ [string]$UserAgent
+ )
+
+ # Build a parameter hashtable for Invoke-WebRequestDigest
+ $params = @{
+ Uri = $Uri
+ Method = $Method
+ Body = $Body
+ Credential = $Credential
+ Headers = $Headers
+ ContentType = $ContentType
+ OperationTimeoutSeconds = $OperationTimeoutSeconds
+ ConnectionTimeoutSeconds = $ConnectionTimeoutSeconds
+ DisableKeepAlive = $DisableKeepAlive
+ HttpVersion = $HttpVersion
+ MaximumRetryCount = $MaximumRetryCount
+ RetryIntervalSec = $RetryIntervalSec
+ OutFile = $OutFile
+ PassThru = $PassThru
+ SkipCertificateCheck = $SkipCertificateCheck
+ SslProtocol = $SslProtocol
+ TransferEncoding = $TransferEncoding
+ UserAgent = $UserAgent
+ }
+
+ # Call the digest-enabled web request function
+ $webResponse = Invoke-WebRequestDigest @params
+
+ if ($null -eq $webResponse) {
+ return $null
+ }
+
+ # Parse the response content based on its media type
+ $content = $webResponse.Content
+ if ($content) {
+ # Get Content-Type header if available
+ $mediaType = $webResponse.Headers.'Content-Type'
+ if ($mediaType -match 'application/json') {
+ return $content | ConvertFrom-Json
+ }
+ elseif ($mediaType -match 'application/xml' -or $mediaType -match 'text/xml') {
+ return [xml]$content
+ }
+ else {
+ # For non-parsed content (plain text or other formats)
+ return $content
+ }
+ }
+ else {
+ return $null
+ }
+}
+
+Export-ModuleMember -Function Invoke-WebRequestDigest
+Export-ModuleMember -Function Invoke-RestMethodDigest
diff --git a/examples/WebAuth-ApikeyJWT.ps1 b/examples/Authentication/Web-AuthApiKey.ps1
similarity index 94%
rename from examples/WebAuth-ApikeyJWT.ps1
rename to examples/Authentication/Web-AuthApiKey.ps1
index dcf242d54..e7b88e135 100644
--- a/examples/WebAuth-ApikeyJWT.ps1
+++ b/examples/Authentication/Web-AuthApiKey.ps1
@@ -29,7 +29,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Get
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/WebAuth-ApikeyJWT.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/WebAuth-ApikeyJWT.ps1
.NOTES
Author: Pode Team
@@ -44,7 +44,7 @@ param(
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasic.ps1 b/examples/Authentication/Web-AuthBasic.ps1
similarity index 93%
rename from examples/Web-AuthBasic.ps1
rename to examples/Authentication/Web-AuthBasic.ps1
index e0a886a02..ef795df87 100644
--- a/examples/Web-AuthBasic.ps1
+++ b/examples/Authentication/Web-AuthBasic.ps1
@@ -23,7 +23,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasic.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasic.ps1
.NOTES
Author: Pode Team
@@ -31,7 +31,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -75,7 +75,7 @@ Start-PodeServer -Threads 2 {
return @{ Message = 'Invalid details supplied' }
}
-
+
# POST request to get current user (since there's no session, authentication will always happen)
Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ScriptBlock {
Write-PodeJsonResponse -Value @{
diff --git a/examples/Web-AuthBasicAccess.ps1 b/examples/Authentication/Web-AuthBasicAccess.ps1
similarity index 96%
rename from examples/Web-AuthBasicAccess.ps1
rename to examples/Authentication/Web-AuthBasicAccess.ps1
index 1bd7d36c5..0c07afe5d 100644
--- a/examples/Web-AuthBasicAccess.ps1
+++ b/examples/Authentication/Web-AuthBasicAccess.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Dot-SourceScript.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Dot-SourceScript.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicAdhoc.ps1 b/examples/Authentication/Web-AuthBasicAdhoc.ps1
similarity index 94%
rename from examples/Web-AuthBasicAdhoc.ps1
rename to examples/Authentication/Web-AuthBasicAdhoc.ps1
index c4eea0b73..b7b905ec5 100644
--- a/examples/Web-AuthBasicAdhoc.ps1
+++ b/examples/Authentication/Web-AuthBasicAdhoc.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAdhoc.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicAdhoc.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicAnon.ps1 b/examples/Authentication/Web-AuthBasicAnon.ps1
similarity index 94%
rename from examples/Web-AuthBasicAnon.ps1
rename to examples/Authentication/Web-AuthBasicAnon.ps1
index 597c3cf9c..2f6648779 100644
--- a/examples/Web-AuthBasicAnon.ps1
+++ b/examples/Authentication/Web-AuthBasicAnon.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAnon.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicAnon.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicBearer.ps1 b/examples/Authentication/Web-AuthBasicBearer.ps1
similarity index 80%
rename from examples/Web-AuthBasicBearer.ps1
rename to examples/Authentication/Web-AuthBasicBearer.ps1
index f6c73820d..5ccec6948 100644
--- a/examples/Web-AuthBasicBearer.ps1
+++ b/examples/Authentication/Web-AuthBasicBearer.ps1
@@ -9,10 +9,16 @@
.EXAMPLE
To run the sample: ./Web-AuthBasicBearer.ps1
- Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' }
+ Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' } -ResponseHeadersVariable headers
+
+.EXAMPLE
+ "No Authorization header found"
+
+ Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -ResponseHeadersVariable headers -Verbose -SkipHttpErrorCheck
+ $headers | Format-List
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicBearer.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicBearer.ps1
.NOTES
Author: Pode Team
@@ -20,7 +26,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -64,7 +70,7 @@ Start-PodeServer -Threads 2 {
}
# GET request to get list of users (since there's no session, authentication will always happen)
- Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock {
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
diff --git a/examples/Web-AuthBasicClientcert.ps1 b/examples/Authentication/Web-AuthBasicClientcert.ps1
similarity index 92%
rename from examples/Web-AuthBasicClientcert.ps1
rename to examples/Authentication/Web-AuthBasicClientcert.ps1
index 489b37b72..d99470183 100644
--- a/examples/Web-AuthBasicClientcert.ps1
+++ b/examples/Authentication/Web-AuthBasicClientcert.ps1
@@ -10,7 +10,7 @@
To run the sample: ./Web-AuthBasicClientcert.ps1
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicClientcert.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicClientcert.ps1
.NOTES
Author: Pode Team
@@ -18,7 +18,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicHeader.ps1 b/examples/Authentication/Web-AuthBasicHeader.ps1
similarity index 84%
rename from examples/Web-AuthBasicHeader.ps1
rename to examples/Authentication/Web-AuthBasicHeader.ps1
index 0fb0f8d27..169d3f445 100644
--- a/examples/Web-AuthBasicHeader.ps1
+++ b/examples/Authentication/Web-AuthBasicHeader.ps1
@@ -17,7 +17,8 @@
The example used here is Basic authentication.
Login:
- $session = (Invoke-WebRequest -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }).Headers['pode.sid']
+ Invoke-RestMethod -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } -ResponseHeadersVariable headers -SkipHttpErrorCheck
+ $session = $headers['pode.sid']
Users:
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ 'pode.sid' = "$session" }
@@ -26,7 +27,7 @@
Invoke-WebRequest -Uri http://localhost:8081/logout -Method Post -Headers @{ 'pode.sid' = "$session" }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicHeader.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicHeader.ps1
.NOTES
Author: Pode Team
@@ -34,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -81,13 +82,13 @@ Start-PodeServer -Threads 2 {
}
# POST request to login
- Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login'
+ Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' -ErrorContentType 'application/json'
# POST request to logout
- Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout
+ Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout -ErrorContentType 'application/json'
# POST request to get list of users - the "pode.sid" header is expected
- Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ScriptBlock {
+ Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ErrorContentType 'application/json' -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
diff --git a/examples/Authentication/Web-AuthBearerJWT.ps1 b/examples/Authentication/Web-AuthBearerJWT.ps1
new file mode 100644
index 000000000..1ae85402f
--- /dev/null
+++ b/examples/Authentication/Web-AuthBearerJWT.ps1
@@ -0,0 +1,248 @@
+<#
+.SYNOPSIS
+ A PowerShell script to set up a Pode server with JWT authentication and various route configurations.
+
+.DESCRIPTION
+ This script initializes a Pode server that listens on a specified port, enables request and error logging,
+ and configures JWT authentication using either the request header or query parameters. It also defines
+ a protected route to fetch a list of users, requiring authentication.
+
+.PARAMETER Location
+ Specifies where the API key (JWT token) is expected.
+ Valid values: 'Header', 'Query'.
+ Default: 'Header'.
+
+.EXAMPLE
+ # Run the sample
+ ./WebAuth-bearerJWT.ps1
+
+ JWT payload:
+ {
+ "sub": "1234567890",
+ "name": "morty",
+ "username":"morty",
+ "type": "Human",
+ "id" : "M0R7Y302",
+ "admin": true,
+ "iat": 1516239022,
+ "exp": 2634234231,
+ "iss": "auth.example.com",
+ "sub": "1234567890",
+ "aud": "myapi.example.com",
+ "nbf": 1690000000,
+ "jti": "unique-token-id",
+ "role": "admin"
+ }
+
+.EXAMPLE
+ # Example request using PS512 JWT authentication
+ $jwt = ConvertTo-PodeJwt -PfxPath ./cert.pfx -RsaPaddingScheme Pss -PfxPassword (ConvertTo-SecureString 'mySecret' -AsPlainText -Force)
+ $headers = @{ 'Authorization' = "Bearer $jwt" }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/auth/bearer/jwt/PS512' -Method Get -Headers $headers
+
+.EXAMPLE
+ # Example request using RS384 JWT authentication
+ $headers = @{ 'Authorization' = 'Bearer ' }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/users' -Method Get -Headers $headers
+
+.EXAMPLE
+ # Example request using HS256 JWT authentication
+ $jwt = ConvertTo-PodeJwt -Algorithm HS256 -Secret (ConvertTo-SecureString 'secret' -AsPlainText -Force) -Payload @{id='id';name='Morty'}
+ $headers = @{ 'Authorization' = "Bearer $jwt" }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/users' -Method Get -Headers $headers
+
+ .LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthbearerJWT.ps1
+
+ .NOTES
+ - This script uses Pode to create a lightweight web server with authentication.
+ - JWT authentication is handled via Bearer tokens passed in either the header or query.
+ - Ensure the private key is securely stored and managed for RS256-based JWT signing.
+ - Using query parameters for authentication is **discouraged** due to security risks.
+ - Always use HTTPS in production to protect sensitive authentication data.
+
+ Author: Pode Team
+ License: MIT License
+#>
+
+param(
+ [Parameter()]
+ [ValidateSet('Header', 'Query' )]
+ [string]
+ $Location = 'Header'
+)
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 -ApplicationName 'webauth' {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+
+ New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+
+
+ $JwtVerificationMode = 'Lenient' # Set your desired verification mode (Lenient or Strict)
+
+ $certificateTypes = @{
+ 'RS256' = @{
+ KeyType = 'RSA'
+ KeyLength = 2048
+ }
+ 'RS384' = @{
+ KeyType = 'RSA'
+ KeyLength = 3072
+ }
+ 'RS512' = @{
+ KeyType = 'RSA'
+ KeyLength = 4096
+ }
+ 'PS256' = @{
+ KeyType = 'RSA'
+ KeyLength = 2048
+ }
+ 'PS384' = @{
+ KeyType = 'RSA'
+ KeyLength = 3072
+ }
+ 'PS512' = @{
+ KeyType = 'RSA'
+ KeyLength = 4096
+ }
+ 'ES256' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 256
+ }
+ 'ES384' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 384
+ }
+ 'ES512' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 521
+ }
+ }
+
+ $CertsPath = Join-Path -Path (Get-PodeServerPath) -ChildPath "certs"
+ if (!(Test-Path -Path $CertsPath -PathType Container)) {
+ New-Item -Path $CertsPath -ItemType Directory
+ }
+ foreach ($alg in $certificateTypes.Keys) {
+ $x509Certificate = New-PodeSelfSignedCertificate -Loopback -KeyType $certificateTypes[$alg].KeyType -KeyLength $certificateTypes[$alg].KeyLength -CertificatePurpose CodeSigning -Ephemeral -Exportable
+
+ Export-PodeCertificate -Certificate $x509Certificate -Format PFX -Path (join-path -path $CertsPath -ChildPath $alg)
+
+ # Define the authentication location dynamically (e.g., `/auth/bearer/jwt/{algorithm}`)
+ $pathRoute = "/auth/bearer/jwt/$alg"
+ # Register Pode Bearer Authentication
+ Write-PodeHost "🔹 Registering JWT Authentication for: $alg ($Location)"
+
+ $rsaPaddingScheme = if ($alg.StartsWith('PS')) { 'Pss' } else { 'Pkcs1V15' }
+
+ $param = @{
+ Location = $Location
+ AsJWT = $true
+ RsaPaddingScheme = $rsaPaddingScheme
+ JwtVerificationMode = $JwtVerificationMode
+ X509Certificate = $x509Certificate
+ }
+
+ New-PodeAuthBearerScheme @param |
+ Add-PodeAuth -Name "Bearer_JWT_$alg" -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jst.name
+ Type = $jst.type
+ }
+ }
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path $pathRoute -Authentication "Bearer_JWT_$alg" -ScriptBlock {
+
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+ }
+
+
+
+ # setup bearer auth
+ New-PodeAuthBearerScheme -Location $Location -AsJWT -Secret (ConvertTo-SecureString 'your-256-bit-secret' -AsPlainText -Force) -JwtVerificationMode Lenient | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jst.name
+ Type = $jst.type
+ }
+ }
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock {
+
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+
+
+ Register-PodeEvent -Type Stop -Name 'CleanCerts' -ScriptBlock {
+ if ( (Test-Path -Path "$(Get-PodeServerPath)/cert" -PathType Container)) {
+ Remove-Item -Path "$(Get-PodeServerPath)/cert" -Recurse -Force
+ Write-PodeHost "$(Get-PodeServerPath)/cert removed."
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1 b/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1
new file mode 100644
index 000000000..90fe58d93
--- /dev/null
+++ b/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1
@@ -0,0 +1,274 @@
+<#
+.SYNOPSIS
+ A PowerShell script demonstrating the full lifecycle of JWT authentication using X.509 certificates in Pode.
+
+.DESCRIPTION
+ This script sets up a Pode server that listens on a specified port and enables JWT authentication using X.509 certificates.
+ It showcases the full JWT authentication lifecycle, including login, renewal, validation, and retrieval of user information.
+ Authentication is performed using JWT tokens signed with the selected cryptographic algorithm.
+
+.PARAMETER Location
+ Specifies where the API key (JWT token) is expected.
+ Valid values: 'Header', 'Query'.
+ Default: 'Header'.
+
+.PARAMETER Algorithm
+ Specifies the cryptographic algorithm used for JWT signing and verification.
+ Valid values: 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512'.
+ Default: 'ES512'.
+
+.EXAMPLE
+ # Run the sample
+ ./WebAuth-bearerJWT.ps1
+
+ JWT payload example:
+ {
+ "sub": "1234567890",
+ "name": "morty",
+ "username": "morty",
+ "type": "Human",
+ "id": "M0R7Y302",
+ "admin": true,
+ "iat": 1516239022,
+ "exp": 2634234231,
+ "iss": "auth.example.com",
+ "aud": "myapi.example.com",
+ "nbf": 1690000000,
+ "jti": "unique-token-id",
+ "role": "admin"
+ }
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthbearerJWTLifecycle.ps1
+
+.NOTES
+ - This script uses Pode to create a lightweight web server with authentication.
+ - JWT authentication is handled via Bearer tokens passed in either the header or query parameters.
+ - JWTs are signed using X.509 certificates and verified based on the selected algorithm.
+ - The script implements endpoints for login, token renewal, and token validation.
+ - Ensure the private key is securely stored and managed for RS256-based JWT signing.
+ - Using query parameters for authentication is **discouraged** due to security risks.
+ - Always use HTTPS in production to protect sensitive authentication data.
+
+ Author: Pode Team
+ License: MIT License
+#>
+
+param(
+ [Parameter()]
+ [ValidateSet('Header', 'Query' )]
+ [string]
+ $Location = 'Header',
+
+ [Parameter()]
+ [ValidateSet( 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512')]
+ [string]
+ $Algorithm = 'ES512'
+)
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# Define a function to autenticate user credentials
+function Test-User {
+ param (
+ [string]$username,
+ [string]$password
+ )
+ if ($username -eq 'morty' -and $password -eq 'pickle') {
+ return @{
+ Id = 'M0R7Y302'
+ Username = 'morty.smith'
+ Name = 'Morty Smith'
+ Groups = 'Domain Users'
+ }
+ }
+ throw 'Invalid credentials'
+}
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 -ApplicationName 'webauth' {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8043 -Protocol Https -SelfSigned
+
+ New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+ # Configure CORS
+ Set-PodeSecurityAccessControl -Origin '*' -Duration 7200 -WithOptions -AuthorizationHeader -autoMethods -AutoHeader -Credentials -CrossDomainXhrRequests
+
+
+ # Enable OpenAPI documentation
+
+ Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses
+ Add-PodeOAInfo -Title 'JWT Test' -Version 1.0.17 -Description 'test'
+ Add-PodeOAServerEndpoint -url '/auth/bearer/jwt' -Description 'default endpoint'
+ # Enable OpenAPI viewers
+ Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger'
+ Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode
+ Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode
+ Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode
+ Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode
+ Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode
+
+ # Enable OpenAPI editor and bookmarks
+ Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor'
+ Enable-PodeOAViewer -Bookmarks -Path '/docs'
+
+
+ $JwtVerificationMode = 'Strict' # Set your desired verification mode (Lenient or Strict)
+ # $SecurePassword = ConvertTo-SecureString 'MySecurePassword' -AsPlainText -Force
+
+ $param = @{
+ Location = $Location
+ AsJWT = $true
+ JwtVerificationMode = $JwtVerificationMode
+ SelfSigned = $true
+ }
+ # Register Pode Bearer Authentication
+ New-PodeAuthBearerScheme @param |
+ Add-PodeAuth -Name "Bearer_JWT_$Algorithm" -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.id -ieq 'M0R7Y302') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jWt.name
+ Type = $jWt.type
+ sub = $jWt.Id
+ username = $jWt.Username
+ groups = $jWt.Groups
+ }
+ }
+ }
+ else {
+ write-podehost $jwt -Explode
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -PassThru -Method Get -Path "/auth/bearer/jwt/$Algorithm" -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ Write-PodeJsonResponse -Value $WebEvent.auth.User
+ } | Set-PodeOARouteInfo -Summary 'Get my info.' -Tags 'user' -OperationId "myinfo_$Algorithm"
+
+
+
+ Add-PodeRoute -PassThru -Method Post -Path '/auth/bearer/jwt/login' -ArgumentList $Algorithm -ScriptBlock {
+ param(
+ [string]
+ $Algorithm
+ )
+ try {
+ # In a real scenario, you'd validate the incoming credentials from $WebEvent.data
+ $username = $WebEvent.Data.username
+ $password = $WebEvent.Data.password
+ $user = Test-User -username $username -password $password
+
+
+ $payload = @{
+ sub = $user.Id
+ name = $user.Name
+ username = $user.Username
+ id = $user.Id
+ groups = $user.Groups
+ type = 'human'
+ }
+
+ # If valid, generate a JWT that matches the 'ExampleApiKeyCert' scheme
+ $jwt = ConvertTo-PodeJwt -Payload $payload -Authentication "Bearer_JWT_$Algorithm" -Expiration 600
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ 'success' = $true
+ 'user' = $user
+ 'token' = $jwt
+ }
+
+ }
+ catch {
+ write-podehost $_.Exception.Message
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid credentials' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Tags 'user' -OperationId 'loginUser' -PassThru |
+ Set-PodeOARequest -RequestBody (
+ New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOAStringProperty -Name 'username' -Description 'The user name for login' -Default 'morty' |
+ New-PodeOAStringProperty -Name 'password' -Description 'The password for login in clear text' -Format Password -Default 'pickle' |
+ New-PodeOAObjectProperty)
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true |
+ New-PodeOAStringProperty -Name 'user' -Description 'The user for login' -Example 'morty' |
+ New-PodeOAStringProperty -Name 'token' -Description 'Bearen JWT token' -Example '6656565' |
+ New-PodeOAObjectProperty
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied'
+
+ Add-PodeRoute -PassThru -Method Post -Path '/auth/bearer/jwt/renew' -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ try {
+
+ $jwt = Update-PodeJwt
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ 'success' = $true
+ 'token' = $jwt
+ }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'Extend JWT Token.' -Tags 'JWT' -OperationId 'renewToken' -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true |
+ New-PodeOAStringProperty -Name 'user' -Description 'The user for login' -Example 'morty' |
+ New-PodeOAStringProperty -Name 'token' -Description 'Bearen JWT token' -Example 'eyJ0eXAiOiJKV1QifQ.eyJpZCI6Ik0wUjdZMzAyIi ... UG9kZSJ9.hhU1fmykkSyZhUCr1NSZto-dGyt50r5OUlYj5SgL88EFlnulSOtsM-61tht-X5lEZVP7TCwG2q6ZELiA-4zey7BTIEecKg8zQ4NasZQi6eq9scSL0WJPNHNiGf91F1BsSAQmTxmtJz9-R9l7dxxonFlgLhq9ZwToPuAEK76lYuEQ45ERH-LoO5En9nRnar5N8SLe244To_T7UPKKBgd_DQNSuW4pShMbeK1_TTwELxroV2-d7bPyhUKIwrP61DDsGxgYCzsJ_8XG4YOfFg_u3bHp_JEplCFPoc5KUVNOQHFCzYR0WMZDhRDMnAF6J8Xn0RKTsFB7q1QNC0NF1-7TGQ' |
+ New-PodeOAObjectProperty
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 401 -Description 'Invalid JWT token supplied'
+
+
+ Add-PodeRoute -PassThru -Method Get -Path '/auth/bearer/jwt/info' -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ try {
+ $jwtInfo = ConvertFrom-PodeJwt -Outputs 'Header,Payload,Signature' -HumanReadable
+ $jwtInfo.success = $true
+ Write-PodeJsonResponse -StatusCode 200 -Value $jwtInfo
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'return JWT Token info.' -Tags 'JWT' -OperationId 'getInfoToken' -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOAObjectProperty -Properties (
+ ( New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true),
+ (New-PodeOAObjectProperty -Name Header ),
+ ( New-PodeOAObjectProperty -Name Payload), ( New-PodeOAStringProperty -Name Signature)
+ )
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 401 -Description 'Invalid JWT token supplied'
+
+}
\ No newline at end of file
diff --git a/examples/Authentication/Web-AuthDigest.ps1 b/examples/Authentication/Web-AuthDigest.ps1
new file mode 100644
index 000000000..779df40cc
--- /dev/null
+++ b/examples/Authentication/Web-AuthDigest.ps1
@@ -0,0 +1,215 @@
+<#
+.SYNOPSIS
+ PowerShell script to set up a Pode server with Digest authentication or make client requests.
+
+.DESCRIPTION
+ This script can either:
+ - Start a Pode server that listens on a specified port and uses Digest authentication to secure access.
+ - Act as a client to send requests with Digest authentication.
+
+ The authentication details are checked against predefined user data.
+ For non-MD5 algorithms, use ./utility/DigestClient.ps1.
+
+.PARAMETER Client
+ If specified, the script runs in client mode instead of starting a server.
+
+.PARAMETER Algorithm
+ The Digest authentication algorithm(s) to use. Supported values: MD5, SHA-1, SHA-256, SHA-512, SHA-384, SHA-512/256.
+ Defaults to all supported algorithms.
+
+.PARAMETER QualityOfProtection
+ Specifies the Quality of Protection (qop) to use in Digest authentication.
+ Valid options:
+ - 'auth': Authentication only.
+ - 'auth-int': Authentication with integrity protection.
+ - 'auth,auth-int': Support both modes.
+
+.EXAMPLE
+ To start the Pode server with default settings:
+ ```powershell
+ ./Web-AuthDigest.ps1
+ ```
+
+.EXAMPLE
+ To start the Pode server with SHA-256 authentication only:
+ ```powershell
+ ./Web-AuthDigest.ps1 -Algorithm SHA-256
+ ```
+
+.EXAMPLE
+ To run in client mode and send a Digest-authenticated request:
+ ```powershell
+ ./Web-AuthDigest.ps1 -Client
+ ```
+
+.EXAMPLE
+ Client request example using default .Net Digest support:
+
+ ```powershell
+ # Define the URI and credentials
+ $uri = [System.Uri]::new("http://localhost:8081/users")
+ $username = "morty"
+ $password = "pickle"
+
+ # Create a credential cache and add Digest authentication
+ $credentialCache = [System.Net.CredentialCache]::new()
+ $networkCredential = [System.Net.NetworkCredential]::new($username, $password)
+ $credentialCache.Add($uri, "Digest", $networkCredential)
+
+ # Create the HTTP client handler with the credential cache
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ $handler.Credentials = $credentialCache
+
+ # Create the HTTP client
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ # Send the GET request
+ $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
+ $response = $httpClient.SendAsync($requestMessage).Result
+
+ # Display response headers and content
+ $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" }
+ $content = $response.Content.ReadAsStringAsync().Result
+ $content
+ ```
+.EXAMPLE
+ Client request example using `Invoke-WebRequestDigest`:
+
+ ```powershell
+ Import-Module './client/Invoke-Digest.psm1'
+
+ # Define the URI and credentials
+ $uri = 'http://localhost:8081/users'
+ $username = 'morty'
+ $password = 'pickle'
+
+ # Convert the password to a SecureString and create a credential object
+ $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+ $credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+ # Make a GET request using Digest authentication
+ $response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+
+ # Display response headers and content
+ $response.Headers | Format-List
+ Write-Output $response.Content
+ ```
+
+.EXAMPLE
+ Running the server with `auth-int` quality of protection:
+ ```powershell
+ ./Web-AuthDigest.ps1 -QualityOfProtection auth-int
+ ```
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthDigest.ps1
+
+.NOTES
+ Author: Pode Team
+ License: MIT License
+#>
+
+[CmdletBinding(DefaultParameterSetName = 'Server')]
+param(
+ [Parameter(ParameterSetName = 'Client')]
+ [switch]
+ $Client,
+
+ [Parameter(ParameterSetName = 'Server')]
+ [string[]]
+ $Algorithm = @('MD5', 'SHA-1', 'SHA-256', 'SHA-512', 'SHA-384', 'SHA-512/256'),
+
+ [Parameter(ParameterSetName = 'Server')]
+ [ValidateSet('auth', 'auth-int', 'auth,auth-int' )]
+ [string[]]
+ $QualityOfProtection = 'auth,auth-int'
+)
+if ($Client) {
+ Import-Module './Modules/Invoke-Digest.psm1'
+ $uri = 'http://localhost:8081/users'
+ $username = 'morty'
+ $password = 'pickle'
+
+ $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+ $credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+ $response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+ $response | Format-List *
+
+ Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential -OutFile 'outfile.json'
+
+ $response = Invoke-RestMethodDigest -Uri $uri -Method 'GET' -Credential $credential
+ $response
+ return
+}
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+
+ # setup digest auth
+ New-PodeAuthDigestScheme -Algorithm $Algorithm -QualityOfProtection $QualityOfProtection | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
+ param($username, $params)
+
+ # here you'd check a real user storage, this is just for example
+ if ($username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = 'M0R7Y302'
+ Name = 'Morty'
+ Type = 'Human'
+ }
+ Password = 'pickle'
+ }
+ }
+
+ return $null
+ }
+ # If QualityOfProtection is 'auth-int' skip GET because it is not supported
+ if ($QualityOfProtection -ne 'auth-int') {
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+ }
+
+ Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
+ if ($WebEvent.data) {
+ Write-PodeJsonResponse -Value $WebEvent.data -StatusCode 200
+ }
+ else {
+ Write-PodeJsonResponse -Value @{success = $false } -StatusCode 400
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/examples/Web-AuthForm.ps1 b/examples/Authentication/Web-AuthForm.ps1
similarity index 92%
rename from examples/Web-AuthForm.ps1
rename to examples/Authentication/Web-AuthForm.ps1
index 45fad7a19..095f6539d 100644
--- a/examples/Web-AuthForm.ps1
+++ b/examples/Authentication/Web-AuthForm.ps1
@@ -22,7 +22,7 @@
You will be redirected to the login page, where you can log in with the credentials provided above.
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthForm.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthForm.ps1
.NOTES
Author: Pode Team
@@ -31,7 +31,7 @@
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -63,7 +63,7 @@ Start-PodeServer -Threads 2 {
Enable-PodeSessionMiddleware -Duration 120 -Extend
# setup form auth (