Unpacking Software Livestream

Join our monthly Unpacking Software livestream to hear about the latest news, chat and opinion on packaging, software deployment and lifecycle management!

Learn More

Chocolatey Product Spotlight

Join the Chocolatey Team on our regular monthly stream where we put a spotlight on the most recent Chocolatey product releases. You'll have a chance to have your questions answered in a live Ask Me Anything format.

Learn More

Chocolatey Coding Livestream

Join us for the Chocolatey Coding Livestream, where members of our team dive into the heart of open source development by coding live on various Chocolatey projects. Tune in to witness real-time coding, ask questions, and gain insights into the world of package management. Don't miss this opportunity to engage with our team and contribute to the future of Chocolatey!

Learn More

Calling All Chocolatiers! Whipping Up Windows Automation with Chocolatey Central Management

Webinar from
Wednesday, 17 January 2024

We are delighted to announce the release of Chocolatey Central Management v0.12.0, featuring seamless Deployment Plan creation, time-saving duplications, insightful Group Details, an upgraded Dashboard, bug fixes, user interface polishing, and refined documentation. As an added bonus we'll have members of our Solutions Engineering team on-hand to dive into some interesting ways you can leverage the new features available!

Watch On-Demand
Chocolatey Community Coffee Break

Join the Chocolatey Team as we discuss all things Community, what we do, how you can get involved and answer your Chocolatey questions.

Watch The Replays
Chocolatey and Intune Overview

Webinar Replay from
Wednesday, 30 March 2022

At Chocolatey Software we strive for simple, and teaching others. Let us teach you just how simple it could be to keep your 3rd party applications updated across your devices, all with Intune!

Watch On-Demand
Chocolatey For Business. In Azure. In One Click.

Livestream from
Thursday, 9 June 2022

Join James and Josh to show you how you can get the Chocolatey For Business recommended infrastructure and workflow, created, in Azure, in around 20 minutes.

Watch On-Demand
The Future of Chocolatey CLI

Livestream from
Thursday, 04 August 2022

Join Paul and Gary to hear more about the plans for the Chocolatey CLI in the not so distant future. We'll talk about some cool new features, long term asks from Customers and Community and how you can get involved!

Watch On-Demand
Hacktoberfest Tuesdays 2022

Livestreams from
October 2022

For Hacktoberfest, Chocolatey ran a livestream every Tuesday! Re-watch Cory, James, Gary, and Rain as they share knowledge on how to contribute to open-source projects such as Chocolatey CLI.

Watch On-Demand

Pode 2.10.0

  • 1
  • 2
  • 3

All Checks are Passing

3 Passing Tests


Validation Testing Passed


Verification Testing Passed

Details

Scan Testing Successful:

No detections found in any package files

Details
Learn More

Deployment Method: Individual Install, Upgrade, & Uninstall

To install Pode, run the following command from the command line or from PowerShell:

>

To upgrade Pode, run the following command from the command line or from PowerShell:

>

To uninstall Pode, run the following command from the command line or from PowerShell:

>

Deployment Method:

NOTE

This applies to both open source and commercial editions of Chocolatey.

1. Enter Your Internal Repository Url

(this should look similar to https://community.chocolatey.org/api/v2/)


2. Setup Your Environment

1. Ensure you are set for organizational deployment

Please see the organizational deployment guide

2. Get the package into your environment

  • Open Source or Commercial:
    • Proxy Repository - Create a proxy nuget repository on Nexus, Artifactory Pro, or a proxy Chocolatey repository on ProGet. Point your upstream to https://community.chocolatey.org/api/v2/. Packages cache on first access automatically. Make sure your choco clients are using your proxy repository as a source and NOT the default community repository. See source command for more information.
    • You can also just download the package and push it to a repository Download

3. Copy Your Script

choco upgrade pode -y --source="'INTERNAL REPO URL'" [other options]

See options you can pass to upgrade.

See best practices for scripting.

Add this to a PowerShell script or use a Batch script with tools and in places where you are calling directly to Chocolatey. If you are integrating, keep in mind enhanced exit codes.

If you do use a PowerShell script, use the following to ensure bad exit codes are shown as failures:


choco upgrade pode -y --source="'INTERNAL REPO URL'" 
$exitCode = $LASTEXITCODE

Write-Verbose "Exit code was $exitCode"
$validExitCodes = @(0, 1605, 1614, 1641, 3010)
if ($validExitCodes -contains $exitCode) {
  Exit 0
}

Exit $exitCode

- name: Install pode
  win_chocolatey:
    name: pode
    version: '2.10.0'
    source: INTERNAL REPO URL
    state: present

See docs at https://docs.ansible.com/ansible/latest/modules/win_chocolatey_module.html.


chocolatey_package 'pode' do
  action    :install
  source   'INTERNAL REPO URL'
  version  '2.10.0'
end

See docs at https://docs.chef.io/resource_chocolatey_package.html.


cChocoPackageInstaller pode
{
    Name     = "pode"
    Version  = "2.10.0"
    Source   = "INTERNAL REPO URL"
}

Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.


package { 'pode':
  ensure   => '2.10.0',
  provider => 'chocolatey',
  source   => 'INTERNAL REPO URL',
}

Requires Puppet Chocolatey Provider module. See docs at https://forge.puppet.com/puppetlabs/chocolatey.


4. If applicable - Chocolatey configuration/installation

See infrastructure management matrix for Chocolatey configuration elements and examples.

Package Approved

This package was approved as a trusted package on 15 Apr 2024.

Description

Pode is a Cross-Platform framework for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.

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, Swagger, and ReDoc support
  • Listen on a single or multiple IP address/hostnames
  • Cross-platform support for HTTP, HTTPS, TCP and SMTP
  • Cross-platform support for WebSockets, including secure WebSockets
  • 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
  • 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
  • (Windows) Open the hosted server as a desktop application

src\Libs\net6.0\Pode.deps.json
{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v6.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v6.0": {
      "Pode/2.10.0": {
        "runtime": {
          "Pode.dll": {}
        }
      }
    }
  },
  "libraries": {
    "Pode/2.10.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}
src\Libs\net6.0\Pode.dll
md5: B951C5CA3E75A58C01266A955A45778C | sha1: B8484206845A8A3AB4EA02DFC0F8005771849935 | sha256: AABBD7AC81F400E96D2E9CEC403C7303AA8CF4B1881BB4125E6DD0D8D970BD5C | sha512: F28F22071ADD5743DDB7386B9035351B7DA96A03886E58837DC8E1A129BF5A3BA8E88FAA87F67F922F5F97736321EF6BA86A297B50D9599D397DB4BBEE6D9D53
src\Libs\net6.0\Pode.pdb
 
src\Libs\net7.0\Pode.deps.json
{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v7.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v7.0": {
      "Pode/2.10.0": {
        "runtime": {
          "Pode.dll": {}
        }
      }
    }
  },
  "libraries": {
    "Pode/2.10.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}
src\Libs\net7.0\Pode.dll
md5: B59D4CA983D7C49E0300C40CCDB082BD | sha1: 0A1FA0DCC9876E856D59139EDD3EAD3E43995F5A | sha256: 83A1E13B02EF3F669643504315ACA891561859DAF374046E16B04629588293EA | sha512: A873D21E96C32B080885AB57C781006ACF7113C394CD1BB7709A6FAB3798B75F86EB6B9C0BED26BFC7D7A09927FCF29D946A0E0A415AC5FBDDB7BD44D8C50BBF
src\Libs\net7.0\Pode.pdb
 
src\Libs\net8.0\Pode.deps.json
{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v8.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v8.0": {
      "Pode/2.10.0": {
        "runtime": {
          "Pode.dll": {}
        }
      }
    }
  },
  "libraries": {
    "Pode/2.10.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}
src\Libs\net8.0\Pode.dll
md5: CA4F7C5EA6F9692514351E5B5A6CD200 | sha1: EB4F7D7CB0E99B41458BC323C2502060259A66C4 | sha256: 0856A4438F22F7B4AF643FA4770A1D347890DBD9DAA8627377C6C6905A5BD9C0 | sha512: 418895527F665DD9ED67E35AFFB9B4AC4359D305012E1ADC23FA7370D37D58BD0B61F81632D2163BCD02442DDB05E2E54510CC5585ACBE4EC13E9ACC11B6A951
src\Libs\net8.0\Pode.pdb
 
src\Libs\netstandard2.0\Pode.deps.json
{
  "runtimeTarget": {
    "name": ".NETStandard,Version=v2.0/",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETStandard,Version=v2.0": {},
    ".NETStandard,Version=v2.0/": {
      "Pode/2.10.0": {
        "dependencies": {
          "NETStandard.Library": "2.0.3"
        },
        "runtime": {
          "Pode.dll": {}
        }
      },
      "Microsoft.NETCore.Platforms/1.1.0": {},
      "NETStandard.Library/2.0.3": {
        "dependencies": {
          "Microsoft.NETCore.Platforms": "1.1.0"
        }
      }
    }
  },
  "libraries": {
    "Pode/2.10.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "Microsoft.NETCore.Platforms/1.1.0": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
      "path": "microsoft.netcore.platforms/1.1.0",
      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
    },
    "NETStandard.Library/2.0.3": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
      "path": "netstandard.library/2.0.3",
      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
    }
  }
}
src\Libs\netstandard2.0\Pode.dll
md5: 1A245C448AFF21569489EB2C899C87BD | sha1: 6EF7E509C5F61FFED146168F9136514C3CDA20D8 | sha256: B6DBB6155B7B3FBDAAE6E7F22F2817E8040424DC7D025488EA3822458663DDC2 | sha512: A182742F7A4DBB734E53EF4E3B80142C7E5C70664BC399654F1751ED46FF2276C24F317FD7B35255538CA4D42698FBFF13B042CC05075A2C3887F82101DC7EF2
src\Libs\netstandard2.0\Pode.pdb
 
src\LICENSE.txt
The MIT License (MIT)

Copyright (c) [2017-2024] [Matthew Kelly (Badgerati)]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



===========================================================================

Pode includes a number of components with separate copyright notices and license terms.  This product does not necessarily use all the open source components referred to below. Your use of the source code for these components is subject to the terms and conditions of the following licenses.

=============== TABLE OF CONTENTS =============================


SECTION 1: Apache License, V2.0
    >>> Authress-Engineering/openapi-explorer
    >>> stoplightio/elements
    >>> swagger-api/swagger-editor
    >>> swagger-api/swagger-ui
    >>> cloudbase/powershell-yaml
SECTION 2: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES
    >>> mrin9/RapiPdf
    >>> Redocly/redoc
    >>> Phil-Factor/PSYaml

-------------------- SECTION 1: Apache License, V2.0 --------------------


    >>> Authress-Engineering/openapi-explorer
        Apache License
        Version 2.0, January 2004
        http://www.apache.org/licenses/

        TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

        1. Definitions.

            "License" shall mean the terms and conditions for use, reproduction,
            and distribution as defined by Sections 1 through 9 of this document.

            "Licensor" shall mean the copyright owner or entity authorized by
            the copyright owner that is granting the License.

            "Legal Entity" shall mean the union of the acting entity and all
            other entities that control, are controlled by, or are under common
            control with that entity. For the purposes of this definition,
            "control" means (i) the power, direct or indirect, to cause the
            direction or management of such entity, whether by contract or
            otherwise, or (ii) ownership of fifty percent (50%) or more of the
            outstanding shares, or (iii) beneficial ownership of such entity.

            "You" (or "Your") shall mean an individual or Legal Entity
            exercising permissions granted by this License.

            "Source" form shall mean the preferred form for making modifications,
            including but not limited to software source code, documentation
            source, and configuration files.

            "Object" form shall mean any form resulting from mechanical
            transformation or translation of a Source form, including but
            not limited to compiled object code, generated documentation,
            and conversions to other media types.

            "Work" shall mean the work of authorship, whether in Source or
            Object form, made available under the License, as indicated by a
            copyright notice that is included in or attached to the work
            (an example is provided in the Appendix below).

            "Derivative Works" shall mean any work, whether in Source or Object
            form, that is based on (or derived from) the Work and for which the
            editorial revisions, annotations, elaborations, or other modifications
            represent, as a whole, an original work of authorship. For the purposes
            of this License, Derivative Works shall not include works that remain
            separable from, or merely link (or bind by name) to the interfaces of,
            the Work and Derivative Works thereof.

            "Contribution" shall mean any work of authorship, including
            the original version of the Work and any modifications or additions
            to that Work or Derivative Works thereof, that is intentionally
            submitted to Licensor for inclusion in the Work by the copyright owner
            or by an individual or Legal Entity authorized to submit on behalf of
            the copyright owner. For the purposes of this definition, "submitted"
            means any form of electronic, verbal, or written communication sent
            to the Licensor or its representatives, including but not limited to
            communication on electronic mailing lists, source code control systems,
            and issue tracking systems that are managed by, or on behalf of, the
            Licensor for the purpose of discussing and improving the Work, but
            excluding communication that is conspicuously marked or otherwise
            designated in writing by the copyright owner as "Not a Contribution."

            "Contributor" shall mean Licensor and any individual or Legal Entity
            on behalf of whom a Contribution has been received by Licensor and
            subsequently incorporated within the Work.

        2. Grant of Copyright License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            copyright license to reproduce, prepare Derivative Works of,
            publicly display, publicly perform, sublicense, and distribute the
            Work and such Derivative Works in Source or Object form.

        3. Grant of Patent License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            (except as stated in this section) patent license to make, have made,
            use, offer to sell, sell, import, and otherwise transfer the Work,
            where such license applies only to those patent claims licensable
            by such Contributor that are necessarily infringed by their
            Contribution(s) alone or by combination of their Contribution(s)
            with the Work to which such Contribution(s) was submitted. If You
            institute patent litigation against any entity (including a
            cross-claim or counterclaim in a lawsuit) alleging that the Work
            or a Contribution incorporated within the Work constitutes direct
            or contributory patent infringement, then any patent licenses
            granted to You under this License for that Work shall terminate
            as of the date such litigation is filed.

        4. Redistribution. You may reproduce and distribute copies of the
            Work or Derivative Works thereof in any medium, with or without
            modifications, and in Source or Object form, provided that You
            meet the following conditions:

            (a) You must give any other recipients of the Work or
                Derivative Works a copy of this License; and

            (b) You must cause any modified files to carry prominent notices
                stating that You changed the files; and

            (c) You must retain, in the Source form of any Derivative Works
                that You distribute, all copyright, patent, trademark, and
                attribution notices from the Source form of the Work,
                excluding those notices that do not pertain to any part of
                the Derivative Works; and

            (d) If the Work includes a "NOTICE" text file as part of its
                distribution, then any Derivative Works that You distribute must
                include a readable copy of the attribution notices contained
                within such NOTICE file, excluding those notices that do not
                pertain to any part of the Derivative Works, in at least one
                of the following places: within a NOTICE text file distributed
                as part of the Derivative Works; within the Source form or
                documentation, if provided along with the Derivative Works; or,
                within a display generated by the Derivative Works, if and
                wherever such third-party notices normally appear. The contents
                of the NOTICE file are for informational purposes only and
                do not modify the License. You may add Your own attribution
                notices within Derivative Works that You distribute, alongside
                or as an addendum to the NOTICE text from the Work, provided
                that such additional attribution notices cannot be construed
                as modifying the License.

            You may add Your own copyright statement to Your modifications and
            may provide additional or different license terms and conditions
            for use, reproduction, or distribution of Your modifications, or
            for any such Derivative Works as a whole, provided Your use,
            reproduction, and distribution of the Work otherwise complies with
            the conditions stated in this License.

        5. Submission of Contributions. Unless You explicitly state otherwise,
            any Contribution intentionally submitted for inclusion in the Work
            by You to the Licensor shall be under the terms and conditions of
            this License, without any additional terms or conditions.
            Notwithstanding the above, nothing herein shall supersede or modify
            the terms of any separate license agreement you may have executed
            with Licensor regarding such Contributions.

        6. Trademarks. This License does not grant permission to use the trade
            names, trademarks, service marks, or product names of the Licensor,
            except as required for reasonable and customary use in describing the
            origin of the Work and reproducing the content of the NOTICE file.

        7. Disclaimer of Warranty. Unless required by applicable law or
            agreed to in writing, Licensor provides the Work (and each
            Contributor provides its Contributions) on an "AS IS"," BASIS,
            WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
            implied, including, without limitation, any warranties or conditions
            of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
            PARTICULAR PURPOSE. You are solely responsible for determining the
            appropriateness of using or redistributing the Work and assume any
            risks associated with Your exercise of permissions under this License.

        8. Limitation of Liability. In no event and under no legal theory,
            whether in tort (including negligence), contract, or otherwise,
            unless required by applicable law (such as deliberate and grossly
            negligent acts) or agreed to in writing, shall any Contributor be
            liable to You for damages, including any direct, indirect, special,
            incidental, or consequential damages of any character arising as a
            result of this License or out of the use or inability to use the
            Work (including but not limited to damages for loss of goodwill,
            work stoppage, computer failure or malfunction, or any and all
            other commercial damages or losses), even if such Contributor
            has been advised of the possibility of such damages.

        9. Accepting Warranty or Additional Liability. While redistributing
            the Work or Derivative Works thereof, You may choose to offer,
            and charge a fee for, acceptance of support, warranty, indemnity,
            or other liability obligations and/or rights consistent with this
            License. However, in accepting such obligations, You may act only
            on Your own behalf and on Your sole responsibility, not on behalf
            of any other Contributor, and only if You agree to indemnify,
            defend, and hold each Contributor harmless for any liability
            incurred by, or claims asserted against, such Contributor by reason
            of your accepting any such warranty or additional liability.

        END OF TERMS AND CONDITIONS

        APPENDIX: How to apply the Apache License to your work.

            To apply the Apache License to your work, attach the following
            boilerplate notice, with the fields enclosed by brackets "[]"
            replaced with your own identifying information. (Don't include
            the brackets!)  The text should be enclosed in the appropriate
            comment syntax for the file format. We also recommend that a
            file or class name and description of purpose be included on the
            same "printed page" as the copyright notice for easier
            identification within third-party archives.

        Copyright [yyyy] [name of copyright owner]

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS"," BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.

        ADDITIONAL LICENSE INFORMATION
        name: openapi-explorer
        version: 0.0.0
        description: OpenAPI Explorer - API viewer with dynamically generated components, documentation, and interaction console
        author: Authress Developers <[email protected]>

    >>> stoplightio/elements

            Apache License
            Version 2.0, January 2004
            http://www.apache.org/licenses/

        TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

        1. Definitions.

            "License" shall mean the terms and conditions for use, reproduction,
            and distribution as defined by Sections 1 through 9 of this document.

            "Licensor" shall mean the copyright owner or entity authorized by
            the copyright owner that is granting the License.

            "Legal Entity" shall mean the union of the acting entity and all
            other entities that control, are controlled by, or are under common
            control with that entity. For the purposes of this definition,
            "control" means (i) the power, direct or indirect, to cause the
            direction or management of such entity, whether by contract or
            otherwise, or (ii) ownership of fifty percent (50%) or more of the
            outstanding shares, or (iii) beneficial ownership of such entity.

            "You" (or "Your") shall mean an individual or Legal Entity
            exercising permissions granted by this License.

            "Source" form shall mean the preferred form for making modifications,
            including but not limited to software source code, documentation
            source, and configuration files.

            "Object" form shall mean any form resulting from mechanical
            transformation or translation of a Source form, including but
            not limited to compiled object code, generated documentation,
            and conversions to other media types.

            "Work" shall mean the work of authorship, whether in Source or
            Object form, made available under the License, as indicated by a
            copyright notice that is included in or attached to the work
            (an example is provided in the Appendix below).

            "Derivative Works" shall mean any work, whether in Source or Object
            form, that is based on (or derived from) the Work and for which the
            editorial revisions, annotations, elaborations, or other modifications
            represent, as a whole, an original work of authorship. For the purposes
            of this License, Derivative Works shall not include works that remain
            separable from, or merely link (or bind by name) to the interfaces of,
            the Work and Derivative Works thereof.

            "Contribution" shall mean any work of authorship, including
            the original version of the Work and any modifications or additions
            to that Work or Derivative Works thereof, that is intentionally
            submitted to Licensor for inclusion in the Work by the copyright owner
            or by an individual or Legal Entity authorized to submit on behalf of
            the copyright owner. For the purposes of this definition, "submitted"
            means any form of electronic, verbal, or written communication sent
            to the Licensor or its representatives, including but not limited to
            communication on electronic mailing lists, source code control systems,
            and issue tracking systems that are managed by, or on behalf of, the
            Licensor for the purpose of discussing and improving the Work, but
            excluding communication that is conspicuously marked or otherwise
            designated in writing by the copyright owner as "Not a Contribution."

            "Contributor" shall mean Licensor and any individual or Legal Entity
            on behalf of whom a Contribution has been received by Licensor and
            subsequently incorporated within the Work.

        2. Grant of Copyright License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            copyright license to reproduce, prepare Derivative Works of,
            publicly display, publicly perform, sublicense, and distribute the
            Work and such Derivative Works in Source or Object form.

        3. Grant of Patent License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            (except as stated in this section) patent license to make, have made,
            use, offer to sell, sell, import, and otherwise transfer the Work,
            where such license applies only to those patent claims licensable
            by such Contributor that are necessarily infringed by their
            Contribution(s) alone or by combination of their Contribution(s)
            with the Work to which such Contribution(s) was submitted. If You
            institute patent litigation against any entity (including a
            cross-claim or counterclaim in a lawsuit) alleging that the Work
            or a Contribution incorporated within the Work constitutes direct
            or contributory patent infringement, then any patent licenses
            granted to You under this License for that Work shall terminate
            as of the date such litigation is filed.

        4. Redistribution. You may reproduce and distribute copies of the
            Work or Derivative Works thereof in any medium, with or without
            modifications, and in Source or Object form, provided that You
            meet the following conditions:

            (a) You must give any other recipients of the Work or
                Derivative Works a copy of this License; and

            (b) You must cause any modified files to carry prominent notices
                stating that You changed the files; and

            (c) You must retain, in the Source form of any Derivative Works
                that You distribute, all copyright, patent, trademark, and
                attribution notices from the Source form of the Work,
                excluding those notices that do not pertain to any part of
                the Derivative Works; and

            (d) If the Work includes a "NOTICE" text file as part of its
                distribution, then any Derivative Works that You distribute must
                include a readable copy of the attribution notices contained
                within such NOTICE file, excluding those notices that do not
                pertain to any part of the Derivative Works, in at least one
                of the following places: within a NOTICE text file distributed
                as part of the Derivative Works; within the Source form or
                documentation, if provided along with the Derivative Works; or,
                within a display generated by the Derivative Works, if and
                wherever such third-party notices normally appear. The contents
                of the NOTICE file are for informational purposes only and
                do not modify the License. You may add Your own attribution
                notices within Derivative Works that You distribute, alongside
                or as an addendum to the NOTICE text from the Work, provided
                that such additional attribution notices cannot be construed
                as modifying the License.

            You may add Your own copyright statement to Your modifications and
            may provide additional or different license terms and conditions
            for use, reproduction, or distribution of Your modifications, or
            for any such Derivative Works as a whole, provided Your use,
            reproduction, and distribution of the Work otherwise complies with
            the conditions stated in this License.

        5. Submission of Contributions. Unless You explicitly state otherwise,
            any Contribution intentionally submitted for inclusion in the Work
            by You to the Licensor shall be under the terms and conditions of
            this License, without any additional terms or conditions.
            Notwithstanding the above, nothing herein shall supersede or modify
            the terms of any separate license agreement you may have executed
            with Licensor regarding such Contributions.

        6. Trademarks. This License does not grant permission to use the trade
            names, trademarks, service marks, or product names of the Licensor,
            except as required for reasonable and customary use in describing the
            origin of the Work and reproducing the content of the NOTICE file.

        7. Disclaimer of Warranty. Unless required by applicable law or
            agreed to in writing, Licensor provides the Work (and each
            Contributor provides its Contributions) on an "AS IS"," BASIS,
            WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
            implied, including, without limitation, any warranties or conditions
            of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
            PARTICULAR PURPOSE. You are solely responsible for determining the
            appropriateness of using or redistributing the Work and assume any
            risks associated with Your exercise of permissions under this License.

        8. Limitation of Liability. In no event and under no legal theory,
            whether in tort (including negligence), contract, or otherwise,
            unless required by applicable law (such as deliberate and grossly
            negligent acts) or agreed to in writing, shall any Contributor be
            liable to You for damages, including any direct, indirect, special,
            incidental, or consequential damages of any character arising as a
            result of this License or out of the use or inability to use the
            Work (including but not limited to damages for loss of goodwill,
            work stoppage, computer failure or malfunction, or any and all
            other commercial damages or losses), even if such Contributor
            has been advised of the possibility of such damages.

        9. Accepting Warranty or Additional Liability. While redistributing
            the Work or Derivative Works thereof, You may choose to offer,
            and charge a fee for, acceptance of support, warranty, indemnity,
            or other liability obligations and/or rights consistent with this
            License. However, in accepting such obligations, You may act only
            on Your own behalf and on Your sole responsibility, not on behalf
            of any other Contributor, and only if You agree to indemnify,
            defend, and hold each Contributor harmless for any liability
            incurred by, or claims asserted against, such Contributor by reason
            of your accepting any such warranty or additional liability.

        END OF TERMS AND CONDITIONS

        Copyright 2018 Stoplight, Inc.

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS"," BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.

        ADDITIONAL LICENSE INFORMATION
        author: Stoplight <[email protected]>

    >>> swagger-api/swagger-editor


        Apache License
        Version 2.0, January 2004
        http://www.apache.org/licenses/

        TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

        1. Definitions.

            "License" shall mean the terms and conditions for use, reproduction,
            and distribution as defined by Sections 1 through 9 of this document.

            "Licensor" shall mean the copyright owner or entity authorized by
            the copyright owner that is granting the License.

            "Legal Entity" shall mean the union of the acting entity and all
            other entities that control, are controlled by, or are under common
            control with that entity. For the purposes of this definition,
            "control" means (i) the power, direct or indirect, to cause the
            direction or management of such entity, whether by contract or
            otherwise, or (ii) ownership of fifty percent (50%) or more of the
            outstanding shares, or (iii) beneficial ownership of such entity.

            "You" (or "Your") shall mean an individual or Legal Entity
            exercising permissions granted by this License.

            "Source" form shall mean the preferred form for making modifications,
            including but not limited to software source code, documentation
            source, and configuration files.

            "Object" form shall mean any form resulting from mechanical
            transformation or translation of a Source form, including but
            not limited to compiled object code, generated documentation,
            and conversions to other media types.

            "Work" shall mean the work of authorship, whether in Source or
            Object form, made available under the License, as indicated by a
            copyright notice that is included in or attached to the work
            (an example is provided in the Appendix below).

            "Derivative Works" shall mean any work, whether in Source or Object
            form, that is based on (or derived from) the Work and for which the
            editorial revisions, annotations, elaborations, or other modifications
            represent, as a whole, an original work of authorship. For the purposes
            of this License, Derivative Works shall not include works that remain
            separable from, or merely link (or bind by name) to the interfaces of,
            the Work and Derivative Works thereof.

            "Contribution" shall mean any work of authorship, including
            the original version of the Work and any modifications or additions
            to that Work or Derivative Works thereof, that is intentionally
            submitted to Licensor for inclusion in the Work by the copyright owner
            or by an individual or Legal Entity authorized to submit on behalf of
            the copyright owner. For the purposes of this definition, "submitted"
            means any form of electronic, verbal, or written communication sent
            to the Licensor or its representatives, including but not limited to
            communication on electronic mailing lists, source code control systems,
            and issue tracking systems that are managed by, or on behalf of, the
            Licensor for the purpose of discussing and improving the Work, but
            excluding communication that is conspicuously marked or otherwise
            designated in writing by the copyright owner as "Not a Contribution."

            "Contributor" shall mean Licensor and any individual or Legal Entity
            on behalf of whom a Contribution has been received by Licensor and
            subsequently incorporated within the Work.

        2. Grant of Copyright License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            copyright license to reproduce, prepare Derivative Works of,
            publicly display, publicly perform, sublicense, and distribute the
            Work and such Derivative Works in Source or Object form.

        3. Grant of Patent License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            (except as stated in this section) patent license to make, have made,
            use, offer to sell, sell, import, and otherwise transfer the Work,
            where such license applies only to those patent claims licensable
            by such Contributor that are necessarily infringed by their
            Contribution(s) alone or by combination of their Contribution(s)
            with the Work to which such Contribution(s) was submitted. If You
            institute patent litigation against any entity (including a
            cross-claim or counterclaim in a lawsuit) alleging that the Work
            or a Contribution incorporated within the Work constitutes direct
            or contributory patent infringement, then any patent licenses
            granted to You under this License for that Work shall terminate
            as of the date such litigation is filed.

        4. Redistribution. You may reproduce and distribute copies of the
            Work or Derivative Works thereof in any medium, with or without
            modifications, and in Source or Object form, provided that You
            meet the following conditions:

            (a) You must give any other recipients of the Work or
                Derivative Works a copy of this License; and

            (b) You must cause any modified files to carry prominent notices
                stating that You changed the files; and

            (c) You must retain, in the Source form of any Derivative Works
                that You distribute, all copyright, patent, trademark, and
                attribution notices from the Source form of the Work,
                excluding those notices that do not pertain to any part of
                the Derivative Works; and

            (d) If the Work includes a "NOTICE" text file as part of its
                distribution, then any Derivative Works that You distribute must
                include a readable copy of the attribution notices contained
                within such NOTICE file, excluding those notices that do not
                pertain to any part of the Derivative Works, in at least one
                of the following places: within a NOTICE text file distributed
                as part of the Derivative Works; within the Source form or
                documentation, if provided along with the Derivative Works; or,
                within a display generated by the Derivative Works, if and
                wherever such third-party notices normally appear. The contents
                of the NOTICE file are for informational purposes only and
                do not modify the License. You may add Your own attribution
                notices within Derivative Works that You distribute, alongside
                or as an addendum to the NOTICE text from the Work, provided
                that such additional attribution notices cannot be construed
                as modifying the License.

            You may add Your own copyright statement to Your modifications and
            may provide additional or different license terms and conditions
            for use, reproduction, or distribution of Your modifications, or
            for any such Derivative Works as a whole, provided Your use,
            reproduction, and distribution of the Work otherwise complies with
            the conditions stated in this License.

        5. Submission of Contributions. Unless You explicitly state otherwise,
            any Contribution intentionally submitted for inclusion in the Work
            by You to the Licensor shall be under the terms and conditions of
            this License, without any additional terms or conditions.
            Notwithstanding the above, nothing herein shall supersede or modify
            the terms of any separate license agreement you may have executed
            with Licensor regarding such Contributions.

        6. Trademarks. This License does not grant permission to use the trade
            names, trademarks, service marks, or product names of the Licensor,
            except as required for reasonable and customary use in describing the
            origin of the Work and reproducing the content of the NOTICE file.

        7. Disclaimer of Warranty. Unless required by applicable law or
            agreed to in writing, Licensor provides the Work (and each
            Contributor provides its Contributions) on an "AS IS"," BASIS,
            WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
            implied, including, without limitation, any warranties or conditions
            of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
            PARTICULAR PURPOSE. You are solely responsible for determining the
            appropriateness of using or redistributing the Work and assume any
            risks associated with Your exercise of permissions under this License.

        8. Limitation of Liability. In no event and under no legal theory,
            whether in tort (including negligence), contract, or otherwise,
            unless required by applicable law (such as deliberate and grossly
            negligent acts) or agreed to in writing, shall any Contributor be
            liable to You for damages, including any direct, indirect, special,
            incidental, or consequential damages of any character arising as a
            result of this License or out of the use or inability to use the
            Work (including but not limited to damages for loss of goodwill,
            work stoppage, computer failure or malfunction, or any and all
            other commercial damages or losses), even if such Contributor
            has been advised of the possibility of such damages.

        9. Accepting Warranty or Additional Liability. While redistributing
            the Work or Derivative Works thereof, You may choose to offer,
            and charge a fee for, acceptance of support, warranty, indemnity,
            or other liability obligations and/or rights consistent with this
            License. However, in accepting such obligations, You may act only
            on Your own behalf and on Your sole responsibility, not on behalf
            of any other Contributor, and only if You agree to indemnify,
            defend, and hold each Contributor harmless for any liability
            incurred by, or claims asserted against, such Contributor by reason
            of your accepting any such warranty or additional liability.

        END OF TERMS AND CONDITIONS

        APPENDIX: How to apply the Apache License to your work.

            To apply the Apache License to your work, attach the following
            boilerplate notice, with the fields enclosed by brackets "[]"
            replaced with your own identifying information. (Don't include
            the brackets!)  The text should be enclosed in the appropriate
            comment syntax for the file format. We also recommend that a
            file or class name and description of purpose be included on the
            same "printed page" as the copyright notice for easier
            identification within third-party archives.

        Copyright [yyyy] [name of copyright owner]

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS"," BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.

        ADDITIONAL LICENSE INFORMATION
        name: swagger-editor
        description: Swagger Editor
        version: 4.11.2
        contributors (in alphabetical order):
            Anna Bodnia <[email protected]>
            Buu Nguyen <[email protected]>
            Josh Ponelat <[email protected]>
            Kyle Shockey <[email protected]>
            Robert Barnwell <[email protected]>
            Sahar Jafari <[email protected]>"

    >>> swagger-api/swagger-ui

        Apache License
        Version 2.0, January 2004
        http://www.apache.org/licenses/

        TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

        1. Definitions.

            "License" shall mean the terms and conditions for use, reproduction,
            and distribution as defined by Sections 1 through 9 of this document.

            "Licensor" shall mean the copyright owner or entity authorized by
            the copyright owner that is granting the License.

            "Legal Entity" shall mean the union of the acting entity and all
            other entities that control, are controlled by, or are under common
            control with that entity. For the purposes of this definition,
            "control" means (i) the power, direct or indirect, to cause the
            direction or management of such entity, whether by contract or
            otherwise, or (ii) ownership of fifty percent (50%) or more of the
            outstanding shares, or (iii) beneficial ownership of such entity.

            "You" (or "Your") shall mean an individual or Legal Entity
            exercising permissions granted by this License.

            "Source" form shall mean the preferred form for making modifications,
            including but not limited to software source code, documentation
            source, and configuration files.

            "Object" form shall mean any form resulting from mechanical
            transformation or translation of a Source form, including but
            not limited to compiled object code, generated documentation,
            and conversions to other media types.

            "Work" shall mean the work of authorship, whether in Source or
            Object form, made available under the License, as indicated by a
            copyright notice that is included in or attached to the work
            (an example is provided in the Appendix below).

            "Derivative Works" shall mean any work, whether in Source or Object
            form, that is based on (or derived from) the Work and for which the
            editorial revisions, annotations, elaborations, or other modifications
            represent, as a whole, an original work of authorship. For the purposes
            of this License, Derivative Works shall not include works that remain
            separable from, or merely link (or bind by name) to the interfaces of,
            the Work and Derivative Works thereof.

            "Contribution" shall mean any work of authorship, including
            the original version of the Work and any modifications or additions
            to that Work or Derivative Works thereof, that is intentionally
            submitted to Licensor for inclusion in the Work by the copyright owner
            or by an individual or Legal Entity authorized to submit on behalf of
            the copyright owner. For the purposes of this definition, "submitted"
            means any form of electronic, verbal, or written communication sent
            to the Licensor or its representatives, including but not limited to
            communication on electronic mailing lists, source code control systems,
            and issue tracking systems that are managed by, or on behalf of, the
            Licensor for the purpose of discussing and improving the Work, but
            excluding communication that is conspicuously marked or otherwise
            designated in writing by the copyright owner as "Not a Contribution."

            "Contributor" shall mean Licensor and any individual or Legal Entity
            on behalf of whom a Contribution has been received by Licensor and
            subsequently incorporated within the Work.

        2. Grant of Copyright License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            copyright license to reproduce, prepare Derivative Works of,
            publicly display, publicly perform, sublicense, and distribute the
            Work and such Derivative Works in Source or Object form.

        3. Grant of Patent License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            (except as stated in this section) patent license to make, have made,
            use, offer to sell, sell, import, and otherwise transfer the Work,
            where such license applies only to those patent claims licensable
            by such Contributor that are necessarily infringed by their
            Contribution(s) alone or by combination of their Contribution(s)
            with the Work to which such Contribution(s) was submitted. If You
            institute patent litigation against any entity (including a
            cross-claim or counterclaim in a lawsuit) alleging that the Work
            or a Contribution incorporated within the Work constitutes direct
            or contributory patent infringement, then any patent licenses
            granted to You under this License for that Work shall terminate
            as of the date such litigation is filed.

        4. Redistribution. You may reproduce and distribute copies of the
            Work or Derivative Works thereof in any medium, with or without
            modifications, and in Source or Object form, provided that You
            meet the following conditions:

            (a) You must give any other recipients of the Work or
                Derivative Works a copy of this License; and

            (b) You must cause any modified files to carry prominent notices
                stating that You changed the files; and

            (c) You must retain, in the Source form of any Derivative Works
                that You distribute, all copyright, patent, trademark, and
                attribution notices from the Source form of the Work,
                excluding those notices that do not pertain to any part of
                the Derivative Works; and

            (d) If the Work includes a "NOTICE" text file as part of its
                distribution, then any Derivative Works that You distribute must
                include a readable copy of the attribution notices contained
                within such NOTICE file, excluding those notices that do not
                pertain to any part of the Derivative Works, in at least one
                of the following places: within a NOTICE text file distributed
                as part of the Derivative Works; within the Source form or
                documentation, if provided along with the Derivative Works; or,
                within a display generated by the Derivative Works, if and
                wherever such third-party notices normally appear. The contents
                of the NOTICE file are for informational purposes only and
                do not modify the License. You may add Your own attribution
                notices within Derivative Works that You distribute, alongside
                or as an addendum to the NOTICE text from the Work, provided
                that such additional attribution notices cannot be construed
                as modifying the License.

            You may add Your own copyright statement to Your modifications and
            may provide additional or different license terms and conditions
            for use, reproduction, or distribution of Your modifications, or
            for any such Derivative Works as a whole, provided Your use,
            reproduction, and distribution of the Work otherwise complies with
            the conditions stated in this License.

        5. Submission of Contributions. Unless You explicitly state otherwise,
            any Contribution intentionally submitted for inclusion in the Work
            by You to the Licensor shall be under the terms and conditions of
            this License, without any additional terms or conditions.
            Notwithstanding the above, nothing herein shall supersede or modify
            the terms of any separate license agreement you may have executed
            with Licensor regarding such Contributions.

        6. Trademarks. This License does not grant permission to use the trade
            names, trademarks, service marks, or product names of the Licensor,
            except as required for reasonable and customary use in describing the
            origin of the Work and reproducing the content of the NOTICE file.

        7. Disclaimer of Warranty. Unless required by applicable law or
            agreed to in writing, Licensor provides the Work (and each
            Contributor provides its Contributions) on an "AS IS"," BASIS,
            WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
            implied, including, without limitation, any warranties or conditions
            of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
            PARTICULAR PURPOSE. You are solely responsible for determining the
            appropriateness of using or redistributing the Work and assume any
            risks associated with Your exercise of permissions under this License.

        8. Limitation of Liability. In no event and under no legal theory,
            whether in tort (including negligence), contract, or otherwise,
            unless required by applicable law (such as deliberate and grossly
            negligent acts) or agreed to in writing, shall any Contributor be
            liable to You for damages, including any direct, indirect, special,
            incidental, or consequential damages of any character arising as a
            result of this License or out of the use or inability to use the
            Work (including but not limited to damages for loss of goodwill,
            work stoppage, computer failure or malfunction, or any and all
            other commercial damages or losses), even if such Contributor
            has been advised of the possibility of such damages.

        9. Accepting Warranty or Additional Liability. While redistributing
            the Work or Derivative Works thereof, You may choose to offer,
            and charge a fee for, acceptance of support, warranty, indemnity,
            or other liability obligations and/or rights consistent with this
            License. However, in accepting such obligations, You may act only
            on Your own behalf and on Your sole responsibility, not on behalf
            of any other Contributor, and only if You agree to indemnify,
            defend, and hold each Contributor harmless for any liability
            incurred by, or claims asserted against, such Contributor by reason
            of your accepting any such warranty or additional liability.

        END OF TERMS AND CONDITIONS

        APPENDIX: How to apply the Apache License to your work.

            To apply the Apache License to your work, attach the following
            boilerplate notice, with the fields enclosed by brackets "[]"
            replaced with your own identifying information. (Don't include
            the brackets!)  The text should be enclosed in the appropriate
            comment syntax for the file format. We also recommend that a
            file or class name and description of purpose be included on the
            same "printed page" as the copyright notice for easier
            identification within third-party archives.

        Copyright [yyyy] [name of copyright owner]

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS"," BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.

        ADDITIONAL LICENSE INFORMATION
        name: swagger-ui
        version: 5.10.5
        contributors (in alphabetical order) :
            Anna Bodnia <[email protected]>
            Buu Nguyen <[email protected]>
            Josh Ponelat <[email protected]>
            Kyle Shockey <[email protected]>
            Robert Barnwell <[email protected]>
            Sahar Jafari <[email protected]>
            Vladimir Gorej <[email protected]>"

    >>> cloudbase/powershell-yaml
     Apache License
        Version 2.0, January 2004
        http://www.apache.org/licenses/

        TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

        1. Definitions.

            "License" shall mean the terms and conditions for use, reproduction,
            and distribution as defined by Sections 1 through 9 of this document.

            "Licensor" shall mean the copyright owner or entity authorized by
            the copyright owner that is granting the License.

            "Legal Entity" shall mean the union of the acting entity and all
            other entities that control, are controlled by, or are under common
            control with that entity. For the purposes of this definition,
            "control" means (i) the power, direct or indirect, to cause the
            direction or management of such entity, whether by contract or
            otherwise, or (ii) ownership of fifty percent (50%) or more of the
            outstanding shares, or (iii) beneficial ownership of such entity.

            "You" (or "Your") shall mean an individual or Legal Entity
            exercising permissions granted by this License.

            "Source" form shall mean the preferred form for making modifications,
            including but not limited to software source code, documentation
            source, and configuration files.

            "Object" form shall mean any form resulting from mechanical
            transformation or translation of a Source form, including but
            not limited to compiled object code, generated documentation,
            and conversions to other media types.

            "Work" shall mean the work of authorship, whether in Source or
            Object form, made available under the License, as indicated by a
            copyright notice that is included in or attached to the work
            (an example is provided in the Appendix below).

            "Derivative Works" shall mean any work, whether in Source or Object
            form, that is based on (or derived from) the Work and for which the
            editorial revisions, annotations, elaborations, or other modifications
            represent, as a whole, an original work of authorship. For the purposes
            of this License, Derivative Works shall not include works that remain
            separable from, or merely link (or bind by name) to the interfaces of,
            the Work and Derivative Works thereof.

            "Contribution" shall mean any work of authorship, including
            the original version of the Work and any modifications or additions
            to that Work or Derivative Works thereof, that is intentionally
            submitted to Licensor for inclusion in the Work by the copyright owner
            or by an individual or Legal Entity authorized to submit on behalf of
            the copyright owner. For the purposes of this definition, "submitted"
            means any form of electronic, verbal, or written communication sent
            to the Licensor or its representatives, including but not limited to
            communication on electronic mailing lists, source code control systems,
            and issue tracking systems that are managed by, or on behalf of, the
            Licensor for the purpose of discussing and improving the Work, but
            excluding communication that is conspicuously marked or otherwise
            designated in writing by the copyright owner as "Not a Contribution."

            "Contributor" shall mean Licensor and any individual or Legal Entity
            on behalf of whom a Contribution has been received by Licensor and
            subsequently incorporated within the Work.

        2. Grant of Copyright License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            copyright license to reproduce, prepare Derivative Works of,
            publicly display, publicly perform, sublicense, and distribute the
            Work and such Derivative Works in Source or Object form.

        3. Grant of Patent License. Subject to the terms and conditions of
            this License, each Contributor hereby grants to You a perpetual,
            worldwide, non-exclusive, no-charge, royalty-free, irrevocable
            (except as stated in this section) patent license to make, have made,
            use, offer to sell, sell, import, and otherwise transfer the Work,
            where such license applies only to those patent claims licensable
            by such Contributor that are necessarily infringed by their
            Contribution(s) alone or by combination of their Contribution(s)
            with the Work to which such Contribution(s) was submitted. If You
            institute patent litigation against any entity (including a
            cross-claim or counterclaim in a lawsuit) alleging that the Work
            or a Contribution incorporated within the Work constitutes direct
            or contributory patent infringement, then any patent licenses
            granted to You under this License for that Work shall terminate
            as of the date such litigation is filed.

        4. Redistribution. You may reproduce and distribute copies of the
            Work or Derivative Works thereof in any medium, with or without
            modifications, and in Source or Object form, provided that You
            meet the following conditions:

            (a) You must give any other recipients of the Work or
                Derivative Works a copy of this License; and

            (b) You must cause any modified files to carry prominent notices
                stating that You changed the files; and

            (c) You must retain, in the Source form of any Derivative Works
                that You distribute, all copyright, patent, trademark, and
                attribution notices from the Source form of the Work,
                excluding those notices that do not pertain to any part of
                the Derivative Works; and

            (d) If the Work includes a "NOTICE" text file as part of its
                distribution, then any Derivative Works that You distribute must
                include a readable copy of the attribution notices contained
                within such NOTICE file, excluding those notices that do not
                pertain to any part of the Derivative Works, in at least one
                of the following places: within a NOTICE text file distributed
                as part of the Derivative Works; within the Source form or
                documentation, if provided along with the Derivative Works; or,
                within a display generated by the Derivative Works, if and
                wherever such third-party notices normally appear. The contents
                of the NOTICE file are for informational purposes only and
                do not modify the License. You may add Your own attribution
                notices within Derivative Works that You distribute, alongside
                or as an addendum to the NOTICE text from the Work, provided
                that such additional attribution notices cannot be construed
                as modifying the License.

            You may add Your own copyright statement to Your modifications and
            may provide additional or different license terms and conditions
            for use, reproduction, or distribution of Your modifications, or
            for any such Derivative Works as a whole, provided Your use,
            reproduction, and distribution of the Work otherwise complies with
            the conditions stated in this License.

        5. Submission of Contributions. Unless You explicitly state otherwise,
            any Contribution intentionally submitted for inclusion in the Work
            by You to the Licensor shall be under the terms and conditions of
            this License, without any additional terms or conditions.
            Notwithstanding the above, nothing herein shall supersede or modify
            the terms of any separate license agreement you may have executed
            with Licensor regarding such Contributions.

        6. Trademarks. This License does not grant permission to use the trade
            names, trademarks, service marks, or product names of the Licensor,
            except as required for reasonable and customary use in describing the
            origin of the Work and reproducing the content of the NOTICE file.

        7. Disclaimer of Warranty. Unless required by applicable law or
            agreed to in writing, Licensor provides the Work (and each
            Contributor provides its Contributions) on an "AS IS" BASIS,
            WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
            implied, including, without limitation, any warranties or conditions
            of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
            PARTICULAR PURPOSE. You are solely responsible for determining the
            appropriateness of using or redistributing the Work and assume any
            risks associated with Your exercise of permissions under this License.

        8. Limitation of Liability. In no event and under no legal theory,
            whether in tort (including negligence), contract, or otherwise,
            unless required by applicable law (such as deliberate and grossly
            negligent acts) or agreed to in writing, shall any Contributor be
            liable to You for damages, including any direct, indirect, special,
            incidental, or consequential damages of any character arising as a
            result of this License or out of the use or inability to use the
            Work (including but not limited to damages for loss of goodwill,
            work stoppage, computer failure or malfunction, or any and all
            other commercial damages or losses), even if such Contributor
            has been advised of the possibility of such damages.

        9. Accepting Warranty or Additional Liability. While redistributing
            the Work or Derivative Works thereof, You may choose to offer,
            and charge a fee for, acceptance of support, warranty, indemnity,
            or other liability obligations and/or rights consistent with this
            License. However, in accepting such obligations, You may act only
            on Your own behalf and on Your sole responsibility, not on behalf
            of any other Contributor, and only if You agree to indemnify,
            defend, and hold each Contributor harmless for any liability
            incurred by, or claims asserted against, such Contributor by reason
            of your accepting any such warranty or additional liability.

        END OF TERMS AND CONDITIONS

        APPENDIX: How to apply the Apache License to your work.

            To apply the Apache License to your work, attach the following
            boilerplate notice, with the fields enclosed by brackets "{}"
            replaced with your own identifying information. (Don't include
            the brackets!)  The text should be enclosed in the appropriate
            comment syntax for the file format. We also recommend that a
            file or class name and description of purpose be included on the
            same "printed page" as the copyright notice for easier
            identification within third-party archives.

        Copyright 2016-2023 Cloudbase Solutions SRL

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

            http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.

        ADDITIONAL LICENSE INFORMATION
        name: powershell-yaml
        version: 0.4.7
        description: Powershell module for serializing and deserializing YAML
        author: Gabriel Adrian Samfira,Alessandro Pilotti
        CompanyName:  Cloudbase Solutions SRL



-------------------- SECTION 2: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES --------------------

    >>> mrin9/RapiPdf
        MIT License

        Permission is hereby granted, free of charge, to any person obtaining
        a copy of this software and associated documentation files (the
        "Software"), to deal in the Software without restriction, including
        without limitation the rights to use, copy, modify, merge, publish,
        distribute, sublicense, and/or sell copies of the Software, and to
        permit persons to whom the Software is furnished to do so, subject to
        the following conditions:

        The above copyright notice and this permission notice shall be
        included in all copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
        EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
        NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
        LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
        OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
        WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

        ADDITIONAL LICENSE INFORMATION
        name: rapipdf
        version: 2.2.1
        description: RapiPdf - Generate PDF from Open API spec
        author: Mrinmoy Majumdar <[email protected]>

    >>> Redocly/redoc
        The MIT License (MIT)

        Copyright (c) 2015-present, Rebilly, Inc.

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.

        ADDITIONAL LICENSE INFORMATION
        name: redoc
        version: 2.1.3
        description: ReDoc
        author: Roman Hotsiy <[email protected]>


        >>> rapi-doc/RapiDoc
        MIT License

        Copyright (c) 2022 Mrinmoy Majumdar

        Permission is hereby granted, free of charge, to any person obtaining
        a copy of this software and associated documentation files (the
        "Software"), to deal in the Software without restriction, including
        without limitation the rights to use, copy, modify, merge, publish,
        distribute, sublicense, and/or sell copies of the Software, and to
        permit persons to whom the Software is furnished to do so, subject to
        the following conditions:

        The above copyright notice and this permission notice shall be
        included in all copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
        EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
        NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
        LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
        OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
        WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

        ADDITIONAL LICENSE INFORMATION
        name: rapidoc
        version: 9.3.5-beta
        description: RapiDoc - Open API spec viewer with built in console
        author: Mrinmoy Majumdar <[email protected]>

    >>> Phil-Factor/PSYaml
        The MIT License (MIT)

        Copyright (c) 2016 Jakku Labs

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.

        ADDITIONAL LICENSE INFORMATION
        name: PSYaml
        version: 1.0.3
        description: PowerShell module used to intrepret Yaml formatted strings
        author: Phil-Factor, Pezhore
src\Misc\default-doc-bookmarks.html.pode
 
src\Misc\default-error-page.html.pode
 
src\Misc\default-error-page.json.pode
 
src\Misc\default-error-page.xml.pode
 
src\Misc\default-explorer.html.pode
 
src\Misc\default-file-browsing.html.pode
 
src\Misc\default-rapidoc.html.pode
 
src\Misc\default-rapipdf.html.pode
 
src\Misc\default-redoc.html.pode
 
src\Misc\default-stoplight.html.pode
 
src\Misc\default-swagger-editor.html.pode
 
src\Misc\default-swagger.html.pode
 
src\Pode.Internal.psd1
#
# Internal module manifest for module 'Pode'
#
# Generated by: Matthew Kelly (Badgerati)
#
# Generated on: 24/01/2023
#

@{
    # Script module or binary module file associated with this manifest.
    RootModule        = 'Pode.Internal.psm1'

    # Version number of this module.
    ModuleVersion     = '2.10.0'

    # ID used to uniquely identify this module
    GUID              = '86b48c1c-8b59-4f3c-80bb-936d6b3218f6'

    # Author of this module
    Author            = 'Matthew Kelly (Badgerati)'

    # Minimum version of the Windows PowerShell engine required by this module
    PowerShellVersion = '5.1'
}
src\Pode.Internal.psm1
# root path
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path

# import everything
$sysfuncs = Get-ChildItem Function:

# load private functions
Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }

# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }

# get functions from memory and compare to existing to find new functions added
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }

# export the module's public functions
Export-ModuleMember -Function ($funcs.Name)
src\Pode.psd1
#
# Module manifest for module 'Pode'
#
# Generated by: Matthew Kelly (Badgerati)
#
# Generated on: 28/11/2017
#

@{
    # Script module or binary module file associated with this manifest.
    RootModule        = 'Pode.psm1'

    # Version number of this module.
    ModuleVersion     = '2.10.0'

    # ID used to uniquely identify this module
    GUID              = 'e3ea217c-fc3d-406b-95d5-4304ab06c6af'

    # Author of this module
    Author            = 'Matthew Kelly (Badgerati)'

    # Copyright statement for this module
    Copyright         = 'Copyright (c) 2017-2024 Matthew Kelly (Badgerati), licensed under the MIT License.'

    # Description of the functionality provided by this module
    Description       = 'A Cross-Platform PowerShell framework for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.'

    # Minimum version of the Windows PowerShell engine required by this module
    PowerShellVersion = '5.1'

    # Functions to export from this Module
    FunctionsToExport = @(
        # cookies
        'Get-PodeCookie',
        'Get-PodeCookieSecret',
        'Remove-PodeCookie',
        'Set-PodeCookie',
        'Set-PodeCookieSecret',
        'Test-PodeCookie',
        'Test-PodeCookieSigned',
        'Update-PodeCookieExpiry',
        'Get-PodeCookieValue',

        # flash
        'Add-PodeFlashMessage',
        'Clear-PodeFlashMessages',
        'Get-PodeFlashMessage',
        'Get-PodeFlashMessageNames',
        'Remove-PodeFlashMessage',
        'Test-PodeFlashMessage',

        # headers
        'Add-PodeHeader',
        'Add-PodeHeaderBulk',
        'Test-PodeHeader',
        'Get-PodeHeader',
        'Set-PodeHeader',
        'Set-PodeHeaderBulk',
        'Test-PodeHeaderSigned',

        # state
        'Set-PodeState',
        'Get-PodeState',
        'Remove-PodeState',
        'Save-PodeState',
        'Restore-PodeState',
        'Test-PodeState',
        'Get-PodeStateNames',

        # response helpers
        'Set-PodeResponseAttachment',
        'Write-PodeTextResponse',
        'Write-PodeFileResponse',
        'Write-PodeCsvResponse',
        'Write-PodeHtmlResponse',
        'Write-PodeMarkdownResponse',
        'Write-PodeJsonResponse',
        'Write-PodeYamlResponse',
        'Write-PodeXmlResponse',
        'Write-PodeViewResponse',
        'Write-PodeDirectoryResponse',
        'Set-PodeResponseStatus',
        'Move-PodeResponseUrl',
        'Write-PodeTcpClient',
        'Read-PodeTcpClient',
        'Close-PodeTcpClient',
        'Save-PodeRequestFile',
        'Test-PodeRequestFile',
        'Set-PodeViewEngine',
        'Use-PodePartialView',
        'Send-PodeSignal',
        'Add-PodeViewFolder',
        'Send-PodeResponse',

        # sse
        'ConvertTo-PodeSseConnection',
        'Send-PodeSseEvent',
        'Close-PodeSseConnection',
        'Test-PodeSseClientIdSigned',
        'Test-PodeSseClientIdValid',
        'New-PodeSseClientId',
        'Enable-PodeSseSigning',
        'Disable-PodeSseSigning',
        'Set-PodeSseBroadcastLevel',
        'Get-PodeSseBroadcastLevel',
        'Test-PodeSseBroadcastLevel',
        'Set-PodeSseDefaultScope',
        'Get-PodeSseDefaultScope',
        'Test-PodeSseName',
        'Test-PodeSseClientId',

        # utility helpers
        'Close-PodeDisposable',
        'Get-PodeServerPath',
        'Start-PodeStopwatch',
        'Use-PodeStream',
        'Use-PodeScript',
        'Get-PodeConfig',
        'Add-PodeEndware',
        'Use-PodeEndware',
        'Import-PodeModule',
        'Import-PodeSnapIn',
        'Protect-PodeValue',
        'Resolve-PodeValue',
        'Invoke-PodeScriptBlock',
        'Merge-PodeScriptblockArguments',
        'Test-PodeIsUnix',
        'Test-PodeIsWindows',
        'Test-PodeIsMacOS',
        'Test-PodeIsPSCore',
        'Test-PodeIsEmpty',
        'Out-PodeHost',
        'Write-PodeHost',
        'Test-PodeIsIIS',
        'Test-PodeIsHeroku',
        'Get-PodeIISApplicationPath',
        'Out-PodeVariable',
        'Test-PodeIsHosted',
        'New-PodeCron',
        'Test-PodeInRunspace',
        'ConvertFrom-PodeXml',
        'Set-PodeDefaultFolder',
        'Get-PodeDefaultFolder',

        # routes
        'Add-PodeRoute',
        'Add-PodeStaticRoute',
        'Add-PodeSignalRoute',
        'Remove-PodeRoute',
        'Remove-PodeStaticRoute',
        'Remove-PodeSignalRoute',
        'Clear-PodeRoutes',
        'Clear-PodeStaticRoutes',
        'Clear-PodeSignalRoutes',
        'ConvertTo-PodeRoute',
        'Add-PodePage',
        'Get-PodeRoute',
        'Get-PodeStaticRoute',
        'Get-PodeSignalRoute',
        'Use-PodeRoutes',
        'Add-PodeRouteGroup',
        'Add-PodeStaticRouteGroup',
        'Add-PodeSignalRouteGroup',
        'Set-PodeRouteIfExistsPreference',
        'Test-PodeRoute',
        'Test-PodeStaticRoute',
        'Test-PodeSignalRoute',

        # handlers
        'Add-PodeHandler',
        'Remove-PodeHandler',
        'Clear-PodeHandlers',
        'Use-PodeHandlers',

        # schedules
        'Add-PodeSchedule',
        'Remove-PodeSchedule',
        'Clear-PodeSchedule',
        'Invoke-PodeSchedule',
        'Edit-PodeSchedule',
        'Set-PodeScheduleConcurrency',
        'Get-PodeSchedule',
        'Get-PodeScheduleNextTrigger',
        'Use-PodeSchedules',
        'Test-PodeSchedule',
        'Clear-PodeSchedules',

        # timers
        'Add-PodeTimer',
        'Remove-PodeTimer',
        'Clear-PodeTimers',
        'Invoke-PodeTimer',
        'Edit-PodeTimer',
        'Get-PodeTimer',
        'Use-PodeTimers',
        'Test-PodeTimer',

        # tasks
        'Add-PodeTask',
        'Set-PodeTaskConcurrency',
        'Invoke-PodeTask',
        'Remove-PodeTask',
        'Clear-PodeTasks',
        'Edit-PodeTask',
        'Get-PodeTask',
        'Use-PodeTasks',
        'Close-PodeTask',
        'Test-PodeTaskCompleted',
        'Wait-PodeTask',

        # middleware
        'Add-PodeMiddleware',
        'Remove-PodeMiddleware',
        'Clear-PodeMiddleware',
        'Add-PodeAccessRule',
        'Add-PodeLimitRule',
        'New-PodeCsrfToken',
        'Get-PodeCsrfMiddleware',
        'Initialize-PodeCsrf',
        'Enable-PodeCsrfMiddleware',
        'Use-PodeMiddleware',
        'New-PodeMiddleware',
        'Add-PodeBodyParser',
        'Remove-PodeBodyParser',

        # sessions
        'Enable-PodeSessionMiddleware',
        'Remove-PodeSession',
        'Save-PodeSession',
        'Get-PodeSessionId',
        'Reset-PodeSessionExpiry',
        'Get-PodeSessionDuration',
        'Get-PodeSessionExpiry',
        'Test-PodeSessionsEnabled',
        'Get-PodeSessionTabId',
        'Get-PodeSessionInfo',
        'Test-PodeSessionScopeIsBrowser',

        # auth
        'New-PodeAuthScheme',
        'New-PodeAuthAzureADScheme',
        'New-PodeAuthTwitterScheme',
        'Add-PodeAuth',
        'Get-PodeAuth',
        'Clear-PodeAuth',
        'Add-PodeAuthWindowsAd',
        'Add-PodeAuthWindowsLocal',
        'Remove-PodeAuth',
        'Add-PodeAuthMiddleware',
        'Add-PodeAuthIIS',
        'Add-PodeAuthUserFile',
        'ConvertTo-PodeJwt',
        'ConvertFrom-PodeJwt',
        'Test-PodeJwt'
        'Use-PodeAuth',
        'ConvertFrom-PodeOIDCDiscovery',
        'Test-PodeAuthUser',
        'Merge-PodeAuth',
        'Test-PodeAuth',
        'Test-PodeAuthExists',
        'Get-PodeAuthUser',
        'Add-PodeAuthSession',

        # access
        'New-PodeAccessScheme',
        'Add-PodeAccess',
        'Add-PodeAccessCustom',
        'Get-PodeAccess',
        'Test-PodeAccessExists',
        'Test-PodeAccess',
        'Test-PodeAccessUser',
        'Test-PodeAccessRoute',
        'Merge-PodeAccess',
        'Remove-PodeAccess',
        'Clear-PodeAccess',
        'Add-PodeAccessMiddleware',
        'Use-PodeAccess',

        # logging
        'New-PodeLoggingMethod',
        'Enable-PodeRequestLogging',
        'Enable-PodeErrorLogging',
        'Disable-PodeRequestLogging',
        'Disable-PodeErrorLogging',
        'Add-PodeLogger',
        'Remove-PodeLogger',
        'Clear-PodeLoggers',
        'Write-PodeErrorLog',
        'Write-PodeLog',
        'Protect-PodeLogItem',
        'Use-PodeLogging',

        # core
        'Start-PodeServer',
        'Close-PodeServer',
        'Restart-PodeServer',
        'Start-PodeStaticServer',
        'Show-PodeGui',
        'Add-PodeEndpoint',
        'Get-PodeEndpoint',
        'Pode',
        'Get-PodeServerDefaultSecret',
        'Wait-PodeDebugger',
        'Get-PodeVersion',

        # openapi
        'Enable-PodeOpenApi',
        'Get-PodeOADefinition',
        'Select-PodeOADefinition',
        'Add-PodeOAResponse',
        'Remove-PodeOAResponse',
        'Set-PodeOARequest',
        'New-PodeOARequestBody',
        'Test-PodeOADefinitionTag',
        'Test-PodeOADefinition',

        # properties
        'New-PodeOAIntProperty',
        'New-PodeOANumberProperty',
        'New-PodeOAStringProperty',
        'New-PodeOABoolProperty',
        'New-PodeOAObjectProperty',
        'New-PodeOAMultiTypeProperty',
        'Merge-PodeOAProperty',
        'New-PodeOAComponentSchemaProperty',
        'ConvertTo-PodeOAParameter',
        'Set-PodeOARouteInfo',
        'Enable-PodeOAViewer',
        'Test-PodeOAJsonSchemaCompliance',
        'Add-PodeOAInfo',
        'Add-PodeOAExternalDoc',
        'New-PodeOAExternalDoc',
        'Add-PodeOATag',
        'Add-PodeOAServerEndpoint',
        'New-PodeOAExample',
        'New-PodeOAEncodingObject',
        'New-PodeOAResponse',
        'Add-PodeOACallBack',
        'New-PodeOAResponseLink',
        'New-PodeOAContentMediaType',
        'Add-PodeOAExternalRoute',
        'New-PodeOAServerEndpoint',
        'Test-PodeOAVersion',

        # Components
        'Add-PodeOAComponentResponse',
        'Add-PodeOAComponentSchema',
        'Add-PodeOAComponentRequestBody',
        'Add-PodeOAComponentHeader',
        'Add-PodeOAComponentExample',
        'Add-PodeOAComponentParameter',
        'Add-PodeOAComponentResponseLink',
        'Add-PodeOAComponentCallBack',
        'Add-PodeOAComponentPathItem',
        'Add-PodeOAWebhook',
        'Test-PodeOAComponent',
        'Remove-PodeOAComponent',

        # Metrics
        'Get-PodeServerUptime',
        'Get-PodeServerRestartCount',
        'Get-PodeServerRequestMetric',
        'Get-PodeServerSignalMetric',
        'Get-PodeServerActiveRequestMetric',
        'Get-PodeServerActiveSignalMetric',

        # AutoImport
        'Export-PodeModule',
        'Export-PodeSnapin',
        'Export-PodeFunction',
        'Export-PodeSecretVault',

        # Events
        'Register-PodeEvent',
        'Unregister-PodeEvent',
        'Test-PodeEvent',
        'Get-PodeEvent',
        'Clear-PodeEvent',
        'Use-PodeEvents',

        # Security
        'Add-PodeSecurityHeader',
        'Add-PodeSecurityContentSecurityPolicy',
        'Add-PodeSecurityPermissionsPolicy',
        'Remove-PodeSecurity',
        'Remove-PodeSecurityAccessControl',
        'Remove-PodeSecurityContentSecurityPolicy',
        'Remove-PodeSecurityContentTypeOptions',
        'Remove-PodeSecurityCrossOrigin',
        'Remove-PodeSecurityFrameOptions',
        'Remove-PodeSecurityHeader',
        'Remove-PodeSecurityPermissionsPolicy',
        'Remove-PodeSecurityReferrerPolicy',
        'Remove-PodeSecurityStrictTransportSecurity',
        'Set-PodeSecurity',
        'Set-PodeSecurityAccessControl',
        'Set-PodeSecurityContentSecurityPolicy',
        'Set-PodeSecurityContentTypeOptions',
        'Set-PodeSecurityCrossOrigin',
        'Set-PodeSecurityFrameOptions',
        'Set-PodeSecurityPermissionsPolicy',
        'Set-PodeSecurityReferrerPolicy',
        'Set-PodeSecurityStrictTransportSecurity',
        'Hide-PodeSecurityServer',
        'Show-PodeSecurityServer',

        # Verbs
        'Add-PodeVerb',
        'Remove-PodeVerb',
        'Clear-PodeVerbs',
        'Get-PodeVerb',
        'Use-PodeVerbs',

        # WebSockets
        'Set-PodeWebSocketConcurrency',
        'Connect-PodeWebSocket',
        'Disconnect-PodeWebSocket',
        'Remove-PodeWebSocket',
        'Send-PodeWebSocket',
        'Reset-PodeWebSocket',
        'Test-PodeWebSocket'

        # Secrets
        'Register-PodeSecretVault',
        'Unregister-PodeSecretVault',
        'Unlock-PodeSecretVault',
        'Get-PodeSecretVault',
        'Test-PodeSecretVault',
        'Mount-PodeSecret',
        'Dismount-PodeSecret',
        'Get-PodeSecret',
        'Test-PodeSecret',
        'Update-PodeSecret',
        'Remove-PodeSecret',
        'Read-PodeSecret',
        'Set-PodeSecret',

        # File Watchers
        'Add-PodeFileWatcher',
        'Test-PodeFileWatcher',
        'Get-PodeFileWatcher',
        'Remove-PodeFileWatcher',
        'Clear-PodeFileWatchers',
        'Use-PodeFileWatchers',

        # Threading
        'Lock-PodeObject',
        'New-PodeLockable',
        'Remove-PodeLockable',
        'Get-PodeLockable',
        'Test-PodeLockable',
        'Enter-PodeLockable',
        'Exit-PodeLockable',
        'Clear-PodeLockables',
        'New-PodeMutex',
        'Test-PodeMutex',
        'Get-PodeMutex',
        'Remove-PodeMutex',
        'Use-PodeMutex',
        'Enter-PodeMutex',
        'Exit-PodeMutex',
        'Clear-PodeMutexes',
        'New-PodeSemaphore',
        'Test-PodeSemaphore',
        'Get-PodeSemaphore',
        'Remove-PodeSemaphore',
        'Use-PodeSemaphore',
        'Enter-PodeSemaphore',
        'Exit-PodeSemaphore',
        'Clear-PodeSemaphores',

        # caching
        'Get-PodeCache',
        'Set-PodeCache',
        'Test-PodeCache',
        'Remove-PodeCache',
        'Clear-PodeCache',
        'Add-PodeCacheStorage',
        'Remove-PodeCacheStorage',
        'Get-PodeCacheStorage',
        'Test-PodeCacheStorage',
        'Set-PodeCacheDefaultStorage',
        'Get-PodeCacheDefaultStorage',
        'Set-PodeCacheDefaultTtl',
        'Get-PodeCacheDefaultTtl',

        # scoped variables
        'Convert-PodeScopedVariables',
        'Convert-PodeScopedVariable',
        'Add-PodeScopedVariable',
        'Remove-PodeScopedVariable',
        'Test-PodeScopedVariable',
        'Clear-PodeScopedVariables',
        'Get-PodeScopedVariable',
        'Use-PodeScopedVariables'
    )

    # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
    AliasesToExport   = @(
        'Enable-PodeOpenApiViewer',
        'Enable-PodeOA',
        'Get-PodeOpenApiDefinition',
        'New-PodeOASchemaProperty'
    )

    # 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       = @{
            # Tags applied to this module. These help with module discovery in online galleries.
            Tags         = @(
                'powershell', 'web', 'server', 'http', 'https', 'listener', 'rest', 'api', 'tcp',
                'smtp', 'websites', 'powershell-core', 'windows', 'unix', 'linux', 'pode', 'PSEdition_Core',
                'cross-platform', 'file-monitoring', 'multithreaded', 'schedule', 'middleware', 'session',
                'authentication', 'authorisation', 'authorization', 'arm', 'raspberry-pi', 'aws-lambda',
                'azure-functions', 'websockets', 'swagger', 'openapi', 'webserver', 'secrets', 'fim'
            )

            # A URL to the license for this module.
            LicenseUri   = 'https://raw.githubusercontent.com/Badgerati/Pode/master/LICENSE.txt'

            # A URL to the main website for this project.
            ProjectUri   = 'https://github.com/Badgerati/Pode'

            # A URL to an icon representing this module.
            IconUri      = 'https://raw.githubusercontent.com/Badgerati/Pode/master/images/icon.png'

            # Release notes for this particular version of the module
            ReleaseNotes = 'https://github.com/Badgerati/Pode/releases/tag/v2.10.0'
        }
        PwshVersions = @{
            Untested  = '7.1,7.0,6.2,6.1,6.0,5.0,4.0,3.0,2.0,1.0'
            Supported = '7.4,7.3,7.2,5.1'
        }
    }
}
src\Pode.psm1
# root path
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path

# load assemblies
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Net.Http

# Construct the path to the module manifest (.psd1 file)
$moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1'

# Import the module manifest to access its properties
$moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath

$podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' }

if ($podeDll) {
    if ( $moduleManifest.ModuleVersion -ne '$version$') {
        $moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0'))
        if ($podeDll.GetName().Version -ne $moduleVersion) {
            throw "An existing incompatible Pode.DLL version $($podeDll.GetName().Version) is loaded. Version $moduleVersion is required. Open a new Powershell/pwsh session and retry."
        }
    }
}
else {
    if ($PSVersionTable.PSVersion -ge [version]'7.4.0') {
        Add-Type -LiteralPath "$($root)/Libs/net8.0/Pode.dll" -ErrorAction Stop
    }
    elseif ($PSVersionTable.PSVersion -ge [version]'7.3.0') {
        Add-Type -LiteralPath "$($root)/Libs/net7.0/Pode.dll" -ErrorAction Stop
    }
    elseif ($PSVersionTable.PSVersion -ge [version]'7.2.0') {
        Add-Type -LiteralPath "$($root)/Libs/net6.0/Pode.dll" -ErrorAction Stop
    }
    else {
        Add-Type -LiteralPath "$($root)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop
    }
}

# load private functions
Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }

# only import public functions
$sysfuncs = Get-ChildItem Function:

# only import public alias
$sysaliases = Get-ChildItem Alias:

# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }

# 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 $_ }
# export the module's public functions
if ($funcs) {
    if ($aliases) {
        Export-ModuleMember -Function ($funcs.Name) -Alias $aliases.Name
    }
    else {
        Export-ModuleMember -Function ($funcs.Name)
    }
}
src\Private\Access.ps1
function Get-PodeAccessMiddlewareScript {
    return {
        param($opts)

        if ($null -eq $WebEvent.Auth) {
            Set-PodeResponseStatus -Code 403
            return $false
        }

        # test access
        $WebEvent.Auth.IsAuthorised = Invoke-PodeAccessValidation -Name $opts.Name

        # 403 if unauthorised
        if (!$WebEvent.Auth.IsAuthorised) {
            Set-PodeResponseStatus -Code 403
        }

        # run next middleware or stop?
        return $WebEvent.Auth.IsAuthorised
    }
}

function Invoke-PodeAccessValidation {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # get the access method
    $access = $PodeContext.Server.Authorisations.Methods[$Name]

    # if it's a merged access, re-call this function and check against "succeed" value
    if ($access.Merged) {
        foreach ($accName in $access.Access) {
            $result = Invoke-PodeAccessValidation -Name $accName

            # if the access passed, and we only need one access to pass, return true
            if ($result -and $access.PassOne) {
                return $true
            }

            # if the access failed, but we need all to pass, return false
            if (!$result -and !$access.PassOne) {
                return $false
            }
        }

        # if the last access failed, and we only need one access to pass, return false
        if (!$result -and $access.PassOne) {
            return $false
        }

        # if the last access succeeded, and we need all to pass, return true
        if ($result -and !$access.PassOne) {
            return $true
        }

        # default failure
        return $false
    }

    # main access validation logic
    return (Test-PodeAccessRoute -Name $Name)
}
src\Private\Authentication.ps1
function Get-PodeAuthBasicType {
    return {
        param($options)

        # get the auth header
        $header = (Get-PodeHeader -Name 'Authorization')
        if ($null -eq $header) {
            return @{
                Message = 'No Authorization header found'
                Code    = 401
            }
        }

        # ensure the first atom is basic (or opt override)
        $atoms = $header -isplit '\s+'
        if ($atoms.Length -lt 2) {
            return @{
                Message = 'Invalid Authorization header'
                Code    = 400
            }
        }

        if ($atoms[0] -ine $options.HeaderTag) {
            return @{
                Message = "Header is not for $($options.HeaderTag) Authorization"
                Code    = 400
            }
        }

        # decode the auth header
        try {
            $enc = [System.Text.Encoding]::GetEncoding($options.Encoding)
        }
        catch {
            return @{
                Message = 'Invalid encoding specified for Authorization'
                Code    = 400
            }
        }

        try {
            $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1]))
        }
        catch {
            return @{
                Message = 'Invalid Base64 string found in Authorization header'
                Code    = 400
            }
        }

        # validate and return user/result
        $index = $decoded.IndexOf(':')
        $username = $decoded.Substring(0, $index)
        $password = $decoded.Substring($index + 1)

        # build the result
        $result = @($username, $password)

        # convert to credential?
        if ($options.AsCredential) {
            $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force
            $creds = [pscredential]::new($username, $passSecure)
            $result = @($creds)
        }

        # return data for calling validator
        return $result
    }
}

function Get-PodeAuthOAuth2Type {
    return {
        param($options, $schemes)

        # set default scopes
        if (($null -eq $options.Scopes) -or ($options.Scopes.Length -eq 0)) {
            $options.Scopes = @('openid', 'profile', 'email')
        }

        $scopes = ($options.Scopes -join ' ')

        # if there's an error, fail
        if (![string]::IsNullOrWhiteSpace($WebEvent.Query['error'])) {
            return @{
                Message   = $WebEvent.Query['error']
                Code      = 401
                IsErrored = $true
            }
        }

        # set grant type
        $hasInnerScheme = (($null -ne $schemes) -and ($schemes.Length -gt 0))
        $grantType = 'authorization_code'
        if ($hasInnerScheme) {
            $grantType = 'password'
        }

        # if there's a code query param, or inner scheme, get access token
        if ($hasInnerScheme -or ![string]::IsNullOrWhiteSpace($WebEvent.Query['code'])) {
            try {
                # ensure the state is valid
                if ((Test-PodeSessionsInUse) -and ($WebEvent.Query['state'] -ne $WebEvent.Session.Data['__pode_oauth_state__'])) {
                    return @{
                        Message   = 'OAuth2 state returned is invalid'
                        Code      = 401
                        IsErrored = $true
                    }
                }

                # build tokenUrl query with client info
                $body = "client_id=$($options.Client.ID)"
                $body += "&grant_type=$($grantType)"

                if (![string]::IsNullOrEmpty($options.Client.Secret)) {
                    $body += "&client_secret=$([System.Web.HttpUtility]::UrlEncode($options.Client.Secret))"
                }

                # add PKCE code verifier
                if ($options.PKCE.Enabled) {
                    $body += "&code_verifier=$($WebEvent.Session.Data['__pode_oauth_code_verifier__'])"
                }

                # if there's an inner scheme, get the username/password, and set query
                if ($hasInnerScheme) {
                    $body += "&username=$($schemes[-1][0])"
                    $body += "&password=$($schemes[-1][1])"
                    $body += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))"
                }

                # otherwise, set query for auth_code
                else {
                    $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect
                    $body += "&code=$($WebEvent.Query['code'])"
                    $body += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))"
                }

                # POST the tokenUrl
                try {
                    $result = Invoke-RestMethod -Method Post -Uri $options.Urls.Token -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
                }
                catch [System.Net.WebException], [System.Net.Http.HttpRequestException] {
                    $response = Read-PodeWebExceptionDetails -ErrorRecord $_
                    $result = ($response.Body | ConvertFrom-Json)
                }

                # was there an error?
                if (![string]::IsNullOrWhiteSpace($result.error)) {
                    return @{
                        Message   = "$($result.error): $($result.error_description)"
                        Code      = 401
                        IsErrored = $true
                    }
                }

                # get user details - if url supplied
                if (![string]::IsNullOrWhiteSpace($options.Urls.User.Url)) {
                    try {
                        $user = Invoke-RestMethod -Method $options.Urls.User.Method -Uri $options.Urls.User.Url -Headers @{ Authorization = "Bearer $($result.access_token)" }
                    }
                    catch [System.Net.WebException], [System.Net.Http.HttpRequestException] {
                        $response = Read-PodeWebExceptionDetails -ErrorRecord $_
                        $user = ($response.Body | ConvertFrom-Json)
                    }

                    if (![string]::IsNullOrWhiteSpace($user.error)) {
                        return @{
                            Message   = "$($user.error): $($user.error_description)"
                            Code      = 401
                            IsErrored = $true
                        }
                    }
                }
                elseif (![string]::IsNullOrWhiteSpace($result.id_token)) {
                    try {
                        $user = ConvertFrom-PodeJwt -Token $result.id_token -IgnoreSignature
                    }
                    catch {
                        $user = @{ Provider = 'OAuth2' }
                    }
                }
                else {
                    $user = @{ Provider = 'OAuth2' }
                }

                # return the user for the validator
                return @($user, $result.access_token, $result.refresh_token, $result)
            }
            finally {
                if ($null -ne $WebEvent.Session.Data) {
                    # clear state
                    $WebEvent.Session.Data.Remove('__pode_oauth_state__')

                    # clear PKCE
                    if ($options.PKCE.Enabled) {
                        $WebEvent.Session.Data.Remove('__pode_oauth_code_verifier__')
                    }
                }
            }
        }

        # redirect to the authUrl - only if no inner scheme supplied
        if (!$hasInnerScheme) {
            # get the redirectUrl
            $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect

            # add authUrl query params
            $query = "client_id=$($options.Client.ID)"
            $query += '&response_type=code'
            $query += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))"
            $query += '&response_mode=query'
            $query += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))"

            # add csrf state
            if (Test-PodeSessionsInUse) {
                $guid = New-PodeGuid
                $WebEvent.Session.Data['__pode_oauth_state__'] = $guid
                $query += "&state=$($guid)"
            }

            # build a code verifier for PKCE, and add to query
            if ($options.PKCE.Enabled) {
                $guid = New-PodeGuid
                $codeVerifier = "$($guid)-$($guid)"
                $WebEvent.Session.Data['__pode_oauth_code_verifier__'] = $codeVerifier

                $codeChallenge = $codeVerifier
                if ($options.PKCE.CodeChallenge.Method -ieq 'S256') {
                    $codeChallenge = ConvertTo-PodeBase64UrlValue -Value (Invoke-PodeSHA256Hash -Value $codeChallenge) -NoConvert
                }

                $query += "&code_challenge=$($codeChallenge)"
                $query += "&code_challenge_method=$($options.PKCE.CodeChallenge.Method)"
            }

            # are custom parameters already on the URL?
            $url = $options.Urls.Authorise
            if (!$url.Contains('?')) {
                $url += '?'
            }
            else {
                $url += '&'
            }

            # redirect to OAuth2 endpoint
            Move-PodeResponseUrl -Url "$($url)$($query)"
            return @{ IsRedirected = $true }
        }

        # hmm, this is unexpected
        return @{
            Message   = 'Well, this is awkward...'
            Code      = 500
            IsErrored = $true
        }
    }
}

function Get-PodeOAuth2RedirectHost {
    param(
        [Parameter()]
        [string]
        $RedirectUrl
    )

    if ($RedirectUrl.StartsWith('/')) {
        if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) {
            $protocol = Get-PodeHeader -Name 'X-Forwarded-Proto'
            if ([string]::IsNullOrWhiteSpace($protocol)) {
                $protocol = 'https'
            }

            $domain = "$($protocol)://$($WebEvent.Request.Host)"
        }
        else {
            $domain = Get-PodeEndpointUrl
        }

        $RedirectUrl = "$($domain.TrimEnd('/'))$($RedirectUrl)"
    }

    return $RedirectUrl
}

function Get-PodeAuthClientCertificateType {
    return {
        param($options)
        $cert = $WebEvent.Request.ClientCertificate

        # ensure we have a client cert
        if ($null -eq $cert) {
            return @{
                Message = 'No client certificate supplied'
                Code    = 401
            }
        }

        # ensure the cert has a thumbprint
        if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) {
            return @{
                Message = 'Invalid client certificate supplied'
                Code    = 401
            }
        }

        # ensure the cert hasn't expired, or has it even started
        $now = [datetime]::Now
        if (($cert.NotAfter -lt $now) -or ($cert.NotBefore -gt $now)) {
            return @{
                Message = 'Invalid client certificate supplied'
                Code    = 401
            }
        }

        # return data for calling validator
        return @($cert, $WebEvent.Request.ClientCertificateErrors)
    }
}

function Get-PodeAuthApiKeyType {
    return {
        param($options)

        # get api key from appropriate location
        $apiKey = [string]::Empty

        switch ($options.Location.ToLowerInvariant()) {
            'header' {
                $apiKey = Get-PodeHeader -Name $options.LocationName
            }

            'query' {
                $apiKey = $WebEvent.Query[$options.LocationName]
            }

            'cookie' {
                $apiKey = Get-PodeCookieValue -Name $options.LocationName
            }
        }

        # 400 if no key
        if ([string]::IsNullOrWhiteSpace($apiKey)) {
            return @{
                Message = "No $($options.LocationName) $($options.Location) found"
                Code    = 400
            }
        }

        # build the result
        $apiKey = $apiKey.Trim()
        $result = @($apiKey)

        # convert as jwt?
        if ($options.AsJWT) {
            try {
                $payload = ConvertFrom-PodeJwt -Token $apiKey -Secret $options.Secret
                Test-PodeJwt -Payload $payload
            }
            catch {
                if ($_.Exception.Message -ilike '*jwt*') {
                    return @{
                        Message = $_.Exception.Message
                        Code    = 400
                    }
                }

                throw
            }

            $result = @($payload)
        }

        # return the result
        return $result
    }
}

function Get-PodeAuthBearerType {
    return {
        param($options)

        # get the auth header
        $header = (Get-PodeHeader -Name 'Authorization')
        if ($null -eq $header) {
            return @{
                Message   = 'No Authorization header found'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
                Code      = 400
            }
        }

        # ensure the first atom is bearer
        $atoms = $header -isplit '\s+'
        if ($atoms.Length -lt 2) {
            return @{
                Message   = 'Invalid Authorization header'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
                Code      = 400
            }
        }

        if ($atoms[0] -ine $options.HeaderTag) {
            return @{
                Message   = "Authorization header is not $($options.HeaderTag)"
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
                Code      = 400
            }
        }

        # 400 if no token
        $token = $atoms[1]
        if ([string]::IsNullOrWhiteSpace($token)) {
            return @{
                Message = 'No Bearer token found'
                Code    = 400
            }
        }

        # build the result
        $token = $token.Trim()
        $result = @($token)

        # convert as jwt?
        if ($options.AsJWT) {
            try {
                $payload = ConvertFrom-PodeJwt -Token $token -Secret $options.Secret
                Test-PodeJwt -Payload $payload
            }
            catch {
                if ($_.Exception.Message -ilike '*jwt*') {
                    return @{
                        Message = $_.Exception.Message
                        #https://www.rfc-editor.org/rfc/rfc6750 Bearer token should return 401
                        Code    = 401
                    }
                }

                throw
            }

            $result = @($payload)
        }

        # return the result
        return $result
    }
}

function Get-PodeAuthBearerPostValidator {
    return {
        param($token, $result, $options)

        # if there's no user, fail with challenge
        if (($null -eq $result) -or ($null -eq $result.User)) {
            return @{
                Message   = 'User not found'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_token)
                Code      = 401
            }
        }

        # check for an error and description
        if (![string]::IsNullOrWhiteSpace($result.Error)) {
            return @{
                Message   = 'Authorization failed'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription)
                Code      = 401
            }
        }

        # check the scopes
        $hasAuthScopes = (($null -ne $options.Scopes) -and ($options.Scopes.Length -gt 0))
        $hasTokenScope = ![string]::IsNullOrWhiteSpace($result.Scope)

        # 403 if we have auth scopes but no token scope
        if ($hasAuthScopes -and !$hasTokenScope) {
            return @{
                Message   = 'Invalid Scope'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope)
                Code      = 403
            }
        }

        # 403 if we have both, but token not in auth scope
        if ($hasAuthScopes -and $hasTokenScope -and ($options.Scopes -notcontains $result.Scope)) {
            return @{
                Message   = 'Invalid Scope'
                Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope)
                Code      = 403
            }
        }

        # return result
        return $result
    }
}

function New-PodeAuthBearerChallenge {
    param(
        [Parameter()]
        [string[]]
        $Scopes,

        [Parameter()]
        [ValidateSet('', 'invalid_request', 'invalid_token', 'insufficient_scope')]
        [string]
        $ErrorType,

        [Parameter()]
        [string]
        $ErrorDescription
    )

    $items = @()
    if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) {
        $items += "scope=`"$($Scopes -join ' ')`""
    }

    if (![string]::IsNullOrWhiteSpace($ErrorType)) {
        $items += "error=`"$($ErrorType)`""
    }

    if (![string]::IsNullOrWhiteSpace($ErrorDescription)) {
        $items += "error_description=`"$($ErrorDescription)`""
    }

    return ($items -join ', ')
}

function Get-PodeAuthDigestType {
    return {
        param($options)

        # get the auth header - send challenge if missing
        $header = (Get-PodeHeader -Name 'Authorization')
        if ($null -eq $header) {
            return @{
                Message   = 'No Authorization header found'
                Challenge = (New-PodeAuthDigestChallenge)
                Code      = 401
            }
        }

        # if auth header isn't digest send challenge
        $atoms = $header -isplit '\s+'
        if ($atoms.Length -lt 2) {
            return @{
                Message = 'Invalid Authorization header'
                Code    = 400
            }
        }

        if ($atoms[0] -ine $options.HeaderTag) {
            return @{
                Message   = "Authorization header is not $($options.HeaderTag)"
                Challenge = (New-PodeAuthDigestChallenge)
                Code      = 401
            }
        }

        # parse the other atoms of the header (after the scheme), return 400 if none
        $params = ConvertFrom-PodeAuthDigestHeader -Parts ($atoms[1..$($atoms.Length - 1)])
        if ($params.Count -eq 0) {
            return @{
                Message = 'Invalid Authorization header'
                Code    = 400
            }
        }

        # if no username then 401 and challenge
        if ([string]::IsNullOrWhiteSpace($params.username)) {
            return @{
                Message   = 'Authorization header is missing username'
                Challenge = (New-PodeAuthDigestChallenge)
                Code      = 401
            }
        }

        # return 400 if domain doesnt match request domain
        if ($WebEvent.Path -ine $params.uri) {
            return @{
                Message = 'Invalid Authorization header'
                Code    = 400
            }
        }

        # return data for calling validator
        return @($params.username, $params)
    }
}

function Get-PodeAuthDigestPostValidator {
    return {
        param($username, $params, $result, $options)

        # if there's no user or password, fail with challenge
        if (($null -eq $result) -or ($null -eq $result.User) -or [string]::IsNullOrWhiteSpace($result.Password)) {
            return @{
                Message   = 'User not found'
                Challenge = (New-PodeAuthDigestChallenge)
                Code      = 401
            }
        }

        # generate the first hash
        $hash1 = Invoke-PodeMD5Hash -Value "$($params.username):$($params.realm):$($result.Password)"

        # generate the second hash
        $hash2 = Invoke-PodeMD5Hash -Value "$($WebEvent.Method.ToUpperInvariant()):$($params.uri)"

        # generate final hash
        $final = Invoke-PodeMD5Hash -Value "$($hash1):$($params.nonce):$($params.nc):$($params.cnonce):$($params.qop):$($hash2)"

        # compare final hash to client response
        if ($final -ne $params.response) {
            return @{
                Message   = 'Hashes failed to match'
                Challenge = (New-PodeAuthDigestChallenge)
                Code      = 401
            }
        }

        # hashes are valid, remove password and return result
        $null = $result.Remove('Password')
        return $result
    }
}

function ConvertFrom-PodeAuthDigestHeader {
    param(
        [Parameter()]
        [string[]]
        $Parts
    )

    if (($null -eq $Parts) -or ($Parts.Length -eq 0)) {
        return @{}
    }

    $obj = @{}
    $value = ($Parts -join ' ')

    @($value -isplit ',(?=(?:[^"]|"[^"]*")*$)') | ForEach-Object {
        if ($_ -imatch '(?<name>\w+)=["]?(?<value>[^"]+)["]?$') {
            $obj[$Matches['name']] = $Matches['value']
        }
    }

    return $obj
}

function New-PodeAuthDigestChallenge {
    $items = @('qop="auth"', 'algorithm="MD5"', "nonce=`"$(New-PodeGuid -Secure -NoDashes)`"")
    return ($items -join ', ')
}

function Get-PodeAuthFormType {
    return {
        param($options)

        # get user/pass keys to get from payload
        $userField = $options.Fields.Username
        $passField = $options.Fields.Password

        # get the user/pass
        $username = $WebEvent.Data.$userField
        $password = $WebEvent.Data.$passField

        # if either are empty, fail auth
        if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
            return @{
                Message = 'Username or Password not supplied'
                Code    = 401
            }
        }

        # build the result
        $result = @($username, $password)

        # convert to credential?
        if ($options.AsCredential) {
            $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force
            $creds = [pscredential]::new($username, $passSecure)
            $result = @($creds)
        }

        # return data for calling validator
        return $result
    }
}

function Get-PodeAuthUserFileMethod {
    return {
        param($username, $password, $options)

        # using pscreds?
        if (($null -eq $options) -and ($username -is [pscredential])) {
            $_username = ([pscredential]$username).UserName
            $_password = ([pscredential]$username).GetNetworkCredential().Password
            $_options = [hashtable]$password
        }
        else {
            $_username = $username
            $_password = $password
            $_options = $options
        }

        # load the file
        $users = (Get-Content -Path $_options.FilePath -Raw | ConvertFrom-Json)

        # find the user by username - only use the first one
        $user = @(foreach ($_user in $users) {
                if ($_user.Username -ieq $_username) {
                    $_user
                    break
                }
            })[0]

        # fail if no user
        if ($null -eq $user) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        # check the user's password
        if (![string]::IsNullOrWhiteSpace($_options.HmacSecret)) {
            $hash = Invoke-PodeHMACSHA256Hash -Value $_password -Secret $_options.HmacSecret
        }
        else {
            $hash = Invoke-PodeSHA256Hash -Value $_password
        }

        if ($user.Password -ne $hash) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        # convert the user to a hashtable
        $user = @{
            Name     = $user.Name
            Username = $user.Username
            Email    = $user.Email
            Groups   = $user.Groups
            Metadata = $user.Metadata
        }

        # is the user valid for any users/groups?
        if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        $result = @{ User = $user }

        # call additional scriptblock if supplied
        if ($null -ne $_options.ScriptBlock.Script) {
            $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
        }

        # return final result, this could contain a user obj, or an error message from custom scriptblock
        return $result
    }
}

function Get-PodeAuthWindowsADMethod {
    return {
        param($username, $password, $options)

        # using pscreds?
        if (($null -eq $options) -and ($username -is [pscredential])) {
            $_username = ([pscredential]$username).UserName
            $_password = ([pscredential]$username).GetNetworkCredential().Password
            $_options = [hashtable]$password
        }
        else {
            $_username = $username
            $_password = $password
            $_options = $options
        }

        # parse username to remove domains
        $_username = (($_username -split '@')[0] -split '\\')[-1]

        # validate and retrieve the AD user
        $noGroups = $_options.NoGroups
        $directGroups = $_options.DirectGroups
        $keepCredential = $_options.KeepCredential

        $result = Get-PodeAuthADResult `
            -Server $_options.Server `
            -Domain $_options.Domain `
            -SearchBase $_options.SearchBase `
            -Username $_username `
            -Password $_password `
            -Provider $_options.Provider `
            -NoGroups:$noGroups `
            -DirectGroups:$directGroups `
            -KeepCredential:$keepCredential

        # if there's a message, fail and return the message
        if (![string]::IsNullOrWhiteSpace($result.Message)) {
            return $result
        }

        # if there's no user, then, err, oops
        if (Test-PodeIsEmpty $result.User) {
            return @{ Message = 'An unexpected error occured' }
        }

        # is the user valid for any users/groups - if not, error!
        if (!(Test-PodeAuthUserGroups -User $result.User -Users $_options.Users -Groups $_options.Groups)) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        # call additional scriptblock if supplied
        if ($null -ne $_options.ScriptBlock.Script) {
            $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
        }

        # return final result, this could contain a user obj, or an error message from custom scriptblock
        return $result
    }
}

function Invoke-PodeAuthInbuiltScriptBlock {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $User,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $UsingVariables,

        [switch]
        $NoSplat
    )

    return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return -Splat:(!$NoSplat))
}

function Get-PodeAuthWindowsLocalMethod {
    return {
        param($username, $password, $options)

        # using pscreds?
        if (($null -eq $options) -and ($username -is [pscredential])) {
            $_username = ([pscredential]$username).UserName
            $_password = ([pscredential]$username).GetNetworkCredential().Password
            $_options = [hashtable]$password
        }
        else {
            $_username = $username
            $_password = $password
            $_options = $options
        }

        $user = @{
            UserType           = 'Local'
            AuthenticationType = 'WinNT'
            Username           = $_username
            Name               = [string]::Empty
            Fqdn               = $PodeContext.Server.ComputerName
            Domain             = 'localhost'
            Groups             = @()
        }

        Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
        $context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Machine', $PodeContext.Server.ComputerName)
        $valid = $context.ValidateCredentials($_username, $_password)

        if (!$valid) {
            return @{ Message = 'Invalid credentials supplied' }
        }

        try {
            $tmpUsername = $_username -replace '\\', '/'
            if ($_username -inotlike "$($PodeContext.Server.ComputerName)*") {
                $tmpUsername = "$($PodeContext.Server.ComputerName)/$($_username)"
            }

            $ad = [adsi]"WinNT://$($tmpUsername)"
            $user.Name = @($ad.FullName)[0]

            if (!$_options.NoGroups) {
                $cmd = "`$ad = [adsi]'WinNT://$($tmpUsername)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })"
                $user.Groups = [string[]](powershell -c $cmd)
            }
        }
        finally {
            Close-PodeDisposable -Disposable $ad -Close
        }

        # is the user valid for any users/groups - if not, error!
        if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        $result = @{ User = $user }

        # call additional scriptblock if supplied
        if ($null -ne $_options.ScriptBlock.Script) {
            $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
        }

        # return final result, this could contain a user obj, or an error message from custom scriptblock
        return $result
    }
}

function Get-PodeAuthWindowsADIISMethod {
    return {
        param($token, $options)

        # get the close handler
        $win32Handler = Add-Type -Name Win32CloseHandle -PassThru -MemberDefinition @'
            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool CloseHandle(IntPtr handle);
'@

        try {
            # parse the auth token and get the user
            $winAuthToken = [System.IntPtr][Int]"0x$($token)"
            $winIdentity = New-Object System.Security.Principal.WindowsIdentity($winAuthToken, 'Windows')

            # get user and domain
            $username = ($winIdentity.Name -split '\\')[-1]
            $domain = ($winIdentity.Name -split '\\')[0]

            # create base user object
            $user = @{
                UserType           = 'Domain'
                Identity           = @{
                    AccessToken = $winIdentity.AccessToken
                }
                AuthenticationType = $winIdentity.AuthenticationType
                DistinguishedName  = [string]::Empty
                Username           = $username
                Name               = [string]::Empty
                Email              = [string]::Empty
                Fqdn               = [string]::Empty
                Domain             = $domain
                Groups             = @()
            }

            # if the domain isn't local, attempt AD user
            if (![string]::IsNullOrWhiteSpace($domain) -and (@('.', $PodeContext.Server.ComputerName) -inotcontains $domain)) {
                # get the server's fdqn (and name/email)
                try {
                    # Open ADSISearcher and change context to given domain
                    $searcher = [adsisearcher]''
                    $searcher.SearchRoot = [adsi]"LDAP://$($domain)"
                    $searcher.Filter = "ObjectSid=$($winIdentity.User.Value.ToString())"

                    # Query the ADSISearcher for the above defined SID
                    $ad = $searcher.FindOne()

                    # Save it to our existing array for later usage
                    $user.DistinguishedName = @($ad.Properties.distinguishedname)[0]
                    $user.Name = @($ad.Properties.name)[0]
                    $user.Email = @($ad.Properties.mail)[0]
                    $user.Fqdn = (Get-PodeADServerFromDistinguishedName -DistinguishedName $user.DistinguishedName)
                }
                finally {
                    Close-PodeDisposable -Disposable $searcher
                }

                try {
                    if (!$options.NoGroups) {

                        # open a new connection
                        $result = (Open-PodeAuthADConnection -Server $user.Fqdn -Domain $domain -Provider $options.Provider)
                        if (!$result.Success) {
                            return @{ Message = "Failed to connect to Domain Server '$($user.Fqdn)' of $domain for $($user.DistinguishedName)." }
                        }

                        # get the connection
                        $connection = $result.Connection

                        # get the users groups
                        $directGroups = $options.DirectGroups
                        $user.Groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider)
                    }
                }
                finally {
                    if ($null -ne $connection) {
                        Close-PodeDisposable -Disposable $connection.Searcher
                        Close-PodeDisposable -Disposable $connection.Entry -Close
                        $connection.Credential = $null
                    }
                }
            }

            # otherwise, get details of local user
            else {
                # get the user's name and groups
                try {
                    $user.UserType = 'Local'

                    if (!$options.NoLocalCheck) {
                        $localUser = $winIdentity.Name -replace '\\', '/'
                        $ad = [adsi]"WinNT://$($localUser)"
                        $user.Name = @($ad.FullName)[0]

                        # dirty, i know :/ - since IIS runs using pwsh, the InvokeMember part fails
                        # we can safely call windows powershell here, as IIS is only on windows.
                        if (!$options.NoGroups) {
                            $cmd = "`$ad = [adsi]'WinNT://$($localUser)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })"
                            $user.Groups = [string[]](powershell -c $cmd)
                        }
                    }
                }
                finally {
                    Close-PodeDisposable -Disposable $ad -Close
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            return @{ Message = 'Failed to retrieve user using Authentication Token' }
        }
        finally {
            $win32Handler::CloseHandle($winAuthToken)
        }

        # is the user valid for any users/groups - if not, error!
        if (!(Test-PodeAuthUserGroups -User $user -Users $options.Users -Groups $options.Groups)) {
            return @{ Message = 'You are not authorised to access this website' }
        }

        $result = @{ User = $user }

        # call additional scriptblock if supplied
        if ($null -ne $options.ScriptBlock.Script) {
            $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables
        }

        # return final result, this could contain a user obj, or an error message from custom scriptblock
        return $result
    }
}

function Test-PodeAuthUserGroups {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $User,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter()]
        [string[]]
        $Groups
    )

    $haveUsers = (($null -ne $Users) -and ($Users.Length -gt 0))
    $haveGroups = (($null -ne $Groups) -and ($Groups.Length -gt 0))

    # if there are no groups/users supplied, return user is valid
    if (!$haveUsers -and !$haveGroups) {
        return $true
    }

    # before checking supplied groups, is the user in the supplied list of authorised users?
    if ($haveUsers -and (@($Users) -icontains $User.Username)) {
        return $true
    }

    # if there are groups supplied, check the user is a member of one
    if ($haveGroups) {
        foreach ($group in $Groups) {
            if (@($User.Groups) -icontains $group) {
                return $true
            }
        }
    }

    return $false
}

function Invoke-PodeAuthValidation {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # get auth method
    $auth = $PodeContext.Server.Authentications.Methods[$Name]

    # if it's a merged auth, re-call this function and check against "succeed" value
    if ($auth.Merged) {
        $results = @{}
        foreach ($authName in $auth.Authentications) {
            $result = Invoke-PodeAuthValidation -Name $authName

            # if the auth is trying to redirect, we need to bubble the this back now
            if ($result.Redirected) {
                return $result
            }

            # if the auth passed, and we only need one auth to pass, return current result
            if ($result.Success -and $auth.PassOne) {
                return $result
            }

            # if the auth failed, but we need all to pass, return current result
            if (!$result.Success -and !$auth.PassOne) {
                return $result
            }

            # remember result if we need all to pass
            if (!$auth.PassOne) {
                $results[$authName] = $result
            }
        }
        # if the last auth failed, and we only need one auth to pass, set failure and return
        if (!$result.Success -and $auth.PassOne) {
            return $result
        }

        # if the last auth succeeded, and we need all to pass, merge users/headers and return result
        if ($result.Success -and !$auth.PassOne) {
            # invoke scriptblock, or use result of merge default
            if ($null -ne $auth.ScriptBlock.Script) {
                $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables -NoSplat
            }
            else {
                $result = $results[$auth.MergeDefault]
            }

            # reset default properties and return
            $result.Success = $true
            $result.Auth = $results.Keys
            return $result
        }

        # default failure
        return @{
            Success    = $false
            StatusCode = 500
        }
    }

    # main auth validation logic
    $result = (Test-PodeAuthValidation -Name $Name)
    $result.Auth = $Name
    return $result
}

function Test-PodeAuthValidation {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    try {
        # get auth method
        $auth = $PodeContext.Server.Authentications.Methods[$Name]

        # auth result
        $result = $null

        # run pre-auth middleware
        if ($null -ne $auth.Scheme.Middleware) {
            if (!(Invoke-PodeMiddleware -Middleware $auth.Scheme.Middleware)) {
                return @{
                    Success = $false
                }
            }
        }

        # run auth scheme script to parse request for data
        $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables)

        # call inner schemes first
        if ($null -ne $auth.Scheme.InnerScheme) {
            $schemes = @()

            $_scheme = $auth.Scheme
            $_inner = @(while ($null -ne $_scheme.InnerScheme) {
                    $_scheme = $_scheme.InnerScheme
                    $_scheme
                })

            for ($i = $_inner.Length - 1; $i -ge 0; $i--) {
                $_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables)

                $_tmp_args += , $schemes
                $result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat)
                if ($result -is [hashtable]) {
                    break
                }

                $schemes += , $result
                $result = $null
            }

            $_args += , $schemes
        }

        if ($null -eq $result) {
            $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat)
        }

        # if data is a hashtable, then don't call validator (parser either failed, or forced a success)
        if ($result -isnot [hashtable]) {
            $original = $result

            $_args = @($result) + @($auth.Arguments)
            $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat)

            # if we have user, then run post validator if present
            if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) {
                $_args = @($original) + @($result) + @($auth.Scheme.Arguments)
                $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat)
            }
        }

        # is the auth trying to redirect ie: oauth?
        if ($result.IsRedirected) {
            return @{
                Success    = $false
                Redirected = $true
            }
        }

        # if there's no result, or no user, then the auth failed - but allow auth if anon enabled
        if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) {
            $code = (Protect-PodeValue -Value $result.Code -Default 401)

            # set the www-auth header
            $validCode = (($code -eq 401) -or ![string]::IsNullOrEmpty($result.Challenge))

            if ($validCode) {
                if ($null -eq $result) {
                    $result = @{}
                }

                if ($null -eq $result.Headers) {
                    $result.Headers = @{}
                }

                if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) {
                    $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge
                    $result.Headers['WWW-Authenticate'] = $authHeader
                }
            }

            return @{
                Success         = $false
                StatusCode      = $code
                Description     = $result.Message
                Headers         = $result.Headers
                FailureRedirect = [bool]$result.IsErrored
            }
        }

        # authentication was successful
        return @{
            Success = $true
            User    = $result.User
            Headers = $result.Headers
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        return @{
            Success    = $false
            StatusCode = 500
            Exception  = $_
        }
    }
}

function Get-PodeAuthMiddlewareScript {
    return {
        param($opts)

        return Test-PodeAuthInternal `
            -Name $opts.Name `
            -Login:($opts.Login) `
            -Logout:($opts.Logout) `
            -AllowAnon:($opts.Anon)
    }
}

function Test-PodeAuthInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $Login,

        [switch]
        $Logout,

        [switch]
        $AllowAnon
    )

    # get the auth method
    $auth = $PodeContext.Server.Authentications.Methods[$Name]

    # check for logout command
    if ($Logout) {
        Remove-PodeAuthSession

        if ($PodeContext.Server.Sessions.Info.UseHeaders) {
            return Set-PodeAuthStatus `
                -StatusCode 401 `
                -Name $Name `
                -NoSuccessRedirect
        }
        else {
            $auth.Failure.Url = (Protect-PodeValue -Value $auth.Failure.Url -Default $WebEvent.Request.Url.AbsolutePath)
            return Set-PodeAuthStatus `
                -StatusCode 302 `
                -Name $Name `
                -NoSuccessRedirect
        }
    }

    # if the session already has a user/isAuth'd, then skip auth - or allow anon
    if (Test-PodeSessionsInUse) {
        # existing session auth'd
        if (Test-PodeAuthUser) {
            $WebEvent.Auth = $WebEvent.Session.Data.Auth
            return Set-PodeAuthStatus `
                -Name $Name `
                -LoginRoute:($Login) `
                -NoSuccessRedirect
        }

        # if we're allowing anon access, and using sessions, then stop here - as a session will be created from a login route for auth'ing users
        if ($AllowAnon) {
            if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
                Revoke-PodeSession
            }

            return $true
        }
    }

    # check if the login flag is set, in which case just return and load a login get-page (allowing anon access)
    if ($Login -and !$PodeContext.Server.Sessions.Info.UseHeaders -and ($WebEvent.Method -ieq 'get')) {
        if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
            Revoke-PodeSession
        }

        return $true
    }

    try {
        $result = Invoke-PodeAuthValidation -Name $Name
    }
    catch {
        $_ | Write-PodeErrorLog
        return Set-PodeAuthStatus `
            -StatusCode 500 `
            -Description $_.Exception.Message `
            -Name $Name
    }

    # did the auth force a redirect?
    if ($result.Redirected) {
        return $false
    }

    # if auth failed, are we allowing anon access?
    if (!$result.Success -and $AllowAnon) {
        return $true
    }

    # if auth failed, set appropriate response headers/redirects
    if (!$result.Success) {
        return Set-PodeAuthStatus `
            -StatusCode $result.StatusCode `
            -Description $result.Description `
            -Headers $result.Headers `
            -Name $Name `
            -LoginRoute:$Login `
            -NoFailureRedirect:($result.FailureRedirect)
    }

    # if auth passed, assign the user to the session
    $WebEvent.Auth = [ordered]@{
        User            = $result.User
        IsAuthenticated = $true
        IsAuthorised    = $true
        Store           = !$auth.Sessionless
        Name            = $result.Auth
    }

    # successful auth
    $authName = $null
    if ($auth.Merged -and !$auth.PassOne) {
        $authName = $Name
    }
    else {
        $authName = @($result.Auth)[0]
    }

    return Set-PodeAuthStatus `
        -Headers $result.Headers `
        -Name $authName `
        -LoginRoute:$Login
}

function Get-PodeAuthWwwHeaderValue {
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Realm,

        [Parameter()]
        [string]
        $Challenge
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return [string]::Empty
    }

    $header = $Name
    if (![string]::IsNullOrWhiteSpace($Realm)) {
        $header += " realm=`"$($Realm)`""
    }

    if (![string]::IsNullOrWhiteSpace($Challenge)) {
        $header += ", $($Challenge)"
    }

    return $header
}

function Remove-PodeAuthSession {
    # blank out the auth
    $WebEvent.Auth = @{}

    # if a session auth is found, blank it
    if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
        $WebEvent.Session.Data.Remove('Auth')
    }

    # Delete the current session (remove from store, blank it, and remove from Response)
    Revoke-PodeSession
}

function Get-PodeAuthFailureInfo {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $Info,

        [Parameter()]
        [string]
        $BaseName
    )

    # base name
    if ([string]::IsNullOrEmpty($BaseName)) {
        $BaseName = $Name
    }

    # get auth method
    $auth = $PodeContext.Server.Authentications.Methods[$Name]

    # cached failure?
    if ($null -ne $auth.Cache.Failure) {
        return $auth.Cache.Failure
    }

    # find failure info
    if ($null -eq $Info) {
        $Info = @{
            Url     = $auth.Failure.Url
            Message = $auth.Failure.Message
        }
    }

    if ([string]::IsNullOrEmpty($Info.Url)) {
        $Info.Url = $auth.Failure.Url
    }

    if ([string]::IsNullOrEmpty($Info.Message)) {
        $Info.Message = $auth.Failure.Message
    }

    if ((![string]::IsNullOrEmpty($Info.Url) -and ![string]::IsNullOrEmpty($Info.Message)) -or [string]::IsNullOrEmpty($auth.Parent)) {
        $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Failure = $Info
        return $Info
    }

    return (Get-PodeAuthFailureInfo -Name $auth.Parent -Info $Info -BaseName $BaseName)
}

function Get-PodeAuthSuccessInfo {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $Info,

        [Parameter()]
        [string]
        $BaseName
    )

    # base name
    if ([string]::IsNullOrEmpty($BaseName)) {
        $BaseName = $Name
    }

    # get auth method
    $auth = $PodeContext.Server.Authentications.Methods[$Name]

    # cached success?
    if ($null -ne $auth.Cache.Success) {
        return $auth.Cache.Success
    }

    # find success info
    if ($null -eq $Info) {
        $Info = @{
            Url       = $auth.Success.Url
            UseOrigin = $auth.Success.UseOrigin
        }
    }

    if ([string]::IsNullOrEmpty($Info.Url)) {
        $Info.Url = $auth.Success.Url
    }

    if (!$Info.UseOrigin) {
        $Info.UseOrigin = $auth.Success.UseOrigin
    }

    if ((![string]::IsNullOrEmpty($Info.Url) -and $Info.UseOrigin) -or [string]::IsNullOrEmpty($auth.Parent)) {
        $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Success = $Info
        return $Info
    }

    return (Get-PodeAuthSuccessInfo -Name $auth.Parent -Info $Info -BaseName $BaseName)
}

function Set-PodeAuthStatus {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $StatusCode = 0,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [hashtable]
        $Headers,

        [switch]
        $LoginRoute,

        [switch]
        $NoSuccessRedirect,

        [switch]
        $NoFailureRedirect
    )

    # if we have any headers, set them
    if (($null -ne $Headers) -and ($Headers.Count -gt 0)) {
        foreach ($key in $Headers.Keys) {
            Set-PodeHeader -Name $key -Value $Headers[$key]
        }
    }

    # get auth method
    $auth = $PodeContext.Server.Authentications.Methods[$Name]

    # cookie redirect name
    $redirectCookie = 'pode.redirecturl'

    # get Success object from auth
    $success = Get-PodeAuthSuccessInfo -Name $Name

    # if a statuscode supplied, assume failure
    if ($StatusCode -gt 0) {
        # get Failure object from auth
        $failure = Get-PodeAuthFailureInfo -Name $Name

        # override description with the failureMessage if supplied
        $Description = (Protect-PodeValue -Value $failure.Message -Default $Description)

        # add error to flash
        if ($LoginRoute -and !$auth.Sessionless -and ![string]::IsNullOrWhiteSpace($Description)) {
            Add-PodeFlashMessage -Name 'auth-error' -Message $Description
        }

        # check if we have a failure url redirect
        if (!$NoFailureRedirect -and ![string]::IsNullOrWhiteSpace($failure.Url)) {
            if ($success.UseOrigin -and ($WebEvent.Method -ieq 'get')) {
                $null = Set-PodeCookie -Name $redirectCookie -Value $WebEvent.Request.Url.PathAndQuery
            }

            Move-PodeResponseUrl -Url $failure.Url
        }
        else {
            Set-PodeResponseStatus -Code $StatusCode -Description $Description
        }

        return $false
    }

    # if no statuscode, success, so check if we have a success url redirect (but only for auto-login routes)
    if ((!$NoSuccessRedirect -or $LoginRoute) -and ![string]::IsNullOrWhiteSpace($success.Url)) {
        $url = $success.Url

        if ($success.UseOrigin) {
            $tmpUrl = Get-PodeCookieValue -Name $redirectCookie
            Remove-PodeCookie -Name $redirectCookie

            if (![string]::IsNullOrWhiteSpace($tmpUrl)) {
                $url = $tmpUrl
            }
        }

        Move-PodeResponseUrl -Url $url
        return $false
    }

    return $true
}

function Get-PodeADServerFromDistinguishedName {
    param(
        [Parameter()]
        [string]
        $DistinguishedName
    )

    if ([string]::IsNullOrWhiteSpace($DistinguishedName)) {
        return [string]::Empty
    }

    $parts = @($DistinguishedName -split ',')
    $name = @()

    foreach ($part in $parts) {
        if ($part -imatch '^DC=(?<name>.+)$') {
            $name += $Matches['name']
        }
    }

    return ($name -join '.')
}

function Get-PodeAuthADResult {
    param(
        [Parameter()]
        [string]
        $Server,

        [Parameter()]
        [string]
        $Domain,

        [Parameter()]
        [string]
        $SearchBase,

        [Parameter()]
        [string]
        $Username,

        [Parameter()]
        [string]
        $Password,

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider,

        [switch]
        $NoGroups,

        [switch]
        $DirectGroups,

        [switch]
        $KeepCredential
    )

    try {
        # validate the user's AD creds
        $result = (Open-PodeAuthADConnection -Server $Server -Domain $Domain -Username $Username -Password $Password -Provider $Provider)
        if (!$result.Success) {
            return @{ Message = 'Invalid credentials supplied' }
        }

        # get the connection
        $connection = $result.Connection

        # get the user
        $user = (Get-PodeAuthADUser -Connection $connection -Username $Username -Provider $Provider)
        if ($null -eq $user) {
            return @{ Message = 'User not found in Active Directory' }
        }

        # get the users groups
        $groups = @()
        if (!$NoGroups) {
            $groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider)
        }

        # check if we want to keep the credentials in the User object
        if ($KeepCredential) {
            $credential = [pscredential]::new($($Domain + '\' + $Username), (ConvertTo-SecureString -String $Password -AsPlainText -Force))
        }
        else {
            $credential = $null
        }

        # return the user
        return @{
            User = @{
                UserType           = 'Domain'
                AuthenticationType = 'LDAP'
                DistinguishedName  = $user.DistinguishedName
                Username           = ($Username -split '\\')[-1]
                Name               = $user.Name
                Email              = $user.Email
                Fqdn               = $Server
                Domain             = $Domain
                Groups             = $groups
                Credential         = $credential
            }
        }
    }
    finally {
        if ($null -ne $connection) {
            switch ($Provider.ToLowerInvariant()) {
                'openldap' {
                    $connection.Username = $null
                    $connection.Password = $null
                }

                'activedirectory' {
                    $connection.Credential = $null
                }

                'directoryservices' {
                    Close-PodeDisposable -Disposable $connection.Searcher
                    Close-PodeDisposable -Disposable $connection.Entry -Close
                }
            }
        }
    }
}

function Open-PodeAuthADConnection {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter()]
        [string]
        $Domain,

        [Parameter()]
        [string]
        $SearchBase,

        [Parameter()]
        [string]
        $Username,

        [Parameter()]
        [string]
        $Password,

        [Parameter()]
        [ValidateSet('LDAP', 'WinNT')]
        [string]
        $Protocol = 'LDAP',

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider
    )

    $result = $true
    $connection = $null

    # validate the user's AD creds
    switch ($Provider.ToLowerInvariant()) {
        'openldap' {
            if (![string]::IsNullOrWhiteSpace($SearchBase)) {
                $baseDn = $SearchBase
            }
            else {
                $baseDn = "DC=$(($Server -split '\.') -join ',DC=')"
            }

            $query = (Get-PodeAuthADQuery -Username $Username)
            $hostname = "$($Protocol)://$($Server)"

            $user = $Username
            if (!$Username.StartsWith($Domain)) {
                $user = "$($Domain)\$($Username)"
            }

            $null = (ldapsearch -x -LLL -H "$($hostname)" -D "$($user)" -w "$($Password)" -b "$($baseDn)" -o ldif-wrap=no "$($query)" dn)
            if (!$? -or ($LASTEXITCODE -ne 0)) {
                $result = $false
            }
            else {
                $connection = @{
                    Hostname = $hostname
                    Username = $user
                    BaseDN   = $baseDn
                    Password = $Password
                }
            }
        }

        'activedirectory' {
            try {
                $creds = [pscredential]::new($Username, (ConvertTo-SecureString -String $Password -AsPlainText -Force))
                $null = Get-ADUser -Identity $Username -Credential $creds -ErrorAction Stop
                $connection = @{
                    Credential = $creds
                }
            }
            catch {
                $result = $false
            }
        }

        'directoryservices' {
            if ([string]::IsNullOrWhiteSpace($Password)) {
                $ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)")
            }
            else {
                $ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)", "$($Username)", "$($Password)")
            }

            if (Test-PodeIsEmpty $ad.distinguishedName) {
                $result = $false
            }
            else {
                $connection = @{
                    Entry = $ad
                }
            }
        }
    }

    return @{
        Success    = $result
        Connection = $connection
    }
}

function Get-PodeAuthADQuery {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Username
    )

    return "(&(objectCategory=person)(samaccountname=$($Username)))"
}

function Get-PodeAuthADUser {
    param(
        [Parameter(Mandatory = $true)]
        $Connection,

        [Parameter(Mandatory = $true)]
        [string]
        $Username,

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider
    )

    $query = (Get-PodeAuthADQuery -Username $Username)
    $user = $null

    # generate query to find user
    switch ($Provider.ToLowerInvariant()) {
        'openldap' {
            $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" name mail)
            if (!$? -or ($LASTEXITCODE -ne 0)) {
                return $null
            }

            $user = @{
                DistinguishedName = (Get-PodeOpenLdapValue -Lines $result -Property 'dn')
                Name              = (Get-PodeOpenLdapValue -Lines $result -Property 'name')
                Email             = (Get-PodeOpenLdapValue -Lines $result -Property 'mail')
            }
        }

        'activedirectory' {
            $result = Get-ADUser -LDAPFilter $query -Credential $Connection.Credential -Properties mail
            $user = @{
                DistinguishedName = $result.DistinguishedName
                Name              = $result.Name
                Email             = $result.mail
            }
        }

        'directoryservices' {
            $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
            $Connection.Searcher.filter = $query

            $result = $Connection.Searcher.FindOne().Properties
            if (Test-PodeIsEmpty $result) {
                return $null
            }

            $user = @{
                DistinguishedName = @($result.distinguishedname)[0]
                Name              = @($result.name)[0]
                Email             = @($result.mail)[0]
            }
        }
    }

    return $user
}

function Get-PodeOpenLdapValue {
    param(
        [Parameter()]
        [string[]]
        $Lines,

        [Parameter()]
        [string]
        $Property,

        [switch]
        $All
    )

    foreach ($line in $Lines) {
        if ($line -imatch "^$($Property)\:\s+(?<$($Property)>.+)$") {
            # return the first found
            if (!$All) {
                return $Matches[$Property]
            }

            # return array of all
            $Matches[$Property]
        }
    }
}

function Get-PodeAuthADGroups {
    param(
        [Parameter(Mandatory = $true)]
        $Connection,

        [Parameter()]
        [string]
        $DistinguishedName,

        [Parameter()]
        [string]
        $Username,

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider,

        [switch]
        $Direct
    )

    if ($Direct) {
        return (Get-PodeAuthADGroupsDirect -Connection $Connection -Username $Username -Provider $Provider)
    }

    return (Get-PodeAuthADGroupsAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider)
}

function Get-PodeAuthADGroupsDirect {
    param(
        [Parameter(Mandatory = $true)]
        $Connection,

        [Parameter()]
        [string]
        $Username,

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider
    )

    # create the query
    $query = "(&(objectCategory=person)(samaccountname=$($Username)))"
    $groups = @()

    # get the groups
    switch ($Provider.ToLowerInvariant()) {
        'openldap' {
            $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" memberof)
            $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'memberof' -All)
        }

        'activedirectory' {
            $groups = (Get-ADPrincipalGroupMembership -Identity $Username -Credential $Connection.Credential).distinguishedName
        }

        'directoryservices' {
            if ($null -eq $Connection.Searcher) {
                $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
            }

            $Connection.Searcher.filter = $query
            $groups = @($Connection.Searcher.FindOne().Properties.memberof)
        }
    }

    $groups = @(foreach ($group in $groups) {
            if ($group -imatch '^CN=(?<group>.+?),') {
                $Matches['group']
            }
        })

    return $groups
}

function Get-PodeAuthADGroupsAll {
    param(
        [Parameter(Mandatory = $true)]
        $Connection,

        [Parameter()]
        [string]
        $DistinguishedName,

        [Parameter()]
        [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
        [string]
        $Provider
    )

    # create the query
    $query = "(member:1.2.840.113556.1.4.1941:=$($DistinguishedName))"
    $groups = @()

    # get the groups
    switch ($Provider.ToLowerInvariant()) {
        'openldap' {
            $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" samaccountname)
            $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'sAMAccountName' -All)
        }

        'activedirectory' {
            $groups = (Get-ADObject -LDAPFilter $query -Credential $Connection.Credential).Name
        }

        'directoryservices' {
            if ($null -eq $Connection.Searcher) {
                $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
            }

            $null = $Connection.Searcher.PropertiesToLoad.Add('samaccountname')
            $Connection.Searcher.filter = $query
            $groups = @($Connection.Searcher.FindAll().Properties.samaccountname)
        }
    }

    return $groups
}

function Get-PodeAuthDomainName {
    if (Test-PodeIsUnix) {
        $dn = (dnsdomainname)
        if ([string]::IsNullOrWhiteSpace($dn)) {
            $dn = (/usr/sbin/realm list --name-only)
        }

        return $dn
    }
    else {
        $domain = $env:USERDNSDOMAIN
        if ([string]::IsNullOrWhiteSpace($domain)) {
            $domain = (Get-CimInstance -Class Win32_ComputerSystem -Verbose:$false).Domain
        }

        return $domain
    }
}

function Find-PodeAuth {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeContext.Server.Authentications.Methods[$Name]
}

<#
.SYNOPSIS
  Expands a list of authentication names, including merged authentication methods.

.DESCRIPTION
  The Expand-PodeAuthMerge function takes an array of authentication names and expands it by resolving any merged authentication methods
  into their individual components. It is particularly useful in scenarios where authentication methods are combined or merged, and there
  is a need to process each individual method separately.

.PARAMETER Names
  An array of authentication method names. These names can include both discrete authentication methods and merged ones.

.EXAMPLE
  $expandedAuthNames = Expand-PodeAuthMerge -Names @('BasicAuth', 'CustomMergedAuth')

  Expands the provided authentication names, resolving 'CustomMergedAuth' into its constituent authentication methods if it's a merged one.
#>
function Expand-PodeAuthMerge {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Names
    )

    # Initialize a hashtable to store expanded authentication names
    $authNames = @{}

    # Iterate over each authentication name
    foreach ($authName in $Names) {
        # Handle the special case of anonymous access
        if ($authName -eq '%_allowanon_%') {
            $authNames[$authName] = $true
        }
        else {
            # Retrieve the authentication method from the Pode context
            $_auth = $PodeContext.Server.Authentications.Methods[$authName]

            # Check if the authentication is a merged one and expand it
            if ($_auth.merged) {
                foreach ($key in (Expand-PodeAuthMerge -Names $_auth.Authentications)) {
                    $authNames[$key] = $true
                }
            }
            else {
                # If not merged, add the authentication name to the list
                $authNames[$_auth.Name] = $true
            }
        }
    }

    # Return the keys of the hashtable, which are the expanded authentication names
    return $authNames.Keys
}


function Import-PodeAuthADModule {
    if (!(Test-PodeIsWindows)) {
        throw 'Active Directory module only available on Windows'
    }

    if (!(Test-PodeModuleInstalled -Name ActiveDirectory)) {
        throw 'Active Directory module is not installed'
    }

    Import-Module -Name ActiveDirectory -Force -ErrorAction Stop
    Export-PodeModule -Name ActiveDirectory
}

function Get-PodeAuthADProvider {
    param(
        [switch]
        $OpenLDAP,

        [switch]
        $ADModule
    )

    # openldap (literal, or not windows)
    if ($OpenLDAP -or !(Test-PodeIsWindows)) {
        return 'OpenLDAP'
    }

    # ad module
    if ($ADModule) {
        return 'ActiveDirectory'
    }

    # ds
    return 'DirectoryServices'
}
src\Private\AutoImport.ps1
function Import-PodeFunctionsIntoRunspaceState {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath
    )

    # do nothing if disabled
    if (!$PodeContext.Server.AutoImport.Functions.Enabled) {
        return
    }

    # if export only, and there are none, do nothing
    if ($PodeContext.Server.AutoImport.Functions.ExportOnly -and ($PodeContext.Server.AutoImport.Functions.ExportList.Length -eq 0)) {
        return
    }

    # script or file functions?
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'script' {
            $funcs = (Get-PodeFunctionsFromScriptBlock -ScriptBlock $ScriptBlock)
        }

        'file' {
            $funcs = (Get-PodeFunctionsFromFile -FilePath $FilePath)
        }
    }

    # looks like we have nothing!
    if (($null -eq $funcs) -or ($funcs.Length -eq 0)) {
        return
    }

    # groups funcs in case there or multiple definitions
    $funcs = ($funcs | Group-Object -Property { $_.Name })

    # import them, but also check if they're exported
    foreach ($func in $funcs) {
        # only exported funcs? is the func exported?
        if ($PodeContext.Server.AutoImport.Functions.ExportOnly -and ($PodeContext.Server.AutoImport.Functions.ExportList -inotcontains $func.Name)) {
            continue
        }

        # load the function
        $funcDef = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new($func.Name, $func.Group[-1].Definition)
        $PodeContext.RunspaceState.Commands.Add($funcDef)
    }
}

function Import-PodeModulesIntoRunspaceState {
    # do nothing if disabled
    if (!$PodeContext.Server.AutoImport.Modules.Enabled) {
        return
    }

    # if export only, and there are none, do nothing
    if ($PodeContext.Server.AutoImport.Modules.ExportOnly -and ($PodeContext.Server.AutoImport.Modules.ExportList.Length -eq 0)) {
        return
    }

    # get modules currently loaded in session
    $modules = Get-Module |
        Where-Object {
            ($_.Name -inotin @('pode', 'pode.internal')) -and ($_.Name -inotlike 'microsoft.powershell.*')
        } | Select-Object -Unique

    # work out which order the modules need to be loaded
    $modulesOrder = @(foreach ($module in $modules) {
            Get-PodeModuleDependencies -Module $module
        }) |
        Where-Object {
            ($_.Name -inotin @('pode', 'pode.internal')) -and ($_.Name -inotlike 'microsoft.powershell.*')
        } | Select-Object -Unique

    # load modules into runspaces, if allowed
    foreach ($module in $modulesOrder) {
        # only exported modules? is the module exported?
        if ($PodeContext.Server.AutoImport.Modules.ExportOnly -and ($PodeContext.Server.AutoImport.Modules.ExportList -inotcontains $module.Name)) {
            continue
        }

        # import the module
        $path = Find-PodeModuleFile -Module $module

        if (($module.ModuleType -ieq 'Manifest') -or ($path.EndsWith('.ps1'))) {
            $PodeContext.RunspaceState.ImportPSModule($path)
        }
        else {
            $PodeContext.Server.Modules[$module.Name] = $path
        }
    }
}

function Import-PodeSnapinsIntoRunspaceState {
    # if non-windows or core, do nothing
    if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
        return
    }

    # do nothing if disabled
    if (!$PodeContext.Server.AutoImport.Snapins.Enabled) {
        return
    }

    # if export only, and there are none, do nothing
    if ($PodeContext.Server.AutoImport.Snapins.ExportOnly -and ($PodeContext.Server.AutoImport.Snapins.ExportList.Length -eq 0)) {
        return
    }

    # load snapins into runspaces, if allowed
    $snapins = (Get-PSSnapin | Where-Object { !$_.IsDefault }).Name | Sort-Object -Unique

    foreach ($snapin in $snapins) {
        # only exported snapins? is the snapin exported?
        if ($PodeContext.Server.AutoImport.Snapins.ExportOnly -and ($PodeContext.Server.AutoImport.Snapins.ExportList -inotcontains $snapin)) {
            continue
        }

        $PodeContext.RunspaceState.ImportPSSnapIn($snapin, [ref]$null)
    }
}

function Initialize-PodeAutoImportConfiguration {
    return @{
        Modules      = @{
            Enabled    = $true
            ExportList = @()
            ExportOnly = $false
        }
        Snapins      = @{
            Enabled    = $true
            ExportList = @()
            ExportOnly = $false
        }
        Functions    = @{
            Enabled    = $true
            ExportList = @()
            ExportOnly = $false
        }
        SecretVaults = @{
            Enabled          = $true
            SecretManagement = @{
                Enabled    = $false
                ExportList = @()
                ExportOnly = $false
            }
        }
    }
}

function Import-PodeSecretVaultsIntoRegistry {
    # do nothing if disabled
    if (!$PodeContext.Server.AutoImport.SecretVaults.Enabled) {
        return
    }

    Import-PodeSecretManagementVaultsIntoRegistry
}

function Import-PodeSecretManagementVaultsIntoRegistry {
    # do nothing if disabled
    if (!$PodeContext.Server.AutoImport.SecretVaults.SecretManagement.Enabled) {
        return
    }

    # if export only, and there are none, do nothing
    if ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportOnly -and ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList.Length -eq 0)) {
        return
    }

    # error if SecretManagement module not installed
    if (!(Test-PodeModuleInstalled -Name Microsoft.PowerShell.SecretManagement)) {
        throw 'Microsoft.PowerShell.SecretManagement module not installed'
    }

    # import the module
    $null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false

    # get the current secret vaults
    $vaults = @(Get-SecretVault -ErrorAction Stop)

    # register the vaults
    foreach ($vault in $vaults) {
        # only exported vaults? is the vault exported?
        if ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportOnly -and ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList -inotcontains $vault.Name)) {
            continue
        }

        # is a vault with this name already registered?
        if (Test-PodeSecretVault -Name $vault.Name) {
            throw "A Secret Vault with the name '$($vault.Name)' has already been registered while auto-importing Secret Vaults"
        }

        # register the vault
        $PodeContext.Server.Secrets.Vaults[$vault.Name] = @{
            Name             = $vault.Name
            Type             = 'secretmanagement'
            Parameters       = $vault.VaultParameters
            AutoImported     = $true
            Unlock           = $null
            Cache            = $null
            SecretManagement = @{
                VaultName  = $vault.Name
                ModuleName = $vault.ModulePath
            }
        }
    }
}

function Read-PodeAutoImportConfiguration {
    param(
        [Parameter()]
        [hashtable]
        $Configuration
    )

    $impModules = $Configuration.AutoImport.Modules
    $impSnapins = $Configuration.AutoImport.Snapins
    $impFuncs = $Configuration.AutoImport.Functions
    $impSecretVaults = $Configuration.AutoImport.SecretVaults

    return @{
        Modules      = @{
            Enabled    = (($null -eq $impModules.Enable) -or [bool]$impModules.Enable)
            ExportList = @()
            ExportOnly = ([bool]$impModules.ExportOnly)
        }
        Snapins      = @{
            Enabled    = (($null -eq $impSnapins.Enable) -or [bool]$impSnapins.Enable)
            ExportList = @()
            ExportOnly = ([bool]$impSnapins.ExportOnly)
        }
        Functions    = @{
            Enabled    = (($null -eq $impFuncs.Enable) -or [bool]$impFuncs.Enable)
            ExportList = @()
            ExportOnly = ([bool]$impFuncs.ExportOnly)
        }
        SecretVaults = @{
            Enabled          = (($null -eq $impSecretVaults.Enable) -or [bool]$impSecretVaults.Enable)
            SecretManagement = @{
                Enabled    = ((($null -eq $impSecretVaults.Enable) -and (Test-PodeModuleInstalled -Name Microsoft.PowerShell.SecretManagement)) -or [bool]$impSecretVaults.Enable)
                ExportList = @()
                ExportOnly = ([bool]$impSecretVaults.SecretManagement.ExportOnly)
            }
        }
    }
}

function Reset-PodeAutoImportConfiguration {
    $PodeContext.Server.AutoImport.Modules.ExportList = @()
    $PodeContext.Server.AutoImport.Snapins.ExportList = @()
    $PodeContext.Server.AutoImport.Functions.ExportList = @()
    $PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList = @()
}
src\Private\Caching.ps1
function Get-PodeCacheInternal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [switch]
        $Metadata
    )

    $meta = $PodeContext.Server.Cache.Items[$Key]
    if ($null -eq $meta) {
        return $null
    }

    # check ttl/expiry
    if ($meta.Expiry -lt [datetime]::UtcNow) {
        Remove-PodeCacheInternal -Key $Key
        return $null
    }

    # return value an metadata if required
    if ($Metadata) {
        return $meta
    }

    # return just the value as default
    return $meta.Value
}

function Set-PodeCacheInternal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [int]
        $Ttl = 0
    )

    # crete (or update) value value
    $PodeContext.Server.Cache.Items[$Key] = @{
        Value  = $InputObject
        Ttl    = $Ttl
        Expiry = [datetime]::UtcNow.AddSeconds($Ttl)
    }
}

function Test-PodeCacheInternal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key
    )

    # if it's not in the cache at all, return false
    if (!$PodeContext.Server.Cache.Items.ContainsKey($Key)) {
        return $false
    }

    # fetch the items metadata, and check expiry. If it's expired return false.
    $meta = $PodeContext.Server.Cache.Items[$Key]

    # check ttl/expiry
    if ($meta.Expiry -lt [datetime]::UtcNow) {
        Remove-PodeCacheInternal -Key $Key
        return $false
    }

    # it exists, and isn't expired
    return $true
}

function Remove-PodeCacheInternal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key
    )

    Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock {
        $null = $PodeContext.Server.Cache.Items.Remove($Key)
    }
}

function Clear-PodeCacheInternal {
    Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock {
        $null = $PodeContext.Server.Cache.Items.Clear()
    }
}

function Start-PodeCacheHousekeeper {
    if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage))) {
        return
    }

    Add-PodeTimer -Name '__pode_cache_housekeeper__' -Interval 10 -ScriptBlock {
        $keys = Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -Return -ScriptBlock {
            if ($PodeContext.Server.Cache.Items.Count -eq 0) {
                return
            }

            return $PodeContext.Server.Cache.Items.Keys.Clone()
        }

        if (Test-PodeIsEmpty $keys) {
            return
        }

        $now = [datetime]::UtcNow

        foreach ($key in $keys) {
            if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) {
                Remove-PodeCacheInternal -Key $key
            }
        }
    }
}
src\Private\Context.ps1
using namespace Pode

function New-PodeContext {
    [CmdletBinding()]
    param(
        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $FilePath,

        [Parameter()]
        [int]
        $Threads = 1,

        [Parameter()]
        [int]
        $Interval = 0,

        [Parameter()]
        [string]
        $ServerRoot,

        [Parameter()]
        [string]
        $Name = $null,

        [Parameter()]
        [string]
        $ServerlessType,

        [Parameter()]
        [string]
        $StatusPageExceptions,

        [Parameter()]
        [string]
        $ListenerType,

        [Parameter()]
        [string[]]
        $EnablePool,

        [switch]
        $DisableTermination,

        [switch]
        $Quiet,

        [switch]
        $EnableBreakpoints
    )

    # set a random server name if one not supplied
    if (Test-PodeIsEmpty $Name) {
        $Name = Get-PodeRandomName
    }

    # are we running in a serverless context
    $isServerless = ![string]::IsNullOrWhiteSpace($ServerlessType)

    # ensure threads are always >0, for to 1 if we're serverless
    if (($Threads -le 0) -or $isServerless) {
        $Threads = 1
    }

    # basic context object
    $ctx = New-Object -TypeName psobject |
        Add-Member -MemberType NoteProperty -Name Threads -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Schedules -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Tasks -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspacePools -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Runspaces -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspaceState -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Tokens -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Threading -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Metrics -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Listeners -Value @() -PassThru |
        Add-Member -MemberType NoteProperty -Name Receivers -Value @() -PassThru |
        Add-Member -MemberType NoteProperty -Name Watchers -Value @() -PassThru |
        Add-Member -MemberType NoteProperty -Name Fim -Value @{} -PassThru

    # set the server name, logic and root, and other basic properties
    $ctx.Server.Name = $Name
    $ctx.Server.Logic = $ScriptBlock
    $ctx.Server.LogicPath = $FilePath
    $ctx.Server.Interval = $Interval
    $ctx.Server.PodeModule = (Get-PodeModuleDetails)
    $ctx.Server.DisableTermination = $DisableTermination.IsPresent
    $ctx.Server.Quiet = $Quiet.IsPresent
    $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName()

    # list of created listeners/receivers
    $ctx.Listeners = @()
    $ctx.Receivers = @()
    $ctx.Watchers = @()

    # default secret that can used when needed, and a secret isn't supplied
    $ctx.Server.DefaultSecret = New-PodeGuid -Secure

    # list of timers/schedules/tasks/fim
    $ctx.Timers = @{
        Enabled = ($EnablePool -icontains 'timers')
        Items   = @{}
    }

    $ctx.Schedules = @{
        Enabled   = ($EnablePool -icontains 'schedules')
        Items     = @{}
        Processes = @{}
    }

    $ctx.Tasks = @{
        Enabled = ($EnablePool -icontains 'tasks')
        Items   = @{}
        Results = @{}
    }

    $ctx.Fim = @{
        Enabled = ($EnablePool -icontains 'files')
        Items   = @{}
    }

    # auto importing (modules, funcs, snap-ins)
    $ctx.Server.AutoImport = Initialize-PodeAutoImportConfiguration

    # basic logging setup
    $ctx.Server.Logging = @{
        Enabled = $true
        Types   = @{}
    }

    # set thread counts
    $ctx.Threads = @{
        General    = $Threads
        Schedules  = 10
        Files      = 1
        Tasks      = 2
        WebSockets = 2
    }

    # set socket details for pode server
    $ctx.Server.Sockets = @{
        Ssl            = @{
            Protocols = Get-PodeDefaultSslProtocols
        }
        ReceiveTimeout = 100
    }

    $ctx.Server.Signals = @{
        Enabled  = $false
        Listener = $null
    }

    $ctx.Server.Http = @{
        Listener = $null
    }

    $ctx.Server.Sse = @{
        Signed         = $false
        Secret         = $null
        Strict         = $false
        DefaultScope   = 'Global'
        BroadcastLevel = @{}
    }

    $ctx.Server.WebSockets = @{
        Enabled     = ($EnablePool -icontains 'websockets')
        Receiver    = $null
        Connections = @{}
    }

    # set default request config
    $ctx.Server.Request = @{
        Timeout  = 30
        BodySize = 100MB
    }

    # default Folders
    $ctx.Server.DefaultFolders = @{
        'Views'  = 'views'
        'Public' = 'public'
        'Errors' = 'errors'
    }

    # check if there is any global configuration
    $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx

    # over status page exceptions
    if (!(Test-PodeIsEmpty $StatusPageExceptions)) {
        if ($null -eq $ctx.Server.Web) {
            $ctx.Server.Web = @{ ErrorPages = @{} }
        }

        $ctx.Server.Web.ErrorPages.ShowExceptions = ($StatusPageExceptions -eq 'show')
    }

    # configure the server's root path
    $ctx.Server.Root = $ServerRoot
    if (!(Test-PodeIsEmpty $ctx.Server.Configuration.Server.Root)) {
        $ctx.Server.Root = Get-PodeRelativePath -Path $ctx.Server.Configuration.Server.Root -RootPath $ctx.Server.Root -JoinRoot -Resolve -TestPath
    }

    if (Test-PodeIsEmpty $ctx.Server.Root) {
        $ctx.Server.Root = $PWD.Path
    }

    # debugging
    if ($EnableBreakpoints) {
        if ($null -eq $ctx.Server.Debug) {
            $ctx.Server.Debug = @{ Breakpoints = @{} }
        }

        $ctx.Server.Debug.Breakpoints.Enabled = $EnableBreakpoints.IsPresent
    }

    # set the server's listener type
    $ctx.Server.ListenerType = $ListenerType

    # set serverless info
    $ctx.Server.ServerlessType = $ServerlessType
    $ctx.Server.IsServerless = $isServerless
    if ($isServerless) {
        $ctx.Server.DisableTermination = $true
    }

    # set the server types
    $ctx.Server.IsService = ($Interval -gt 0)
    $ctx.Server.Types = @()

    # is the server running under IIS? (also, disable termination)
    $ctx.Server.IsIIS = (!$isServerless -and (!(Test-PodeIsEmpty $env:ASPNETCORE_PORT)) -and (!(Test-PodeIsEmpty $env:ASPNETCORE_TOKEN)))
    if ($ctx.Server.IsIIS) {
        $ctx.Server.DisableTermination = $true

        # if under IIS and Azure Web App, force quiet
        if (!(Test-PodeIsEmpty $env:WEBSITE_IIS_SITE_NAME)) {
            $ctx.Server.Quiet = $true
        }

        # set iis token/settings
        $ctx.Server.IIS = @{
            Token    = $env:ASPNETCORE_TOKEN
            Port     = $env:ASPNETCORE_PORT
            Path     = @{
                Raw       = '/'
                Pattern   = '^/'
                IsNonRoot = $false
            }
            Shutdown = $false
        }

        if (![string]::IsNullOrWhiteSpace($env:ASPNETCORE_APPL_PATH) -and ($env:ASPNETCORE_APPL_PATH -ne '/')) {
            $ctx.Server.IIS.Path.Raw = $env:ASPNETCORE_APPL_PATH
            $ctx.Server.IIS.Path.Pattern = "^$($env:ASPNETCORE_APPL_PATH)"
            $ctx.Server.IIS.Path.IsNonRoot = $true
        }
    }

    # is the server running under Heroku?
    $ctx.Server.IsHeroku = (!$isServerless -and (!(Test-PodeIsEmpty $env:PORT)) -and (!(Test-PodeIsEmpty $env:DYNO)))

    # if we're inside a remote host, stop termination
    if ($Host.Name -ieq 'ServerRemoteHost') {
        $ctx.Server.DisableTermination = $true
    }

    # set the IP address details
    $ctx.Server.Endpoints = @{}
    $ctx.Server.EndpointsMap = @{}

    # general encoding for the server
    $ctx.Server.Encoding = New-Object System.Text.UTF8Encoding

    # setup gui details
    $ctx.Server.Gui = @{}

    # shared temp drives
    $ctx.Server.Drives = @{}
    $ctx.Server.InbuiltDrives = @{}

    # shared state between runspaces
    $ctx.Server.State = @{}

    # setup caching
    $ctx.Server.Cache = @{
        Items          = @{}
        Storage        = @{}
        DefaultStorage = $null
        DefaultTtl     = 3600 # 1hr
    }

    # output details, like variables, to be set once the server stops
    $ctx.Server.Output = @{
        Variables = @{}
    }

    # view engine for rendering pages
    $ctx.Server.ViewEngine = @{
        Type           = 'html'
        Extension      = 'html'
        ScriptBlock    = $null
        UsingVariables = $null
        IsDynamic      = $false
    }

    # pode default preferences
    $ctx.Server.Preferences = @{
        Routes = @{
            IfExists = $null
        }
    }

    # routes for pages and api
    $ctx.Server.Routes = @{
        'connect' = [ordered]@{}
        'delete'  = [ordered]@{}
        'get'     = [ordered]@{}
        'head'    = [ordered]@{}
        'merge'   = [ordered]@{}
        'options' = [ordered]@{}
        'patch'   = [ordered]@{}
        'post'    = [ordered]@{}
        'put'     = [ordered]@{}
        'trace'   = [ordered]@{}
        'static'  = [ordered]@{}
        'signal'  = [ordered]@{}
        '*'       = [ordered]@{}
    }

    # verbs for tcp
    $ctx.Server.Verbs = @{}

    # secrets
    $ctx.Server.Secrets = @{
        Vaults = @{}
        Keys   = @{}
    }

    # custom view paths
    $ctx.Server.Views = @{}

    # handlers for tcp
    $ctx.Server.Handlers = @{
        smtp    = @{}
        service = @{}
    }

    # setup basic access placeholders
    $ctx.Server.Access = @{
        Allow = @{}
        Deny  = @{}
    }

    # setup basic limit rules
    $ctx.Server.Limits = @{
        Rules  = @{}
        Active = @{}
    }

    # cookies and session logic
    $ctx.Server.Cookies = @{
        Csrf    = @{}
        Secrets = @{}
    }

    # sessions
    $ctx.Server.Sessions = @{}

    #OpenApi Definition Tag
    $ctx.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $ctx.Server.Configuration.Web.OpenApi.DefaultDefinitionTag

    # server metrics
    $ctx.Metrics = @{
        Server   = @{
            InitialLoadTime = [datetime]::UtcNow
            StartTime       = [datetime]::UtcNow
            RestartCount    = 0
        }
        Requests = @{
            Total       = 0
            StatusCodes = @{}
        }
        Signals  = @{
            Total = 0
        }
    }

    # authentication and authorisation methods
    $ctx.Server.Authentications = @{
        Methods = @{}
    }

    $ctx.Server.Authorisations = @{
        Methods = @{}
    }

    # create new cancellation tokens
    $ctx.Tokens = @{
        Cancellation = New-Object System.Threading.CancellationTokenSource
        Restart      = New-Object System.Threading.CancellationTokenSource
    }

    # requests that should be logged
    $ctx.LogsToProcess = New-Object System.Collections.ArrayList

    # middleware that needs to run
    $ctx.Server.Middleware = @()
    $ctx.Server.BodyParsers = @{}

    # common support values
    $ctx.Server.Compression = @{
        Encodings = @('gzip', 'deflate', 'x-gzip')
    }

    # endware that needs to run
    $ctx.Server.Endware = @()

    # runspace pools
    $ctx.RunspacePools = @{
        Main      = $null
        Web       = $null
        Smtp      = $null
        Tcp       = $null
        Signals   = $null
        Schedules = $null
        Gui       = $null
        Tasks     = $null
        Files     = $null
    }

    # threading locks, etc.
    $ctx.Threading.Lockables = @{
        Global = [hashtable]::Synchronized(@{})
        Cache  = [hashtable]::Synchronized(@{})
        Custom = @{}
    }

    $ctx.Threading.Mutexes = @{}
    $ctx.Threading.Semaphores = @{}

    # setup runspaces
    $ctx.Runspaces = @()

    # setup events
    $ctx.Server.Events = @{
        Start     = [ordered]@{}
        Terminate = [ordered]@{}
        Restart   = [ordered]@{}
        Browser   = [ordered]@{}
        Crash     = [ordered]@{}
        Stop      = [ordered]@{}
        Running   = [ordered]@{}
    }

    # modules
    $ctx.Server.Modules = [ordered]@{}

    # setup security
    $ctx.Server.Security = @{
        ServerDetails = $true
        Headers       = @{}
        Cache         = @{
            ContentSecurity   = @{}
            PermissionsPolicy = @{}
        }
    }

    # scoped variables
    $ctx.Server.ScopedVariables = [ordered]@{}

    # an internal cache for adhoc values, such as module importing checks
    $ctx.Server.InternalCache = @{
        YamlModuleImported = $null
    }

    # return the new context
    return $ctx
}

function New-PodeRunspaceState {
    # create the state, and add the pode modules
    $state = [initialsessionstate]::CreateDefault()
    $state.ImportPSModule($PodeContext.Server.PodeModule.DataPath)
    $state.ImportPSModule($PodeContext.Server.PodeModule.InternalPath)

    # load the vars into the share state
    $session = New-PodeStateContext -Context $PodeContext

    $variables = @(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeContext', $session, $null),
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null),
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PODE_SCOPE_RUNSPACE', $true, $null)
    )

    foreach ($var in $variables) {
        $state.Variables.Add($var)
    }

    $PodeContext.RunspaceState = $state
}

function New-PodeRunspacePools {
    if ($PodeContext.Server.IsServerless) {
        return
    }

    # setup main runspace pool
    $threadsCounts = @{
        Default  = 3
        Timer    = 1
        Log      = 1
        Schedule = 1
        Misc     = 1
    }

    if (!(Test-PodeTimersExist)) {
        $threadsCounts.Timer = 0
    }

    if (!(Test-PodeSchedulesExist)) {
        $threadsCounts.Schedule = 0
    }

    if (!(Test-PodeLoggersExist)) {
        $threadsCounts.Log = 0
    }

    # main runspace - for timers, schedules, etc
    $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum
    $PodeContext.RunspacePools.Main = @{
        Pool  = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host)
        State = 'Waiting'
    }

    # web runspace - if we have any http/s endpoints
    if (Test-PodeEndpoints -Type Http) {
        $PodeContext.RunspacePools.Web = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # smtp runspace - if we have any smtp endpoints
    if (Test-PodeEndpoints -Type Smtp) {
        $PodeContext.RunspacePools.Smtp = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # tcp runspace - if we have any tcp endpoints
    if (Test-PodeEndpoints -Type Tcp) {
        $PodeContext.RunspacePools.Tcp = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # signals runspace - if we have any ws/s endpoints
    if (Test-PodeEndpoints -Type Ws) {
        $PodeContext.RunspacePools.Signals = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # web socket connections runspace - for receiving data for external sockets
    if (Test-PodeWebSocketsExist) {
        $PodeContext.RunspacePools.WebSockets = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }

        New-PodeWebSocketReceiver
    }

    # setup schedule runspace pool -if we have any schedules
    if (Test-PodeSchedulesExist) {
        $PodeContext.RunspacePools.Schedules = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # setup tasks runspace pool -if we have any tasks
    if (Test-PodeTasksExist) {
        $PodeContext.RunspacePools.Tasks = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # setup files runspace pool -if we have any file watchers
    if (Test-PodeFileWatchersExist) {
        $PodeContext.RunspacePools.Files = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }
    }

    # setup gui runspace pool (only for non-ps-core) - if gui enabled
    if (Test-PodeGuiEnabled) {
        $PodeContext.RunspacePools.Gui = @{
            Pool  = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host)
            State = 'Waiting'
        }

        $PodeContext.RunspacePools.Gui.Pool.ApartmentState = 'STA'
    }
}

function Open-PodeRunspacePools {
    if ($PodeContext.Server.IsServerless) {
        return
    }

    $start = [datetime]::Now
    Write-Verbose 'Opening RunspacePools'

    # open pools async
    foreach ($key in $PodeContext.RunspacePools.Keys) {
        $item = $PodeContext.RunspacePools[$key]
        if ($null -eq $item) {
            continue
        }

        $item.Pool.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread
        $item.Pool.CleanupInterval = [timespan]::FromMinutes(5)
        $item.Result = $item.Pool.BeginOpen($null, $null)
    }

    # wait for them all to open
    $queue = @($PodeContext.RunspacePools.Keys)

    while ($queue.Length -gt 0) {
        foreach ($key in $queue) {
            $item = $PodeContext.RunspacePools[$key]
            if ($null -eq $item) {
                $queue = ($queue | Where-Object { $_ -ine $key })
                continue
            }

            if ($item.Pool.RunspacePoolStateInfo.State -iin @('Opened', 'Broken')) {
                $queue = ($queue | Where-Object { $_ -ine $key })
                Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
            }
        }

        if ($queue.Length -gt 0) {
            Start-Sleep -Milliseconds 100
        }
    }

    # report errors for failed pools
    foreach ($key in $PodeContext.RunspacePools.Keys) {
        $item = $PodeContext.RunspacePools[$key]
        if ($null -eq $item) {
            continue
        }

        if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') {
            $item.Pool.EndOpen($item.Result) | Out-Default
            throw "Failed to open RunspacePool: $($key)"
        }
    }

    Write-Verbose "RunspacePools opened [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}

function Close-PodeRunspacePools {
    if ($PodeContext.Server.IsServerless -or ($null -eq $PodeContext.RunspacePools)) {
        return
    }

    $start = [datetime]::Now
    Write-Verbose 'Closing RunspacePools'

    # close pools async
    foreach ($key in $PodeContext.RunspacePools.Keys) {
        $item = $PodeContext.RunspacePools[$key]
        if (($null -eq $item) -or ($item.Pool.IsDisposed)) {
            continue
        }

        $item.Result = $item.Pool.BeginClose($null, $null)
    }

    # wait for them all to close
    $queue = @($PodeContext.RunspacePools.Keys)

    while ($queue.Length -gt 0) {
        foreach ($key in $queue) {
            $item = $PodeContext.RunspacePools[$key]
            if ($null -eq $item) {
                $queue = ($queue | Where-Object { $_ -ine $key })
                continue
            }

            if ($item.Pool.RunspacePoolStateInfo.State -iin @('Closed', 'Broken')) {
                $queue = ($queue | Where-Object { $_ -ine $key })
                Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
            }
        }

        if ($queue.Length -gt 0) {
            Start-Sleep -Milliseconds 100
        }
    }

    # report errors for failed pools
    foreach ($key in $PodeContext.RunspacePools.Keys) {
        $item = $PodeContext.RunspacePools[$key]
        if ($null -eq $item) {
            continue
        }

        if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') {
            $item.Pool.EndClose($item.Result) | Out-Default
            throw "Failed to close RunspacePool: $($key)"
        }
    }

    # dispose pools
    foreach ($key in $PodeContext.RunspacePools.Keys) {
        $item = $PodeContext.RunspacePools[$key]
        if (($null -eq $item) -or ($item.Pool.IsDisposed)) {
            continue
        }

        Close-PodeDisposable -Disposable $item.Pool
    }

    Write-Verbose "RunspacePools closed [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}

function New-PodeStateContext {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Context
    )

    return (New-Object -TypeName psobject |
            Add-Member -MemberType NoteProperty -Name Threads -Value $Context.Threads -PassThru |
            Add-Member -MemberType NoteProperty -Name Timers -Value $Context.Timers -PassThru |
            Add-Member -MemberType NoteProperty -Name Schedules -Value $Context.Schedules -PassThru |
            Add-Member -MemberType NoteProperty -Name Tasks -Value $Context.Tasks -PassThru |
            Add-Member -MemberType NoteProperty -Name Fim -Value $Context.Fim -PassThru |
            Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Context.RunspacePools -PassThru |
            Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru |
            Add-Member -MemberType NoteProperty -Name Metrics -Value $Context.Metrics -PassThru |
            Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $Context.LogsToProcess -PassThru |
            Add-Member -MemberType NoteProperty -Name Threading -Value $Context.Threading -PassThru |
            Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru)
}

function Open-PodeConfiguration {
    param(
        [Parameter()]
        [string]
        $ServerRoot = $null,

        [Parameter()]
        $Context
    )

    $config = @{}

    # set the path to the root config file
    $configPath = (Join-PodeServerRoot -Folder '.' -FilePath 'server.psd1' -Root $ServerRoot)

    # check to see if an environmental config exists (if the env var is set)
    if (!(Test-PodeIsEmpty $env:PODE_ENVIRONMENT)) {
        $_path = (Join-PodeServerRoot -Folder '.' -FilePath "server.$($env:PODE_ENVIRONMENT).psd1" -Root $ServerRoot)
        if (Test-PodePath -Path $_path -NoStatus) {
            $configPath = $_path
        }
    }

    # check the path exists, and load the config
    if (Test-PodePath -Path $configPath -NoStatus) {
        $config = Import-PowerShellDataFile -Path $configPath -ErrorAction Stop
        Set-PodeServerConfiguration -Configuration $config.Server -Context $Context
        Set-PodeWebConfiguration -Configuration $config.Web -Context $Context
    }

    return $config
}

function Set-PodeServerConfiguration {
    param(
        [Parameter()]
        [hashtable]
        $Configuration,

        [Parameter()]
        $Context
    )

    # file monitoring
    $Context.Server.FileMonitor = @{
        Enabled   = [bool]$Configuration.FileMonitor.Enable
        Exclude   = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Exclude))
        Include   = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Include))
        ShowFiles = [bool]$Configuration.FileMonitor.ShowFiles
        Files     = @()
    }

    # logging
    $Context.Server.Logging = @{
        Enabled = (($null -eq $Configuration.Logging.Enable) -or [bool]$Configuration.Logging.Enable)
        Masking = @{
            Patterns = (Remove-PodeEmptyItemsFromArray -Array @($Configuration.Logging.Masking.Patterns))
            Mask     = (Protect-PodeValue -Value $Configuration.Logging.Masking.Mask -Default '********')
        }
        Types   = @{}
    }

    # sockets
    if (!(Test-PodeIsEmpty $Configuration.Ssl.Protocols)) {
        $Context.Server.Sockets.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $Configuration.Ssl.Protocols)
    }

    if ([int]$Configuration.ReceiveTimeout -gt 0) {
        $Context.Server.Sockets.ReceiveTimeout = (Protect-PodeValue -Value $Configuration.ReceiveTimeout $Context.Server.Sockets.ReceiveTimeout)
    }

    # auto-import
    $Context.Server.AutoImport = Read-PodeAutoImportConfiguration -Configuration $Configuration

    # request
    if ([int]$Configuration.Request.Timeout -gt 0) {
        $Context.Server.Request.Timeout = [int]$Configuration.Request.Timeout
    }

    if ([long]$Configuration.Request.BodySize -gt 0) {
        $Context.Server.Request.BodySize = [long]$Configuration.Request.BodySize
    }

    # default folders
    if ($Configuration.DefaultFolders) {
        if ($Configuration.DefaultFolders.Public) {
            $Context.Server.DefaultFolders.Public = $Configuration.DefaultFolders.Public
        }
        if ($Configuration.DefaultFolders.Views) {
            $Context.Server.DefaultFolders.Views = $Configuration.DefaultFolders.Views
        }
        if ($Configuration.DefaultFolders.Errors) {
            $Context.Server.DefaultFolders.Errors = $Configuration.DefaultFolders.Errors
        }
    }

    # debug
    $Context.Server.Debug = @{
        Breakpoints = @{
            Enabled = [bool]$Configuration.Debug.Breakpoints.Enable
        }
    }
}

function Set-PodeWebConfiguration {
    param(
        [Parameter()]
        [hashtable]
        $Configuration,

        [Parameter()]
        $Context
    )

    # setup the main web config
    $Context.Server.Web = @{
        Static           = @{
            Defaults          = $Configuration.Static.Defaults
            RedirectToDefault = [bool]$Configuration.Static.RedirectToDefault
            Cache             = @{
                Enabled = [bool]$Configuration.Static.Cache.Enable
                MaxAge  = [int](Protect-PodeValue -Value $Configuration.Static.Cache.MaxAge -Default 3600)
                Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Include) -NotSlashes)
                Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Exclude) -NotSlashes)
            }
            ValidateLast      = [bool]$Configuration.Static.ValidateLast
        }
        ErrorPages       = @{
            ShowExceptions      = [bool]$Configuration.ErrorPages.ShowExceptions
            StrictContentTyping = [bool]$Configuration.ErrorPages.StrictContentTyping
            Default             = $Configuration.ErrorPages.Default
            Routes              = @{}
        }
        ContentType      = @{
            Default = $Configuration.ContentType.Default
            Routes  = @{}
        }
        TransferEncoding = @{
            Default = $Configuration.TransferEncoding.Default
            Routes  = @{}
        }
        Compression      = @{
            Enabled = [bool]$Configuration.Compression.Enable
        }
    }

    # setup content type route patterns for forced content types
    $Configuration.ContentType.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
        $_type = $Configuration.ContentType.Routes[$_]
        $_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
        $Context.Server.Web.ContentType.Routes[$_pattern] = $_type
    }

    # setup transfer encoding route patterns for forced transfer encodings
    $Configuration.TransferEncoding.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
        $_type = $Configuration.TransferEncoding.Routes[$_]
        $_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
        $Context.Server.Web.TransferEncoding.Routes[$_pattern] = $_type
    }

    # setup content type route patterns for error pages
    $Configuration.ErrorPages.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
        $_type = $Configuration.ErrorPages.Routes[$_]
        $_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
        $Context.Server.Web.ErrorPages.Routes[$_pattern] = $_type
    }
}

function New-PodeAutoRestartServer {
    # don't configure if not supplied, or running as serverless
    $config = (Get-PodeConfig)
    if (($null -eq $config) -or ($null -eq $config.Server.Restart) -or $PodeContext.Server.IsServerless) {
        return
    }

    $restart = $config.Server.Restart

    # period - setup a timer
    $period = [int]$restart.period
    if ($period -gt 0) {
        Add-PodeTimer -Name '__pode_restart_period__' -Interval ($period * 60) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }

    # times - convert into cron expressions
    $times = @(@($restart.times) -ne $null)
    if (($times | Measure-Object).Count -gt 0) {
        $crons = @()

        @($times) | ForEach-Object {
            $atoms = $_ -split '\:'
            $crons += "$([int]$atoms[1]) $([int]$atoms[0]) * * *"
        }

        Add-PodeSchedule -Name '__pode_restart_times__' -Cron @($crons) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }

    # crons - setup schedules
    $crons = @(@($restart.crons) -ne $null)
    if (($crons | Measure-Object).Count -gt 0) {
        Add-PodeSchedule -Name '__pode_restart_crons__' -Cron @($crons) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }
}

function Set-PodeOutputVariables {
    if (Test-PodeIsEmpty $PodeContext.Server.Output.Variables) {
        return
    }

    foreach ($key in $PodeContext.Server.Output.Variables.Keys) {
        try {
            Set-Variable -Name $key -Value $PodeContext.Server.Output.Variables[$key] -Force -Scope Global
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}
src\Private\Cookies.ps1
function ConvertTo-PodeCookie {
    param(
        [Parameter()]
        [System.Net.Cookie]
        $Cookie
    )

    if ($null -eq $Cookie) {
        return @{}
    }

    return @{
        Name      = $Cookie.Name
        Value     = $Cookie.Value
        Expires   = $Cookie.Expires
        Expired   = $Cookie.Expired
        Discard   = $Cookie.Discard
        HttpOnly  = $Cookie.HttpOnly
        Secure    = $Cookie.Secure
        Path      = $Cookie.Path
        TimeStamp = $Cookie.TimeStamp
        Signed    = $Cookie.Value.StartsWith('s:')
    }
}

function ConvertTo-PodeCookieString {
    param(
        [Parameter(Mandatory = $true)]
        $Cookie
    )

    try {
        $builder = [System.Text.StringBuilder]::new()
        $null = $builder.Append($Cookie.Name)
        $null = $builder.Append('=')
        $null = $builder.Append($Cookie.Value)

        if ($Cookie.Discard) {
            $null = $builder.Append('; Discard')
        }

        if ($Cookie.HttpOnly) {
            $null = $builder.Append('; HttpOnly')
        }

        if ($Cookie.Secure) {
            $null = $builder.Append('; Secure')
        }

        if (![string]::IsNullOrEmpty($Cookie.Domain)) {
            $null = $builder.Append('; Domain=')
            $null = $builder.Append($Cookie.Domain)
        }

        if (![string]::IsNullOrEmpty($Cookie.Path)) {
            $null = $builder.Append('; Path=')
            $null = $builder.Append($Cookie.Path)
        }

        if (($null -ne $Cookie.Expires) -and ($Cookie.Expires.Ticks -ne 0)) {
            $secs = ($Cookie.Expires.Subtract([datetime]::UtcNow)).TotalSeconds
            if ($secs -lt 0) {
                $secs = 0
            }

            $null = $builder.Append('; Max-Age=')
            $null = $builder.Append($secs)
        }

        if ($builder.Length -le 1) {
            return $null
        }

        return $builder.ToString()
    }
    finally {
        $builder = $null
    }
}
src\Private\CronParser.ps1
function Get-PodeCronFields {
    return @(
        'Minute',
        'Hour',
        'DayOfMonth',
        'Month',
        'DayOfWeek'
    )
}

function Get-PodeCronFieldConstraints {
    return @{
        MinMax       = @(
            @(0, 59),
            @(0, 23),
            @(1, 31),
            @(1, 12),
            @(0, 6)
        )
        DaysInMonths = @(
            31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        )
        Months       = @(
            'January', 'February', 'March', 'April', 'May', 'June', 'July',
            'August', 'September', 'October', 'November', 'December'
        )
    }
}

function Get-PodeCronPredefined {
    return @{
        # normal
        '@minutely'       = '* * * * *'
        '@hourly'         = '0 * * * *'
        '@daily'          = '0 0 * * *'
        '@weekly'         = '0 0 * * 0'
        '@monthly'        = '0 0 1 * *'
        '@quarterly'      = '0 0 1 1,4,7,10 *'
        '@yearly'         = '0 0 1 1 *'
        '@annually'       = '0 0 1 1 *'

        # twice
        '@twice-hourly'   = '0,30 * * * *'
        '@twice-daily'    = '0 0,12 * * *'
        '@twice-weekly'   = '0 0 * * 0,4'
        '@twice-monthly'  = '0 0 1,15 * *'
        '@twice-yearly'   = '0 0 1 1,6 *'
        '@twice-annually' = '0 0 1 1,6 *'
    }
}

function Get-PodeCronFieldAliases {
    return @{
        Month     = @{
            Jan = 1
            Feb = 2
            Mar = 3
            Apr = 4
            May = 5
            Jun = 6
            Jul = 7
            Aug = 8
            Sep = 9
            Oct = 10
            Nov = 11
            Dec = 12
        }
        DayOfWeek = @{
            Sun = 0
            Mon = 1
            Tue = 2
            Wed = 3
            Thu = 4
            Fri = 5
            Sat = 6
        }
    }
}

function ConvertFrom-PodeCronExpressions {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Expressions
    )

    return @(@($Expressions) | ForEach-Object {
            ConvertFrom-PodeCronExpression -Expression $_
        })
}

function ConvertFrom-PodeCronExpression {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Expression
    )

    $Expression = $Expression.Trim()

    # check predefineds
    $predef = Get-PodeCronPredefined
    if (!(Test-PodeIsEmpty $predef[$Expression])) {
        $Expression = $predef[$Expression]
    }

    # split and check atoms length
    $atoms = @($Expression -isplit '\s+')
    if ($atoms.Length -ne 5) {
        throw "Cron expression should only consist of 5 parts: $($Expression)"
    }

    # basic variables
    $aliasRgx = '(?<tag>[a-z]{3})'

    # get cron obj and validate atoms
    $fields = Get-PodeCronFields
    $constraints = Get-PodeCronFieldConstraints
    $aliases = Get-PodeCronFieldAliases
    $cron = @{}

    for ($i = 0; $i -lt $atoms.Length; $i++) {
        $_cronExp = @{
            Range       = $null
            Values      = $null
            Constraints = $null
            Random      = $false
            WildCard    = $false
        }

        $_atom = $atoms[$i]
        $_field = $fields[$i]
        $_constraint = $constraints.MinMax[$i]
        $_aliases = $aliases[$_field]

        # replace day of week and months with numbers
        if (@('month', 'dayofweek') -icontains $_field) {
            while ($_atom -imatch $aliasRgx) {
                $_alias = $_aliases[$Matches['tag']]
                if ($null -eq $_alias) {
                    throw "Invalid $($_field) alias found: $($Matches['tag'])"
                }

                $_atom = $_atom -ireplace $Matches['tag'], $_alias
                $null = $_atom -imatch $aliasRgx
            }
        }

        # ensure atom is a valid value
        if (!($_atom -imatch '^[\d|/|*|\-|,r]+$')) {
            throw "Invalid atom character: $($_atom)"
        }

        # replace * with min/max constraint
        if ($_atom -ieq '*') {
            $_cronExp.WildCard = $true
            $_atom = ($_constraint -join '-')
        }

        # parse the atom for either a literal, range, array, or interval
        # literal
        if ($_atom -imatch '^(\d+|r)$') {
            # check if it's random
            if ($_atom -ieq 'r') {
                $_cronExp.Values = @(Get-Random -Minimum $_constraint[0] -Maximum ($_constraint[1] + 1))
                $_cronExp.Random = $true
            }
            else {
                $_cronExp.Values = @([int]$_atom)
            }
        }

        # range
        elseif ($_atom -imatch '^(?<min>\d+)\-(?<max>\d+)$') {
            $_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); }
        }

        # array
        elseif ($_atom -imatch '^[\d,]+$') {
            $_cronExp.Values = [int[]](@($_atom -split ',').Trim())
        }

        # interval
        elseif ($_atom -imatch '(?<start>(\d+|\*))\/(?<interval>(\d+|r))$') {
            $start = $Matches['start']
            $interval = $Matches['interval']

            if ($interval -ieq '0') {
                $interval = '1'
            }

            if ([string]::IsNullOrWhiteSpace($start) -or ($start -ieq '*')) {
                $start = '0'
            }

            # set the initial trigger value
            $_cronExp.Values = @([int]$start)

            # check if it's random
            if ($interval -ieq 'r') {
                $_cronExp.Random = $true
            }
            else {
                # loop to get all next values
                $next = [int]$start + [int]$interval
                while ($next -le $_constraint[1]) {
                    $_cronExp.Values += $next
                    $next += [int]$interval
                }
            }
        }

        # error
        else {
            throw "Invalid cron atom format found: $($_atom)"
        }

        # ensure cron expression values are valid
        if ($null -ne $_cronExp.Range) {
            if ($_cronExp.Range.Min -gt $_cronExp.Range.Max) {
                throw "Min value for $($_field) should not be greater than the max value"
            }

            if ($_cronExp.Range.Min -lt $_constraint[0]) {
                throw "Min value '$($_cronExp.Range.Min)' for $($_field) is invalid, should be greater than/equal to $($_constraint[0])"
            }

            if ($_cronExp.Range.Max -gt $_constraint[1]) {
                throw "Max value '$($_cronExp.Range.Max)' for $($_field) is invalid, should be less than/equal to $($_constraint[1])"
            }
        }

        if ($null -ne $_cronExp.Values) {
            $_cronExp.Values | ForEach-Object {
                if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) {
                    throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])"
                }
            }
        }

        # assign value
        $_cronExp.Constraints = $_constraint
        $cron[$_field] = $_cronExp
    }

    # post validation for month/days in month
    if (($null -ne $cron['Month'].Values) -and ($null -ne $cron['DayOfMonth'].Values)) {
        foreach ($mon in $cron['Month'].Values) {
            foreach ($day in $cron['DayOfMonth'].Values) {
                if ($day -gt $constraints.DaysInMonths[$mon - 1]) {
                    throw "$($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied"
                }
            }
        }
    }

    # flag if this cron contains a random atom
    $cron['Random'] = (($cron.Values | Where-Object { $_.Random } | Measure-Object).Count -gt 0)

    # return the parsed cron expression
    return $cron
}

function Reset-PodeRandomCronExpressions {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expressions
    )

    return @(@($Expressions) | ForEach-Object {
            Reset-PodeRandomCronExpression -Expression $_
        })
}

function Reset-PodeRandomCronExpression {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expression
    )

    function Reset-Atom($Atom) {
        if (!$Atom.Random) {
            return $Atom
        }

        if ($Atom.Random) {
            $Atom.Values = @(Get-Random -Minimum $Atom.Constraints[0] -Maximum ($Atom.Constraints[1] + 1))
        }

        return $Atom
    }

    if (!$Expression.Random) {
        return $Expression
    }

    $Expression.Minute = (Reset-Atom -Atom $Expression.Minute)
    $Expression.Hour = (Reset-Atom -Atom $Expression.Hour)
    $Expression.DayOfMonth = (Reset-Atom -Atom $Expression.DayOfMonth)
    $Expression.Month = (Reset-Atom -Atom $Expression.Month)
    $Expression.DayOfWeek = (Reset-Atom -Atom $Expression.DayOfWeek)

    return $Expression
}

function Test-PodeCronExpressions {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expressions,

        [Parameter()]
        $DateTime = $null
    )

    return ((@($Expressions) | Where-Object {
                Test-PodeCronExpression -Expression $_ -DateTime $DateTime
            } | Measure-Object).Count -gt 0)
}

function Test-PodeCronExpression {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expression,

        [Parameter()]
        $DateTime = $null
    )

    function Test-RangeAndValue($AtomContraint, $NowValue) {
        if ($null -ne $AtomContraint.Range) {
            return (!(($NowValue -lt $AtomContraint.Range.Min) -or ($NowValue -gt $AtomContraint.Range.Max)))
        }

        return ($AtomContraint.Values -icontains $NowValue)
    }

    # current time
    if ($null -eq $DateTime) {
        $DateTime = [datetime]::Now
    }

    # check day of month
    if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfMonth -NowValue $DateTime.Day)) {
        return $false
    }

    # check day of week
    if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek))) {
        return $false
    }

    # check month
    if (!(Test-RangeAndValue -AtomContraint $Expression.Month -NowValue $DateTime.Month)) {
        return $false
    }

    # check hour
    if (!(Test-RangeAndValue -AtomContraint $Expression.Hour -NowValue $DateTime.Hour)) {
        return $false
    }

    # check minute
    if (!(Test-RangeAndValue -AtomContraint $Expression.Minute -NowValue $DateTime.Minute)) {
        return $false
    }

    # date is valid
    return $true
}

function Get-PodeCronNextEarliestTrigger {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expressions,

        [Parameter()]
        $StartTime = $null,

        [Parameter()]
        $EndTime = $null
    )

    return (@($Expressions) | Foreach-Object {
            Get-PodeCronNextTrigger -Expression $_ -StartTime $StartTime -EndTime $EndTime
        } | Where-Object { $null -ne $_ } | Sort-Object | Select-Object -First 1)
}

function Get-PodeCronNextTrigger {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Expression,

        [Parameter()]
        $StartTime = $null,

        [Parameter()]
        $EndTime = $null
    )

    # start from the current time, if a start time not defined
    if ($null -eq $StartTime) {
        $StartTime = [datetime]::Now
    }
    $StartTime = $StartTime.AddMinutes(1)

    # the next time to trigger
    $NextTime = [datetime]::new($StartTime.Year, $StartTime.Month, $StartTime.Day, $StartTime.Hour, $StartTime.Minute, 0)

    # first, is the current time valid?
    if (Test-PodeCronExpression -Expression $Expression -DateTime $NextTime) {
        return $NextTime
    }

    # functions for getting the closest value
    function Get-ClosestValue($AtomContraint, $NowValue) {
        $_values = $AtomContraint.Values
        if ($null -eq $_values) {
            $_values = ($AtomContraint.Range.Min..$AtomContraint.Range.Max)
        }

        if (($_values.Length -eq 1) -or ($_values[-1] -lt $NowValue) -or ($_values[0] -gt $NowValue)) {
            return $_values[0]
        }

        return ($_values -ge $NowValue)[0]
    }

    # loop until we get a date
    while ($true) {
        # check the minute
        if (!$Expression.Minute.WildCard) {
            $minute = Get-ClosestValue -AtomContraint $Expression.Minute -NowValue $NextTime.Minute
            if ($minute -lt $NextTime.Minute) {
                $NextTime = $NextTime.AddHours(1)
            }

            $NextTime = $NextTime.AddMinutes($minute - $NextTime.Minute)
        }

        # check hour
        if (!$Expression.Hour.WildCard) {
            $hour = Get-ClosestValue -AtomContraint $Expression.Hour -NowValue $NextTime.Hour
            if ($hour -lt $NextTime.Hour) {
                $NextTime = $NextTime.AddDays(1)
            }

            $_hour = $NextTime.Hour
            $NextTime = $NextTime.AddHours($hour - $NextTime.Hour)
            if ($_hour -ne $hour) {
                $NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, $NextTime.Hour, 0, 0)
                continue
            }
        }

        # check day
        if (!$Expression.DayOfMonth.WildCard) {
            $day = Get-ClosestValue -AtomContraint $Expression.DayOfMonth -NowValue $NextTime.Day
            if (($day -lt $NextTime.Day) -or ($day -gt [datetime]::DaysInMonth($NextTime.Year, $NextTime.Month))) {
                $NextTime = $NextTime.AddMonths(1)
            }

            if ($day -gt [datetime]::DaysInMonth($NextTime.Year, $NextTime.Month)) {
                $NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, 1, 0, 0, 0)
                continue
            }

            $_day = $NextTime.Day
            $NextTime = $NextTime.AddDays($day - $NextTime.Day)
            if ($_day -ne $day) {
                $NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, 0, 0, 0)
                continue
            }
        }

        # check month
        if (!$Expression.Month.WildCard) {
            $month = Get-ClosestValue -AtomContraint $Expression.Month -NowValue $NextTime.Month
            if ($month -lt $NextTime.Month) {
                $NextTime = $NextTime.AddYears(1)
            }

            $_month = $NextTime.Month
            $NextTime = $NextTime.AddMonths($month - $NextTime.Month)
            if ($_month -ne $month) {
                $NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, 1, 0, 0, 0)
                continue
            }
        }

        # check day of week
        if (!$Expression.DayOfWeek.WildCard) {
            $doweek = Get-ClosestValue -AtomContraint $Expression.DayOfWeek -NowValue $NextTime.DayOfWeek

            $_doweek = $NextTime.DayOfWeek
            if ($doweek -lt $NextTime.DayOfWeek) {
                $NextTime = $NextTime.AddDays(7 - ($NextTime.DayOfWeek - $doweek))
            }
            elseif ($doweek -gt $NextTime.DayOfWeek) {
                $NextTime = $NextTime.AddDays($doweek - $NextTime.DayOfWeek)
            }

            if ($_doweek -ne $doweek) {
                $NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, 0, 0, 0)
                continue
            }
        }

        break
    }

    # before we return, make sure the time is valid
    if (!(Test-PodeCronExpression -Expression $Expression -DateTime $NextTime)) {
        throw "Looks like something went wrong trying to calculate the next trigger datetime: $($NextTime)"
    }

    # if before the start or after end then return null
    if (($NextTime -lt $StartTime) -or (($null -ne $EndTime) -and ($NextTime -gt $EndTime))) {
        return $null
    }

    return $NextTime
}
src\Private\Cryptography.ps1
function Invoke-PodeHMACSHA256Hash {
    [CmdletBinding(DefaultParameterSetName = 'String')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'String')]
        [string]
        $Secret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
        [byte[]]
        $SecretBytes
    )

    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
    }

    if ($SecretBytes.Length -eq 0) {
        throw 'No secret supplied for HMAC256 hash'
    }

    $crypto = [System.Security.Cryptography.HMACSHA256]::new($SecretBytes)
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeHMACSHA384Hash {
    [CmdletBinding(DefaultParameterSetName = 'String')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'String')]
        [string]
        $Secret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
        [byte[]]
        $SecretBytes
    )

    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
    }

    if ($SecretBytes.Length -eq 0) {
        throw 'No secret supplied for HMAC384 hash'
    }

    $crypto = [System.Security.Cryptography.HMACSHA384]::new($SecretBytes)
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeHMACSHA512Hash {
    [CmdletBinding(DefaultParameterSetName = 'String')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'String')]
        [string]
        $Secret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
        [byte[]]
        $SecretBytes
    )

    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
    }

    if ($SecretBytes.Length -eq 0) {
        throw 'No secret supplied for HMAC512 hash'
    }

    $crypto = [System.Security.Cryptography.HMACSHA512]::new($SecretBytes)
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeSHA256Hash {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value
    )

    $crypto = [System.Security.Cryptography.SHA256]::Create()
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeSHA1Hash {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value
    )

    $crypto = [System.Security.Cryptography.SHA1]::Create()
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function ConvertTo-PodeBase64Auth {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Username,

        [Parameter(Mandatory = $true)]
        [string]
        $Password
    )

    return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($Username):$($Password)"))
}

function Invoke-PodeMD5Hash {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value
    )

    $crypto = [System.Security.Cryptography.MD5]::Create()
    return [System.BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Value))).Replace('-', '').ToLowerInvariant()
}

function Get-PodeRandomBytes {
    param(
        [Parameter()]
        [int]
        $Length = 16
    )

    return (Use-PodeStream -Stream ([System.Security.Cryptography.RandomNumberGenerator]::Create()) {
            param($p)
            $bytes = [byte[]]::new($Length)
            $p.GetBytes($bytes)
            return $bytes
        })
}

function New-PodeSalt {
    param(
        [Parameter()]
        [int]
        $Length = 8
    )

    $bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
    return [System.Convert]::ToBase64String($bytes)
}

function New-PodeGuid {
    param(
        [Parameter()]
        [int]
        $Length = 16,

        [switch]
        $Secure,

        [switch]
        $NoDashes
    )

    # generate a cryptographically secure guid
    if ($Secure) {
        $bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
        $guid = ([guid]::new($bytes)).ToString()
    }

    # return a normal guid
    else {
        $guid = ([guid]::NewGuid()).ToString()
    }

    if ($NoDashes) {
        $guid = ($guid -ireplace '-', '')
    }

    return $guid
}

function Invoke-PodeValueSign {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    if ($Strict) {
        $Secret = ConvertTo-PodeStrictSecret -Secret $Secret
    }

    return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)"
}

function Invoke-PodeValueUnsign {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    # the signed value must start with "s:"
    if (!$Value.StartsWith('s:')) {
        return $null
    }

    # the signed value must contain a dot - splitting value and signature
    $Value = $Value.Substring(2)
    $periodIndex = $Value.LastIndexOf('.')
    if ($periodIndex -eq -1) {
        return $null
    }

    if ($Strict) {
        $Secret = ConvertTo-PodeStrictSecret -Secret $Secret
    }

    # get the raw value and signature
    $raw = $Value.Substring(0, $periodIndex)
    $sig = $Value.Substring($periodIndex + 1)

    if ((Invoke-PodeHMACSHA256Hash -Value $raw -Secret $Secret) -ne $sig) {
        return $null
    }

    return $raw
}

function Test-PodeValueSigned {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    if ([string]::IsNullOrEmpty($Value)) {
        return $false
    }

    $result = Invoke-PodeValueUnsign -Value $Value -Secret $Secret -Strict:$Strict
    return ![string]::IsNullOrEmpty($result)
}

function ConvertTo-PodeStrictSecret {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Secret
    )

    return "$($Secret);$($WebEvent.Request.UserAgent);$($WebEvent.Request.RemoteEndPoint.Address.IPAddressToString)"
}

function New-PodeJwtSignature {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Algorithm,

        [Parameter(Mandatory = $true)]
        [string]
        $Token,

        [Parameter()]
        [byte[]]
        $SecretBytes
    )

    if (($Algorithm -ine 'none') -and (($null -eq $SecretBytes) -or ($SecretBytes.Length -eq 0))) {
        throw 'No Secret supplied for JWT signature'
    }

    if (($Algorithm -ieq 'none') -and (($null -ne $secretBytes) -and ($SecretBytes.Length -gt 0))) {
        throw 'Expected no secret to be supplied for no signature'
    }

    $sig = $null

    switch ($Algorithm.ToUpperInvariant()) {
        'HS256' {
            $sig = Invoke-PodeHMACSHA256Hash -Value $Token -SecretBytes $SecretBytes
            $sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
        }

        'HS384' {
            $sig = Invoke-PodeHMACSHA384Hash -Value $Token -SecretBytes $SecretBytes
            $sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
        }

        'HS512' {
            $sig = Invoke-PodeHMACSHA512Hash -Value $Token -SecretBytes $SecretBytes
            $sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
        }

        'NONE' {
            $sig = [string]::Empty
        }

        default {
            throw "The JWT algorithm is not currently supported: $($Algorithm)"
        }
    }

    return $sig
}

function ConvertTo-PodeBase64UrlValue {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [switch]
        $NoConvert
    )

    if (!$NoConvert) {
        $Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value))
    }

    $Value = ($Value -ireplace '\+', '-')
    $Value = ($Value -ireplace '/', '_')
    $Value = ($Value -ireplace '=', '')

    return $Value
}

function ConvertFrom-PodeJwtBase64Value {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Value
    )

    # map chars
    $Value = ($Value -ireplace '-', '+')
    $Value = ($Value -ireplace '_', '/')

    # add padding
    switch ($Value.Length % 4) {
        1 {
            $Value = $Value.Substring(0, $Value.Length - 1)
        }

        2 {
            $Value += '=='
        }

        3 {
            $Value += '='
        }
    }

    # convert base64 to string
    try {
        $Value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Value))
    }
    catch {
        throw 'Invalid Base64 encoded value found in JWT'
    }

    # return json
    try {
        return ($Value | ConvertFrom-Json)
    }
    catch {
        throw 'Invalid JSON value found in JWT'
    }
}
src\Private\Endpoints.ps1
function Find-PodeEndpoints {
    param(
        [Parameter()]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Address,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    $endpoints = @()

    # just use a single endpoint/protocol
    if ([string]::IsNullOrWhiteSpace($EndpointName)) {
        $endpoints += @{
            Protocol = $Protocol
            Address  = $Address
            Name     = [string]::Empty
        }
    }

    # get all defined endpoints by name
    else {
        foreach ($name in @($EndpointName)) {
            $_endpoint = Get-PodeEndpointByName -Name $name -ThrowError
            if ($null -ne $_endpoint) {
                $endpoints += @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.RawAddress
                    Name     = $name
                }
            }
        }
    }

    # convert the endpoint's address into host:port format
    foreach ($_endpoint in $endpoints) {
        if (![string]::IsNullOrWhiteSpace($_endpoint.Address)) {
            $_addr = Get-PodeEndpointInfo -Address $_endpoint.Address -AnyPortOnZero
            $_endpoint.Address = "$($_addr.Host):$($_addr.Port)"
        }
    }

    return $endpoints
}

function Get-PodeEndpoints {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')]
        [string[]]
        $Type
    )

    $endpoints = @()

    foreach ($t in $Type) {
        switch ($t.ToLowerInvariant()) {
            'http' {
                $endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('http', 'https') -icontains $_.Protocol })
            }

            'ws' {
                $endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('ws', 'wss') -icontains $_.Protocol })
            }

            'smtp' {
                $endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('smtp', 'smtps') -icontains $_.Protocol })
            }

            'tcp' {
                $endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('tcp', 'tcps') -icontains $_.Protocol })
            }
        }
    }

    return $endpoints
}

function Test-PodeEndpointProtocol {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Http', 'Https', 'Ws', 'Wss', 'Smtp', 'Smtps', 'Tcp', 'Tcps')]
        [string]
        $Protocol
    )

    $endpoint = $PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -ieq $Protocol }
    return ($null -ne $endpoint)
}

function Get-PodeEndpointType {
    param(
        [Parameter()]
        [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
        [string]
        $Protocol
    )

    switch ($Protocol) {
        { $_ -iin @('http', 'https') } {
            'Http'
        }

        { $_ -iin @('ws', 'wss') } {
            'Ws'
        }

        { $_ -iin @('smtp', 'smtps') } {
            'Smtp'
        }

        { $_ -iin @('tcp', 'tcps') } {
            'Tcp'
        }

        default {
            $Protocol
        }
    }
}

function Get-PodeEndpointRunspacePoolName {
    param(
        [Parameter()]
        [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
        [string]
        $Protocol
    )

    switch ($Protocol) {
        { $_ -iin @('http', 'https') } {
            'Web'
        }

        { $_ -iin @('ws', 'wss') } {
            'Signals'
        }

        { $_ -iin @('smtp', 'smtps') } {
            'Smtp'
        }

        { $_ -iin @('tcp', 'tcps') } {
            'Tcp'
        }

        default {
            $Protocol
        }
    }
}

function Test-PodeEndpoints {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')]
        [string]
        $Type
    )

    $endpoints = (Get-PodeEndpoints -Type $Type)
    return (($null -ne $endpoints) -and ($endpoints.Length -gt 0))

}

function Find-PodeEndpointName {
    param(
        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Address,

        [Parameter()]
        [System.Net.EndPoint]
        $LocalAddress,

        [switch]
        $Force,

        [switch]
        $ThrowError,

        [switch]
        $Enabled
    )

    if (!$Enabled -and !$Force) {
        return $null
    }

    if ([string]::IsNullOrWhiteSpace($Protocol) -or
        [string]::IsNullOrWhiteSpace($Address) -or
        [string]::IsNullOrWhiteSpace($LocalAddress)) {
        return $null
    }

    <#
       using Host header
    #>

    # add a default port to the address if missing
    if (!$Address.Contains(':')) {
        $port = Get-PodeDefaultPort -Protocol $Protocol -Real -TlsMode Implicit
        $Address = "$($Address):$($port)"
    }

    # change localhost/computer name to ip address
    if (($Address -ilike 'localhost:*') -or ($Address -ilike "$($PodeContext.Server.ComputerName):*")) {
        $Address = ($Address -ireplace "(localhost|$([regex]::Escape($PodeContext.Server.ComputerName)))\:", "(127\.0\.0\.1|0\.0\.0\.0|\:\:ffff\:127\.0\.0\.1|\:\:ffff\:0\:0|\[\:\:\]|\[\:\:1\]|\:\:1|\:\:|localhost|$([regex]::Escape($PodeContext.Server.ComputerName))):")
    }
    else {
        $Address = [regex]::Escape($Address)
    }

    # create the endpoint key for address
    $key = "$($Protocol)\|$($Address)"

    # try and find endpoint for address
    $key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
            if ($k -imatch $key) {
                $k
                break
            }
        })[0]

    if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
        return $PodeContext.Server.EndpointsMap[$key]
    }

    <#
       using local endpoint from socket
    #>

    # setup the local address as a string
    $_localAddress = "$($LocalAddress.Address.IPAddressToString):$($LocalAddress.Port)"
    $_localAddress = [regex]::Escape($_localAddress)

    # create the endpoint key for local address
    $key = "$($Protocol)\|$($_localAddress)"

    # try and find endpoint for local address
    $key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
            if ($k -imatch $key) {
                $k
                break
            }
        })[0]

    if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
        return $PodeContext.Server.EndpointsMap[$key]
    }

    <#
       check for * address
    #>

    # set * address as string
    $_anyAddress = "(0\.0\.0\.0|\[\:\:\]|\:\:|\:\:ffff\:0\:0):$($LocalAddress.Port)"
    $key = "$($Protocol)\|$($_anyAddress)"

    # try and find endpoint for any address
    $key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
            if ($k -imatch $key) {
                $k
                break
            }
        })[0]

    if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
        return $PodeContext.Server.EndpointsMap[$key]
    }

    # error?
    if ($ThrowError) {
        throw "Endpoint with protocol '$($Protocol)' and address '$($Address)' or local address '$($_localAddress)' does not exist"
    }

    return $null
}

function Get-PodeEndpointByName {
    param(
        [Parameter()]
        [string]
        $Name,

        [switch]
        $ThrowError
    )

    # if an EndpointName was supplied, find it and use it
    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $null
    }

    # ensure it exists
    if ($PodeContext.Server.Endpoints.ContainsKey($Name)) {
        return $PodeContext.Server.Endpoints[$Name]
    }

    # error?
    if ($ThrowError) {
        throw "Endpoint with name '$($Name)' does not exist"
    }

    return $null
}
src\Private\Endware.ps1
function Invoke-PodeEndware {
    param(
        [Parameter()]
        $Endware
    )

    # if there's no endware, do nothing
    if (($null -eq $Endware) -or ($Endware.Length -eq 0)) {
        return
    }

    # loop through each of the endware, invoking the next if it returns true
    foreach ($eware in @($Endware)) {
        if (($null -eq $eware) -or ($null -eq $eware.Logic)) {
            continue
        }

        try {
            $null = Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments $eware.Arguments -UsingVariables $eware.UsingVariables -Scoped -Splat
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}
src\Private\Events.ps1
function Invoke-PodeEvent {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type
    )

    # do nothing if no events
    if (($null -eq $PodeContext.Server.Events) -or ($PodeContext.Server.Events[$Type].Count -eq 0)) {
        return
    }

    # invoke each event's scriptblock
    foreach ($evt in $PodeContext.Server.Events[$Type].Values) {
        if (($null -eq $evt) -or ($null -eq $evt.ScriptBlock)) {
            continue
        }

        try {
            $null = Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $evt.Arguments -UsingVariables $evt.UsingVariables -Scoped -Splat
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}
src\Private\FileMonitor.ps1
function Start-PodeFileMonitor {
    # don't configure if not supplied, or we're running as serverless
    if (!$PodeContext.Server.FileMonitor.Enabled -or $PodeContext.Server.IsServerless) {
        return
    }

    # what folder and filter are we moitoring?
    $folder = $PodeContext.Server.Root
    $filter = '*.*'

    # setup the file monitor
    $watcher = New-Object System.IO.FileSystemWatcher $folder, $filter -Property @{
        IncludeSubdirectories = $true
        NotifyFilter          = [System.IO.NotifyFilters]'FileName,LastWrite,CreationTime'
    }

    $watcher.EnableRaisingEvents = $true

    # setup the monitor timer - only restart server after changes + 2s of no changes
    $timer = New-Object System.Timers.Timer
    $timer.AutoReset = $false
    $timer.Interval = 2000

    # setup the message data for the events
    $msgData = @{
        Timer    = $timer
        Settings = $PodeContext.Server.FileMonitor
    }

    # setup the events script logic
    $action = {
        # if there are exclusions, and one matches, return
        if (($null -ne $Event.MessageData.Settings.Exclude) -and ($Event.SourceEventArgs.Name -imatch $Event.MessageData.Settings.Exclude)) {
            return
        }

        # if there are inclusions, and none match, return
        if (($null -ne $Event.MessageData.Settings.Include) -and ($Event.SourceEventArgs.Name -inotmatch $Event.MessageData.Settings.Include)) {
            return
        }

        # if enabled, add the file to the list of files that trigggered the restart
        if ($Event.MessageData.Settings.ShowFiles) {
            $name = "[$($Event.SourceEventArgs.ChangeType)] $($Event.SourceEventArgs.Name)"

            if ($Event.MessageData.Settings.Files -inotcontains $name) {
                $Event.MessageData.Settings.Files += $name
            }
        }

        # restart the timer
        $Event.MessageData.Timer.Stop()
        $Event.MessageData.Timer.Start()
    }

    # listen out of file created, changed, deleted events
    Register-ObjectEvent -InputObject $watcher -EventName 'Created' `
        -SourceIdentifier (Get-PodeFileMonitorName Create) -Action $action -MessageData $msgData -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Changed' `
        -SourceIdentifier (Get-PodeFileMonitorName Update) -Action $action -MessageData $msgData -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Deleted' `
        -SourceIdentifier (Get-PodeFileMonitorName Delete) -Action $action -MessageData $msgData -SupportEvent

    # listen out for timer ticks to reset server
    Register-ObjectEvent -InputObject $timer -EventName 'Elapsed' -SourceIdentifier (Get-PodeFileMonitorTimerName) -Action {
        # if enabled, show the files that triggered the restart
        if ($Event.MessageData.FileSettings.ShowFiles) {
            if (!$Event.MessageData.Quiet) {
                Write-Host 'The following files have changed:' -ForegroundColor Magenta

                foreach ($file in $Event.MessageData.FileSettings.Files) {
                    Write-Host "> $($file)" -ForegroundColor Magenta
                }
            }

            $Event.MessageData.FileSettings.Files = @()
        }

        # trigger the restart
        $Event.MessageData.Tokens.Restart.Cancel()
        $Event.Sender.Stop()
    } -MessageData @{
        Tokens       = $PodeContext.Tokens
        FileSettings = $PodeContext.Server.FileMonitor
        Quiet        = $PodeContext.Server.Quiet
    } -SupportEvent
}

function Stop-PodeFileMonitor {
    if ($PodeContext.Server.IsServerless) {
        return
    }

    if ($PodeContext.Server.FileMonitor.Enabled) {
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Create) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Delete) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Update) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorTimerName) -Force
    }
}

function Get-PodeFileMonitorName {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Create', 'Delete', 'Update')]
        [string]
        $Type
    )

    return "PodeFileMonitor$($Type)"
}

function Get-PodeFileMonitorTimerName {
    return 'PodeFileMonitorTimer'
}
src\Private\FileWatchers.ps1
using namespace Pode

function Test-PodeFileWatchersExist {
    return (($null -ne $PodeContext.Fim) -and (($PodeContext.Fim.Enabled) -or ($PodeContext.Fim.Items.Count -gt 0)))
}

function New-PodeFileWatcher {
    $watcher = [PodeWatcher]::new($PodeContext.Tokens.Cancellation.Token)
    $watcher.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
    $watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
    return $watcher
}

function Start-PodeFileWatcherRunspace {
    if (!(Test-PodeFileWatchersExist)) {
        return
    }

    try {
        # create the watcher
        $watcher = New-PodeFileWatcher

        # register file watchers and events
        foreach ($item in $PodeContext.Fim.Items.Values) {
            foreach ($path in $item.Paths) {
                Write-Verbose "Creating FileWatcher for '$($path)'"
                $fileWatcher = [PodeFileWatcher]::new($item.Name, $path, $item.IncludeSubdirectories, $item.InternalBufferSize, $item.NotifyFilters)

                foreach ($evt in $item.Events) {
                    Write-Verbose "-> Registering event: $($evt)"
                    $fileWatcher.RegisterEvent($evt)
                }

                $watcher.AddFileWatcher($fileWatcher)
            }
        }

        $watcher.Start()
        $PodeContext.Watchers += $watcher
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $watcher
        throw $_.Exception
    }

    $watchScript = {
        param(
            [Parameter(Mandatory = $true)]
            $Watcher,

            [Parameter(Mandatory = $true)]
            [int]
            $ThreadId
        )

        try {
            while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                $evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token))

                try {
                    try {
                        # get file watcher
                        $fileWatcher = $PodeContext.Fim.Items[$evt.FileWatcher.Name]
                        if ($null -eq $fileWatcher) {
                            continue
                        }

                        # if there are exclusions, and one matches, return
                        $exc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Exclude)
                        if (($null -ne $exc) -and ($evt.Name -imatch $exc)) {
                            continue
                        }

                        # if there are inclusions, and none match, return
                        $inc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Include)
                        if (($null -ne $inc) -and ($evt.Name -inotmatch $inc)) {
                            continue
                        }

                        # set file event object
                        $FileEvent = @{
                            Type       = $evt.ChangeType
                            FullPath   = $evt.FullPath
                            Name       = $evt.Name
                            Old        = @{
                                FullPath = $evt.OldFullPath
                                Name     = $evt.OldName
                            }
                            Parameters = @{}
                            Lockable   = $PodeContext.Threading.Lockables.Global
                            Timestamp  = [datetime]::UtcNow
                            Metadata   = @{}
                        }

                        # do we have any parameters?
                        if ($fileWatcher.Placeholders.Exist -and ($FileEvent.FullPath -imatch $fileWatcher.Placeholders.Path)) {
                            $FileEvent.Parameters = $Matches
                        }

                        # invoke main script
                        $null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat
                    }
                    catch [System.OperationCanceledException] {
                    }
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                }
                finally {
                    $FileEvent = $null
                    Close-PodeDisposable -Disposable $evt
                }
            }
        }
        catch [System.OperationCanceledException] {
        }
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    1..$PodeContext.Threads.Files | ForEach-Object {
        Add-PodeRunspace -Type Files -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher; 'ThreadId' = $_ }
    }

    # script to keep file watcher server alive until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory = $true)]
            $Watcher
        )

        try {
            while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Watcher
        }
    }

    Add-PodeRunspace -Type Files -ScriptBlock $waitScript -Parameters @{ 'Watcher' = $watcher } -NoProfile
}
src\Private\Gui.ps1
function Test-PodeGuiEnabled {
    return ($PodeContext.Server.Gui.Enabled -and
        !$PodeContext.Server.IsServerless -and
        !$PodeContext.Server.IsIIS -and
        !$PodeContext.Server.IsHeroku)
}

function Start-PodeGuiRunspace {
    # do nothing if gui not enabled, or running as serverless
    if (!(Test-PodeGuiEnabled)) {
        return
    }

    $script = {
        try {
            # if there are multiple endpoints, flag warning we're only using the first - unless explicitly set
            if ($null -eq $PodeContext.Server.Gui.Endpoint) {
                if ($PodeContext.Server.Endpoints.Values.Count -gt 1) {
                    Write-PodeHost 'Multiple endpoints defined, only the first will be used for the GUI' -ForegroundColor Yellow
                }
            }

            # get the endpoint on which we're currently listening, or use explicitly passed one
            $uri = (Get-PodeEndpointUrl -Endpoint $PodeContext.Server.Gui.Endpoint)

            # poll the server for a response
            $count = 0

            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                try {
                    $null = Invoke-WebRequest -Method Get -Uri $uri -UseBasicParsing -ErrorAction Stop
                    if (!$?) {
                        throw
                    }

                    break
                }
                catch {
                    $count++
                    if ($count -le 50) {
                        Start-Sleep -Milliseconds 200
                    }
                    else {
                        throw "Failed to connect to URL: $($uri)"
                    }
                }
            }

            # import the WPF assembly
            $null = [System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework')
            $null = [System.Reflection.Assembly]::LoadWithPartialName('PresentationCore')

            # Check for CefSharp
            $loadCef = [bool]([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.FullName.StartsWith('CefSharp.Wpf,') })

            # setup the WPF XAML for the server
            # Check for CefSharp and used Chromium based WPF if Modules exists
            if ($loadCef) {
                $gui_browser = "
                <Window
                    xmlns=`"http://schemas.microsoft.com/winfx/2006/xaml/presentation`"
                    xmlns:wpf=`"clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf`"
                    xmlns:x=`"http://schemas.microsoft.com/winfx/2006/xaml`"
                    Title=`"$($PodeContext.Server.Gui.Title)`"
                    Height=`"$($PodeContext.Server.Gui.Height)`"
                    Width=`"$($PodeContext.Server.Gui.Width)`"
                    ResizeMode=`"$($PodeContext.Server.Gui.ResizeMode)`"
                    WindowStartupLocation=`"CenterScreen`"
                    ShowInTaskbar = `"$($PodeContext.Server.Gui.ShowInTaskbar)`"
                    WindowStyle = `"$($PodeContext.Server.Gui.WindowStyle)`">
                        <Window.TaskbarItemInfo>
                            <TaskbarItemInfo />
                        </Window.TaskbarItemInfo>
                        <Border Grid.Row=`"1`" BorderBrush=`"Gray`" BorderThickness=`"0,1`">
                            <wpf:ChromiumWebBrowser x:Name=`"Browser`" Address=`"$uri`"/>
                        </Border>
                </Window>"
            }
            else {
                # Fall back to the IE based WPF Browser
                $gui_browser = "
                    <Window
                        xmlns=`"http://schemas.microsoft.com/winfx/2006/xaml/presentation`"
                        xmlns:x=`"http://schemas.microsoft.com/winfx/2006/xaml`"
                        Title=`"$($PodeContext.Server.Gui.Title)`"
                        Height=`"$($PodeContext.Server.Gui.Height)`"
                        Width=`"$($PodeContext.Server.Gui.Width)`"
                        ResizeMode=`"$($PodeContext.Server.Gui.ResizeMode)`"
                        WindowStartupLocation=`"CenterScreen`"
                        ShowInTaskbar = `"$($PodeContext.Server.Gui.ShowInTaskbar)`"
                        WindowStyle = `"$($PodeContext.Server.Gui.WindowStyle)`">
                            <Window.TaskbarItemInfo>
                                <TaskbarItemInfo />
                            </Window.TaskbarItemInfo>
                            <WebBrowser Name=`"WebBrowser`"></WebBrowser>
                    </Window>"
            }

            # read in the XAML
            $reader = [System.Xml.XmlNodeReader]::new([xml]$gui_browser)
            $form = [Windows.Markup.XamlReader]::Load($reader)

            # set other options
            $form.TaskbarItemInfo.Description = $form.Title

            # add the icon to the form
            if (!(Test-PodeIsEmpty $PodeContext.Server.Gui.Icon)) {
                $icon = [Uri]::new($PodeContext.Server.Gui.Icon)
                $form.Icon = [Windows.Media.Imaging.BitmapFrame]::Create($icon)
            }

            # set the state of the window onload
            if (!(Test-PodeIsEmpty $PodeContext.Server.Gui.WindowState)) {
                $form.WindowState = $PodeContext.Server.Gui.WindowState
            }

            # get the browser object from XAML and navigate to base page if Cef is not loaded
            if (!$loadCef) {
                $form.FindName('WebBrowser').Navigate($uri)
            }

            # display the form
            Write-PodeHost 'Opening GUI' -ForegroundColor Yellow
            $null = $form.ShowDialog()
            Start-Sleep -Seconds 1
        }
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
        finally {
            # invoke the cancellation token to close the server
            $PodeContext.Tokens.Cancellation.Cancel()
        }
    }

    Add-PodeRunspace -Type Gui -ScriptBlock $script
}
src\Private\Helpers.ps1
using namespace Pode

# read in the content from a dynamic pode file and invoke its content
function ConvertFrom-PodeFile {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Content,

        [Parameter()]
        $Data = @{}
    )

    # if we have data, then setup the data param
    if ($null -ne $Data -and $Data.Count -gt 0) {
        $Content = "param(`$data)`nreturn `"$($Content -replace '"', '``"')`""
    }
    else {
        $Content = "return `"$($Content -replace '"', '``"')`""
    }

    # invoke the content as a script to generate the dynamic content
    return (Invoke-PodeScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return)
}

function Get-PodeViewEngineType {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    # work out the engine to use when parsing the file
    $type = $PodeContext.Server.ViewEngine.Type

    $ext = Get-PodeFileExtension -Path $Path -TrimPeriod
    if (![string]::IsNullOrWhiteSpace($ext) -and ($ext -ine $PodeContext.Server.ViewEngine.Extension)) {
        $type = $ext
    }

    return $type
}

function Get-PodeFileContentUsingViewEngine {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data
    )

    # work out the engine to use when parsing the file
    $engine = Get-PodeViewEngineType -Path $Path

    # setup the content
    $content = [string]::Empty

    # run the relevant engine logic
    switch ($engine.ToLowerInvariant()) {
        'html' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
        }

        'md' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
        }

        'pode' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
            $content = ConvertFrom-PodeFile -Content $content -Data $Data
        }

        default {
            if ($null -ne $PodeContext.Server.ViewEngine.ScriptBlock) {
                $_args = @($Path)
                if (($null -ne $Data) -and ($Data.Count -gt 0)) {
                    $_args = @($Path, $Data)
                }

                $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables -Return -Splat)
            }
        }
    }

    return $content
}

function Get-PodeFileContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    return (Get-Content -Path $Path -Raw -Encoding utf8)
}

function Get-PodeType {
    param(
        [Parameter()]
        $Value
    )

    if ($null -eq $Value) {
        return $null
    }

    $type = $Value.GetType()
    return @{
        Name     = $type.Name.ToLowerInvariant()
        BaseName = $type.BaseType.Name.ToLowerInvariant()
    }
}

function Get-PodePSVersionTable {
    return $PSVersionTable
}

function Test-PodeIsAdminUser {
    # check the current platform, if it's unix then return true
    if (Test-PodeIsUnix) {
        return $true
    }

    try {
        $principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
        if ($null -eq $principal) {
            return $false
        }

        return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    catch [exception] {
        Write-PodeHost 'Error checking user administrator priviledges' -ForegroundColor Red
        Write-PodeHost $_.Exception.Message -ForegroundColor Red
        return $false
    }
}

function Get-PodeHostIPRegex {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Both', 'Hostname', 'IP')]
        [string]
        $Type
    )

    $ip_rgx = '\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all'
    $host_rgx = '([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+'

    switch ($Type.ToLowerInvariant()) {
        'both' {
            return "(?<host>($($ip_rgx)|$($host_rgx)))"
        }

        'hostname' {
            return "(?<host>($($host_rgx)))"
        }

        'ip' {
            return "(?<host>($($ip_rgx)))"
        }
    }
}

function Get-PodePortRegex {
    return '(?<port>\d+)'
}

function Get-PodeEndpointInfo {
    param(
        [Parameter()]
        [string]
        $Address,

        [switch]
        $AnyPortOnZero
    )

    if ([string]::IsNullOrWhiteSpace($Address)) {
        return $null
    }

    $hostRgx = Get-PodeHostIPRegex -Type Both
    $portRgx = Get-PodePortRegex
    $cmbdRgx = "$($hostRgx)\:$($portRgx)"

    # validate that we have a valid ip/host:port address
    if (!(($Address -imatch "^$($cmbdRgx)$") -or ($Address -imatch "^$($hostRgx)[\:]{0,1}") -or ($Address -imatch "[\:]{0,1}$($portRgx)$"))) {
        throw "Failed to parse '$($Address)' as a valid IP/Host:Port address"
    }

    # grab the ip address/hostname
    $_host = $Matches['host']
    if ([string]::IsNullOrWhiteSpace($_host)) {
        $_host = '*'
    }

    # ensure we have a valid ip address/hostname
    if (!(Test-PodeIPAddress -IP $_host)) {
        throw "The IP address supplied is invalid: $($_host)"
    }

    # grab the port
    $_port = $Matches['port']
    if ([string]::IsNullOrWhiteSpace($_port)) {
        $_port = 0
    }

    # ensure the port is valid
    if ($_port -lt 0) {
        throw "The port cannot be negative: $($_port)"
    }

    # return the info
    return @{
        Host = $_host
        Port = (Resolve-PodeValue -Check ($AnyPortOnZero -and ($_port -eq 0)) -TrueValue '*' -FalseValue $_port)
    }
}

function Test-PodeIPAddress {
    param(
        [Parameter()]
        [string]
        $IP,

        [switch]
        $IPOnly
    )

    if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) {
        return $true
    }

    if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
        return (!$IPOnly)
    }

    try {
        $null = [System.Net.IPAddress]::Parse($IP)
        return $true
    }
    catch [exception] {
        return $false
    }
}

function Test-PodeHostname {
    param(
        [Parameter()]
        [string]
        $Hostname
    )

    return ($Hostname -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$")
}

function ConvertTo-PodeIPAddress {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Address
    )

    return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Address).Address.ToString())
}

function Get-PodeIPAddressesForHostname {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Hostname,

        [Parameter(Mandatory = $true)]
        [ValidateSet('All', 'IPv4', 'IPv6')]
        [string]
        $Type
    )

    if (!(Test-PodeHostname -Hostname $Hostname)) {
        return $Hostname
    }

    # get the ip addresses for the hostname
    try {
        $ips = @([System.Net.Dns]::GetHostAddresses($Hostname))
    }
    catch {
        return '127.0.0.1'
    }

    # return ips based on type
    switch ($Type.ToLowerInvariant()) {
        'ipv4' {
            $ips = @(foreach ($ip in $ips) {
                    if ($ip.AddressFamily -ieq 'InterNetwork') {
                        $ip
                    }
                })
        }

        'ipv6' {
            $ips = @(foreach ($ip in $ips) {
                    if ($ip.AddressFamily -ieq 'InterNetworkV6') {
                        $ip
                    }
                })
        }
    }

    return (@($ips)).IPAddressToString
}

function Test-PodeIPAddressLocal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $IP
    )

    return (@('127.0.0.1', '::1', '[::1]', '::ffff:127.0.0.1', 'localhost') -icontains $IP)
}

function Test-PodeIPAddressAny {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $IP
    )

    return (@('0.0.0.0', '*', 'all', '::', '[::]') -icontains $IP)
}

function Test-PodeIPAddressLocalOrAny {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $IP
    )

    return ((Test-PodeIPAddressLocal -IP $IP) -or (Test-PodeIPAddressAny -IP $IP))
}

function Resolve-PodeIPDualMode {
    param(
        [Parameter()]
        [ipaddress]
        $IP
    )

    # do nothing if IPv6Any
    if ($IP -eq [ipaddress]::IPv6Any) {
        return $IP
    }

    # check loopbacks
    if (($IP -eq [ipaddress]::Loopback) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) {
        return @($IP, [ipaddress]::IPv6Loopback)
    }

    if ($IP -eq [ipaddress]::IPv6Loopback) {
        return @($IP, [ipaddress]::Loopback)
    }

    # if iIPv4, convert and return both
    if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) {
        return @($IP, $IP.MapToIPv6())
    }

    # if IPv6, only convert if valid IPv4
    if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) -and $IP.IsIPv4MappedToIPv6) {
        return @($IP, $IP.MapToIPv4())
    }

    # just return the IP
    return $IP
}

function Get-PodeIPAddress {
    param(
        [Parameter()]
        [string]
        $IP,

        [switch]
        $DualMode
    )

    # any address for IPv4 (or IPv6 for DualMode)
    if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) {
        if ($DualMode) {
            return [System.Net.IPAddress]::IPv6Any
        }

        return [System.Net.IPAddress]::Any
    }

    # any address for IPv6 explicitly
    if ($IP -iin @('::', '[::]')) {
        return [System.Net.IPAddress]::IPv6Any
    }

    # localhost
    if ($IP -ieq 'localhost') {
        return [System.Net.IPAddress]::Loopback
    }

    # localhost IPv6 explicitly
    if ($IP -iin @('[::1]', '::1')) {
        return [System.Net.IPAddress]::IPv6Loopback
    }

    # hostname
    if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
        return $IP
    }

    # raw ip
    return [System.Net.IPAddress]::Parse($IP)
}

function Test-PodeIPAddressInRange {
    param(
        [Parameter(Mandatory = $true)]
        $IP,

        [Parameter(Mandatory = $true)]
        $LowerIP,

        [Parameter(Mandatory = $true)]
        $UpperIP
    )

    if ($IP.Family -ine $LowerIP.Family) {
        return $false
    }

    $valid = $true

    foreach ($i in 0..3) {
        if (($IP.Bytes[$i] -lt $LowerIP.Bytes[$i]) -or ($IP.Bytes[$i] -gt $UpperIP.Bytes[$i])) {
            $valid = $false
            break
        }
    }

    return $valid
}

function Test-PodeIPAddressIsSubnetMask {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $IP
    )

    return (($IP -split '/').Length -gt 1)
}

function Get-PodeSubnetRange {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SubnetMask
    )

    # split for ip and number of 1 bits
    $split = $SubnetMask -split '/'
    if ($split.Length -le 1) {
        return $null
    }

    $ip_parts = $split[0] -isplit '\.'
    $bits = [int]$split[1]

    # generate the netmask
    $network = @('', '', '', '')
    $count = 0

    foreach ($i in 0..3) {
        foreach ($b in 1..8) {
            $count++

            if ($count -le $bits) {
                $network[$i] += '1'
            }
            else {
                $network[$i] += '0'
            }
        }
    }

    # covert netmask to bytes
    foreach ($i in 0..3) {
        $network[$i] = [Convert]::ToByte($network[$i], 2)
    }

    # calculate the bottom range
    $bottom = @(foreach ($i in 0..3) {
            [byte]([byte]$network[$i] -band [byte]$ip_parts[$i])
        })

    # calculate the range
    $range = @(foreach ($i in 0..3) {
            256 + (-bnot [byte]$network[$i])
        })

    # calculate the top range
    $top = @(foreach ($i in 0..3) {
            [byte]([byte]$ip_parts[$i] + [byte]$range[$i])
        })

    return @{
        'Lower'   = ($bottom -join '.')
        'Upper'   = ($top -join '.')
        'Range'   = ($range -join '.')
        'Netmask' = ($network -join '.')
        'IP'      = ($ip_parts -join '.')
    }
}

function Add-PodeRunspace {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $Parameters,

        [Parameter()]
        [System.Management.Automation.PSDataCollection[psobject]]
        $OutputStream = $null,

        [switch]
        $Forget,

        [switch]
        $NoProfile,

        [switch]
        $PassThru
    )

    try {
        # create powershell pipelines
        $ps = [powershell]::Create()
        $ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool

        # load modules/drives
        if (!$NoProfile) {
            $null = $ps.AddScript("Open-PodeRunspace -Type '$($Type)'")
        }

        # load main script
        $null = $ps.AddScript($ScriptBlock)

        # load parameters
        if (!(Test-PodeIsEmpty $Parameters)) {
            $Parameters.Keys | ForEach-Object {
                $null = $ps.AddParameter($_, $Parameters[$_])
            }
        }

        # start the pipeline
        if ($null -eq $OutputStream) {
            $pipeline = $ps.BeginInvoke()
        }
        else {
            $pipeline = $ps.BeginInvoke($OutputStream, $OutputStream)
        }

        # do we need to remember this pipeline? sorry, what did you say?
        if ($Forget) {
            $null = $pipeline
        }

        # or do we need to return it for custom processing? ie: tasks
        elseif ($PassThru) {
            return @{
                Pipeline = $ps
                Handler  = $pipeline
            }
        }

        # or store it here for later clean-up
        else {
            $PodeContext.Runspaces += @{
                Pool     = $Type
                Pipeline = $ps
                Handler  = $pipeline
                Stopped  = $false
            }
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Open-PodeRunspace {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Type
    )

    try {
        Import-PodeModules
        Add-PodePSDrives
        $PodeContext.RunspacePools[$Type].State = 'Ready'
    }
    catch {
        if ($PodeContext.RunspacePools[$Type].State -ieq 'waiting') {
            $PodeContext.RunspacePools[$Type].State = 'Error'
        }

        $_ | Out-Default
        $_.ScriptStackTrace | Out-Default
        throw
    }
}

function Close-PodeRunspaces {
    param(
        [switch]
        $ClosePool
    )

    if ($PodeContext.Server.IsServerless) {
        return
    }

    try {
        if (!(Test-PodeIsEmpty $PodeContext.Runspaces)) {
            Write-Verbose 'Waiting until all Listeners are disposed'

            $count = 0
            $continue = $false
            while ($count -le 10) {
                Start-Sleep -Seconds 1
                $count++

                $continue = $false
                foreach ($listener in $PodeContext.Listeners) {
                    if (!$listener.IsDisposed) {
                        $continue = $true
                        break
                    }
                }

                foreach ($receiver in $PodeContext.Receivers) {
                    if (!$receiver.IsDisposed) {
                        $continue = $true
                        break
                    }
                }

                foreach ($watcher in $PodeContext.Watchers) {
                    if (!$watcher.IsDisposed) {
                        $continue = $true
                        break
                    }
                }

                if ($continue) {
                    continue
                }

                break
            }

            Write-Verbose 'All Listeners disposed'

            # now dispose runspaces
            Write-Verbose 'Disposing Runspaces'
            $runspaceErrors = @(foreach ($item in $PodeContext.Runspaces) {
                    if ($item.Stopped) {
                        continue
                    }

                    try {
                        # only do this, if the pool is in error
                        if ($PodeContext.RunspacePools[$item.Pool].State -ieq 'error') {
                            $item.Pipeline.EndInvoke($item.Handler)
                        }
                    }
                    catch {
                        "$($item.Pool) runspace failed to load: $($_.Exception.InnerException.Message)"
                    }

                    Close-PodeDisposable -Disposable $item.Pipeline
                    $item.Stopped = $true
                })

            # dispose of schedule runspaces
            if ($PodeContext.Schedules.Processes.Count -gt 0) {
                foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) {
                    Close-PodeScheduleInternal -Process $PodeContext.Schedules.Processes[$key]
                }
            }

            # dispose of task runspaces
            if ($PodeContext.Tasks.Results.Count -gt 0) {
                foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) {
                    Close-PodeTaskInternal -Result $PodeContext.Tasks.Results[$key]
                }
            }

            $PodeContext.Runspaces = @()
            Write-Verbose 'Runspaces disposed'
        }

        # close/dispose the runspace pools
        if ($ClosePool) {
            Close-PodeRunspacePools
        }

        # check for runspace errors
        if (($null -ne $runspaceErrors) -and ($runspaceErrors.Length -gt 0)) {
            foreach ($err in $runspaceErrors) {
                if ($null -eq $err) {
                    continue
                }

                throw $err
            }
        }

        # garbage collect
        [GC]::Collect()
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Get-PodeConsoleKey {
    if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) {
        return $null
    }

    return [Console]::ReadKey($true)
}

function Test-PodeTerminationPressed {
    param(
        [Parameter()]
        $Key = $null
    )

    if ($PodeContext.Server.DisableTermination) {
        return $false
    }

    return (Test-PodeKeyPressed -Key $Key -Character 'c')
}

function Test-PodeRestartPressed {
    param(
        [Parameter()]
        $Key = $null
    )

    return (Test-PodeKeyPressed -Key $Key -Character 'r')
}

function Test-PodeOpenBrowserPressed {
    param(
        [Parameter()]
        $Key = $null
    )

    return (Test-PodeKeyPressed -Key $Key -Character 'b')
}

function Test-PodeKeyPressed {
    param(
        [Parameter()]
        $Key = $null,

        [Parameter(Mandatory = $true)]
        [string]
        $Character
    )

    if ($null -eq $Key) {
        $Key = Get-PodeConsoleKey
    }

    return (($null -ne $Key) -and ($Key.Key -ieq $Character) -and
        (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-PodeIsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift))))
}

function Close-PodeServerInternal {
    param(
        [switch]
        $ShowDoneMessage
    )

    # ensure the token is cancelled
    if ($null -ne $PodeContext.Tokens.Cancellation) {
        Write-Verbose 'Cancelling main cancellation token'
        $PodeContext.Tokens.Cancellation.Cancel()
    }

    # stop all current runspaces
    Write-Verbose 'Closing runspaces'
    Close-PodeRunspaces -ClosePool

    # stop the file monitor if it's running
    Write-Verbose 'Stopping file monitor'
    Stop-PodeFileMonitor

    try {
        # remove all the cancellation tokens
        Write-Verbose 'Disposing cancellation tokens'
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart

        # dispose mutex/semaphores
        Write-Verbose 'Diposing mutex and semaphores'
        Clear-PodeMutexes
        Clear-PodeSemaphores
    }
    catch {
        $_ | Out-Default
    }

    # remove all of the pode temp drives
    Write-Verbose 'Removing internal PSDrives'
    Remove-PodePSDrives

    if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) {
        Write-PodeHost ' Done' -ForegroundColor Green
    }
}

function New-PodePSDrive {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Name
    )

    # if the path is a share, do nothing
    if ($Path.StartsWith('\\')) {
        return $Path
    }

    # if no name is passed, used a randomly generated one
    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = "PodeDir$(New-PodeGuid)"
    }

    # if the path supplied doesn't exist, error
    if (!(Test-Path $Path)) {
        throw "Path does not exist: $($Path)"
    }

    # resolve the path
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve

    # create the temp drive
    if (!(Test-PodePSDrive -Name $Name -Path $Path)) {
        $drive = (New-PSDrive -Name $Name -PSProvider FileSystem -Root $Path -Scope Global -ErrorAction Stop)
    }
    else {
        $drive = Get-PodePSDrive -Name $Name
    }

    # store internally, and return the drive's name
    if (!$PodeContext.Server.Drives.ContainsKey($drive.Name)) {
        $PodeContext.Server.Drives[$drive.Name] = $Path
    }

    return "$($drive.Name):$([System.IO.Path]::DirectorySeparatorChar)"
}

function Get-PodePSDrive {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (Get-PSDrive -Name $Name -PSProvider FileSystem -Scope Global -ErrorAction Ignore)
}

function Test-PodePSDrive {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Path
    )

    $drive = Get-PodePSDrive -Name $Name
    if ($null -eq $drive) {
        return $false
    }

    if (![string]::IsNullOrWhiteSpace($Path)) {
        return ($drive.Root -ieq $Path)
    }

    return $true
}

function Add-PodePSDrives {
    foreach ($key in $PodeContext.Server.Drives.Keys) {
        $null = New-PodePSDrive -Path $PodeContext.Server.Drives[$key] -Name $key
    }
}

function Import-PodeModules {
    # import other modules in the session
    foreach ($path in $PodeContext.Server.Modules.Values) {
        $null = Import-Module $path -DisableNameChecking -Scope Global -ErrorAction Stop
    }
}

<#
.SYNOPSIS
Creates and registers inbuilt PowerShell drives for the Pode server's default folders.

.DESCRIPTION
This function sets up inbuilt PowerShell drives for the Pode web server's default directories: views, public content, and error pages. For each of these directories, if the physical path exists on the server, a new PowerShell drive is created and mapped to this path. These drives provide an easy and consistent way to access server resources like views, static files, and custom error pages within the Pode application.

The function leverages `$PodeContext` to access the server's configuration and to determine the paths for these default folders. If a folder's path exists, the function uses `New-PodePSDrive` to create a PowerShell drive for it and stores this drive in the server's `InbuiltDrives` dictionary, keyed by the folder type.

.PARAMETER None

.EXAMPLE
Add-PodePSInbuiltDrive

This example is typically called within the Pode server setup script or internally by the Pode framework to initialize the PowerShell drives for the server's default folders.

.NOTES
- The function is designed to be used within the Pode framework and relies on the global `$PodeContext` variable for configuration.
- It specifically checks for the existence of paths for views, public content, and errors before attempting to create drives for them.
- This is an internal function and may change in future releases of Pode.
#>
function Add-PodePSInbuiltDrive {

    # create drive for views, if path exists
    $path = (Join-PodeServerRoot -Folder $PodeContext.Server.DefaultFolders.Views)
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Views] = (New-PodePSDrive -Path $path)
    }

    # create drive for public content, if path exists
    $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Public)
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Public] = (New-PodePSDrive -Path $path)
    }

    # create drive for errors, if path exists
    $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Errors)
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Errors] = (New-PodePSDrive -Path $path)
    }
}

function Remove-PodePSDrives {
    $null = Get-PSDrive PodeDir* | Remove-PSDrive
}

function Join-PodeServerRoot {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Folder,

        [Parameter()]
        [string]
        $FilePath,

        [Parameter()]
        [string]
        $Root
    )

    # use the root path of the server
    if ([string]::IsNullOrWhiteSpace($Root)) {
        $Root = $PodeContext.Server.Root
    }

    # join the folder/file to the root path
    return [System.IO.Path]::Combine($Root, $Folder, $FilePath)
}

function Remove-PodeEmptyItemsFromArray {
    param(
        [Parameter(ValueFromPipeline = $true)]
        $Array
    )

    if ($null -eq $Array) {
        return @()
    }

    return @( @($Array -ne ([string]::Empty)) -ne $null )
}

function Remove-PodeNullKeysFromHashtable {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $Hashtable
    )

    foreach ($key in ($Hashtable.Clone()).Keys) {
        if ($null -eq $Hashtable[$key]) {
            $null = $Hashtable.Remove($key)
            continue
        }

        if (($Hashtable[$key] -is [string]) -and [string]::IsNullOrEmpty($Hashtable[$key])) {
            $null = $Hashtable.Remove($key)
            continue
        }

        if ($Hashtable[$key] -is [array]) {
            if (($Hashtable[$key].Length -eq 1) -and ($null -eq $Hashtable[$key][0])) {
                $null = $Hashtable.Remove($key)
                continue
            }

            foreach ($item in $Hashtable[$key]) {
                if (($item -is [hashtable]) -or ($item -is [System.Collections.Specialized.OrderedDictionary])) {
                    $item | Remove-PodeNullKeysFromHashtable
                }
            }

            continue
        }

        if (($Hashtable[$key] -is [hashtable]) -or ($Hashtable[$key] -is [System.Collections.Specialized.OrderedDictionary])) {
            $Hashtable[$key] | Remove-PodeNullKeysFromHashtable
            continue
        }
    }
}

function Get-PodeFileExtension {
    param(
        [Parameter()]
        [string]
        $Path,

        [switch]
        $TrimPeriod
    )

    $ext = [System.IO.Path]::GetExtension($Path)
    if ($TrimPeriod) {
        $ext = $ext.Trim('.')
    }

    return $ext
}

function Get-PodeFileName {
    param(
        [Parameter()]
        [string]
        $Path,

        [switch]
        $WithoutExtension
    )

    if ($WithoutExtension) {
        return [System.IO.Path]::GetFileNameWithoutExtension($Path)
    }

    return [System.IO.Path]::GetFileName($Path)
}

function Test-PodeValidNetworkFailure {
    param(
        [Parameter()]
        $Exception
    )

    $msgs = @(
        '*network name is no longer available*',
        '*nonexistent network connection*',
        '*the response has completed*',
        '*broken pipe*'
    )

    $match = @(foreach ($msg in $msgs) {
            if ($Exception.Message -ilike $msg) {
                $msg
            }
        })[0]

    return ($null -ne $match)
}

function ConvertFrom-PodeHeaderQValue {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string]
        $Value
    )

    $qs = [ordered]@{}

    # return if no value
    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $qs
    }

    # split the values up
    $parts = @($Value -isplit ',').Trim()

    # go through each part and check its q-value
    foreach ($part in $parts) {
        # default of 1 if no q-value
        if ($part.IndexOf(';q=') -eq -1) {
            $qs[$part] = 1.0
            continue
        }

        # parse for q-value
        $atoms = @($part -isplit ';q=')
        $qs[$atoms[0]] = [double]$atoms[1]
    }

    return $qs
}

function Get-PodeAcceptEncoding {
    param(
        [Parameter()]
        [string]
        $AcceptEncoding,

        [switch]
        $ThrowError
    )

    # return if no encoding
    if ([string]::IsNullOrWhiteSpace($AcceptEncoding)) {
        return [string]::Empty
    }

    # return empty if not compressing
    if (!$PodeContext.Server.Web.Compression.Enabled) {
        return [string]::Empty
    }

    # convert encoding form q-form
    $encodings = ConvertFrom-PodeHeaderQValue -Value $AcceptEncoding
    if ($encodings.Count -eq 0) {
        return [string]::Empty
    }

    # check the encodings for one that matches
    $normal = @('identity', '*')
    $valid = @()

    # build up supported and invalid
    foreach ($encoding in $encodings.Keys) {
        if (($encoding -iin $PodeContext.Server.Compression.Encodings) -or ($encoding -iin $normal)) {
            $valid += @{
                Name  = $encoding
                Value = $encodings[$encoding]
            }
        }
    }

    # if it's empty, just return empty
    if ($valid.Length -eq 0) {
        return [string]::Empty
    }

    # find the highest ranked match
    $found = @{}
    $failOnIdentity = $false

    foreach ($encoding in $valid) {
        if ($encoding.Value -gt $found.Value) {
            $found = $encoding
        }

        if (!$failOnIdentity -and ($encoding.Value -eq 0) -and ($encoding.Name -iin $normal)) {
            $failOnIdentity = $true
        }
    }

    # force found to identity/* if the 0 is not identity - meaning it's still allowed
    if (($found.Value -eq 0) -and !$failOnIdentity) {
        $found = @{
            Name  = 'identity'
            Value = 1.0
        }
    }

    # return invalid, error, or return empty for idenity?
    if ($found.Value -eq 0) {
        if ($ThrowError) {
            throw (New-PodeRequestException -StatusCode 406)
        }
    }

    # else, we're safe
    if ($found.Name -iin $normal) {
        return [string]::Empty
    }

    if ($found.Name -ieq 'x-gzip') {
        return 'gzip'
    }

    return $found.Name
}

function Get-PodeRanges {
    param(
        [Parameter()]
        [string]
        $Range,

        [switch]
        $ThrowError
    )

    # return if no ranges
    if ([string]::IsNullOrWhiteSpace($Range)) {
        return $null
    }

    # split on '='
    $parts = @($Range -isplit '=').Trim()
    if (($parts.Length -le 1) -or ([string]::IsNullOrWhiteSpace($parts[1]))) {
        return $null
    }

    $unit = $parts[0]
    if ($unit -ine 'bytes') {
        if ($ThrowError) {
            throw (New-PodeRequestException -StatusCode 416)
        }

        return $null
    }

    # split on ','
    $parts = @($parts[1] -isplit ',').Trim()

    # parse into From-To hashtable array
    $ranges = @()

    foreach ($atom in $parts) {
        if ($atom -inotmatch '(?<start>[\d]+){0,1}\s?\-\s?(?<end>[\d]+){0,1}') {
            if ($ThrowError) {
                throw (New-PodeRequestException -StatusCode 416)
            }

            return $null
        }

        $ranges += @{
            Start = $Matches['start']
            End   = $Matches['end']
        }
    }

    return $ranges
}

function Get-PodeTransferEncoding {
    param(
        [Parameter()]
        [string]
        $TransferEncoding,

        [switch]
        $ThrowError
    )

    # return if no encoding
    if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
        return [string]::Empty
    }

    # convert encoding form q-form
    $encodings = ConvertFrom-PodeHeaderQValue -Value $TransferEncoding
    if ($encodings.Count -eq 0) {
        return [string]::Empty
    }

    # check the encodings for one that matches
    $normal = @('chunked', 'identity')
    $invalid = @()

    # if we see a supported one, return immediately. else build up invalid one
    foreach ($encoding in $encodings.Keys) {
        if ($encoding -iin $PodeContext.Server.Compression.Encodings) {
            if ($encoding -ieq 'x-gzip') {
                return 'gzip'
            }

            return $encoding
        }

        if ($encoding -iin $normal) {
            continue
        }

        $invalid += $encoding
    }

    # if we have any invalid, throw a 415 error
    if ($invalid.Length -gt 0) {
        if ($ThrowError) {
            throw (New-PodeRequestException -StatusCode 415)
        }

        return $invalid[0]
    }

    # else, we're safe
    return [string]::Empty
}

function Get-PodeEncodingFromContentType {
    param(
        [Parameter()]
        [string]
        $ContentType
    )

    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return [System.Text.Encoding]::UTF8
    }

    $parts = @($ContentType -isplit ';').Trim()

    foreach ($part in $parts) {
        if ($part.StartsWith('charset')) {
            return [System.Text.Encoding]::GetEncoding(($part -isplit '=')[1].Trim())
        }
    }

    return [System.Text.Encoding]::UTF8
}

function New-PodeRequestException {
    param(
        [Parameter(Mandatory = $true)]
        [int]
        $StatusCode
    )

    $err = [System.Net.Http.HttpRequestException]::new()
    $err.Data.Add('PodeStatusCode', $StatusCode)
    return $err
}

function ConvertTo-PodeResponseContent {
    param(
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [string]
        $Delimiter = ',',

        [switch]
        $AsHtml
    )

    # split for the main content type
    $ContentType = Split-PodeContentType -ContentType $ContentType

    # if there is no content-type then convert straight to string
    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return ([string]$InputObject)
    }

    # run action for the content type
    switch ($ContentType) {
        { $_ -ilike '*/json' } {
            if ($InputObject -isnot [string]) {
                if ($Depth -le 0) {
                    return (ConvertTo-Json -InputObject $InputObject -Compress)
                }
                else {
                    return (ConvertTo-Json -InputObject $InputObject -Depth $Depth -Compress)
                }
            }

            if ([string]::IsNullOrWhiteSpace($InputObject)) {
                return '{}'
            }
        }

        { $_ -ilike '*/yaml' -or $_ -ilike '*/x-yaml' } {
            if ($InputObject -isnot [string]) {
                if ($Depth -le 0) {
                    return (ConvertTo-PodeYamlInternal -InputObject $InputObject )
                }
                else {
                    return (ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth  )
                }
            }

            if ([string]::IsNullOrWhiteSpace($InputObject)) {
                return '[]'
            }
        }

        { $_ -ilike '*/xml' } {
            if ($InputObject -isnot [string]) {
                $temp = @(foreach ($item in $InputObject) {
                        New-Object psobject -Property $item
                    })

                return ($temp | ConvertTo-Xml -Depth $Depth -As String -NoTypeInformation)
            }

            if ([string]::IsNullOrWhiteSpace($InputObject)) {
                return [string]::Empty
            }
        }

        { $_ -ilike '*/csv' } {
            if ($InputObject -isnot [string]) {
                $temp = @(foreach ($item in $InputObject) {
                        New-Object psobject -Property $item
                    })

                if (Test-PodeIsPSCore) {
                    $temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -IncludeTypeInformation:$false)
                }
                else {
                    $temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -NoTypeInformation)
                }

                return ($temp -join ([environment]::NewLine))
            }

            if ([string]::IsNullOrWhiteSpace($InputObject)) {
                return [string]::Empty
            }
        }

        { $_ -ilike '*/html' } {
            if ($InputObject -isnot [string]) {
                return (($InputObject | ConvertTo-Html) -join ([environment]::NewLine))
            }

            if ([string]::IsNullOrWhiteSpace($InputObject)) {
                return [string]::Empty
            }
        }

        { $_ -ilike '*/markdown' } {
            if ($AsHtml -and ($PSVersionTable.PSVersion.Major -ge 7)) {
                return ($InputObject | ConvertFrom-Markdown).Html
            }
        }
    }

    return ([string]$InputObject)
}

function ConvertFrom-PodeRequestContent {
    param(
        [Parameter()]
        $Request,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $TransferEncoding
    )

    # get the requests content type
    $ContentType = Split-PodeContentType -ContentType $ContentType

    # result object for data/files
    $Result = @{
        Data  = @{}
        Files = @{}
    }

    # if there is no content-type then do nothing
    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return $Result
    }

    # if the content-type is not multipart/form-data, get the string data
    if ($ContentType -ine 'multipart/form-data') {
        # get the content based on server type
        if ($PodeContext.Server.IsServerless) {
            switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) {
                'awslambda' {
                    $Content = $Request.body
                }

                'azurefunctions' {
                    $Content = $Request.RawBody
                }
            }
        }
        else {
            # if the request is compressed, attempt to uncompress it
            if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
                # create a compressed stream to decompress the req bytes
                $ms = New-Object -TypeName System.IO.MemoryStream
                $ms.Write($Request.RawBody, 0, $Request.RawBody.Length)
                $null = $ms.Seek(0, 0)
                $stream = New-Object "System.IO.Compression.$($TransferEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Decompress)

                # read the decompressed bytes
                $Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding
            }
            else {
                $Content = $Request.Body
            }
        }

        # if there is no content then do nothing
        if ([string]::IsNullOrWhiteSpace($Content)) {
            return $Result
        }

        # check if there is a defined custom body parser
        if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
            $parser = $PodeContext.Server.BodyParsers[$ContentType]
            $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $Content -UsingVariables $parser.UsingVariables -Return)
            $Content = $null
            return $Result
        }
    }

    # run action for the content type
    switch ($ContentType) {
        { $_ -ilike '*/json' } {
            if (Test-PodeIsPSCore) {
                $Result.Data = ($Content | ConvertFrom-Json -AsHashtable)
            }
            else {
                $Result.Data = ($Content | ConvertFrom-Json)
            }
        }

        { $_ -ilike '*/xml' } {
            $Result.Data = [xml]($Content)
        }

        { $_ -ilike '*/csv' } {
            $Result.Data = ($Content | ConvertFrom-Csv)
        }

        { $_ -ilike '*/x-www-form-urlencoded' } {
            $Result.Data = (ConvertFrom-PodeNameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content)))
        }

        { $_ -ieq 'multipart/form-data' } {
            # parse multipart form data
            $form = $null

            if ($PodeContext.Server.IsServerless) {
                switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) {
                    'awslambda' {
                        $Content = $Request.body
                    }

                    'azurefunctions' {
                        $Content = $Request.Body
                    }
                }

                $form = [PodeForm]::Parse($Content, $WebEvent.ContentType, [System.Text.Encoding]::UTF8)
            }
            else {
                $Request.ParseFormData()
                $form = $Request.Form
            }

            # set the files/data
            foreach ($file in $form.Files) {
                $Result.Files.Add($file.FileName, $file)
            }

            foreach ($item in $form.Data) {
                if ($item.IsSingular) {
                    $Result.Data.Add($item.Key, $item.Values[0])
                }
                else {
                    $Result.Data.Add($item.Key, $item.Values)
                }
            }

            $form = $null
        }

        default {
            $Result.Data = $Content
        }
    }

    $Content = $null
    return $Result
}

function Split-PodeContentType {
    param(
        [Parameter()]
        [string]
        $ContentType
    )

    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return [string]::Empty
    }

    return @($ContentType -isplit ';')[0].Trim()
}

function ConvertFrom-PodeNameValueToHashTable {
    param(
        [Parameter()]
        [System.Collections.Specialized.NameValueCollection]
        $Collection
    )

    if ((Get-PodeCount -Object $Collection) -eq 0) {
        return @{}
    }

    $ht = @{}
    foreach ($key in $Collection.Keys) {
        $htKey = $key
        if (!$key) {
            $htKey = ''
        }

        $ht[$htKey] = $Collection.Get($key)
    }

    return $ht
}

function Get-PodeCount {
    param(
        [Parameter()]
        $Object
    )

    if ($null -eq $Object) {
        return 0
    }

    if ($Object -is [string]) {
        return $Object.Length
    }

    if ($Object -is [System.Collections.Specialized.NameValueCollection] -and $Object.Count -eq 0) {
        return 0
    }

    return $Object.Count
}

<#
.SYNOPSIS
    Tests if a given file system path is valid and optionally if it is not a directory.

.DESCRIPTION
    This function tests if the provided file system path is valid. It checks if the path is not null or whitespace, and if the item at the path exists. If the item exists and is not a directory (unless the $FailOnDirectory switch is not used), it returns true. If the path is not valid, it can optionally set a 404 response status code.

.PARAMETER Path
    The file system path to test for validity.

.PARAMETER NoStatus
    A switch to suppress setting the 404 response status code if the path is not valid.

.PARAMETER FailOnDirectory
    A switch to indicate that the function should return false if the path is a directory.

.PARAMETER Force
    A switch to indicate that the file with the hidden attribute has to be includede

.PARAMETER ReturnItem
    Return the item file item itself instead of true or false

.EXAMPLE
    $isValid = Test-PodePath -Path "C:\temp\file.txt"
    if ($isValid) {
        # The file exists and is not a directory
    }

.EXAMPLE
    $isValid = Test-PodePath -Path "C:\temp\folder" -FailOnDirectory
    if (!$isValid) {
        # The path is a directory or does not exist
    }

.NOTES
    This function is used within the Pode framework to validate file system paths for serving static content.

#>

function Test-PodePath {
    param(
        [Parameter()]
        $Path,

        [switch]
        $NoStatus,

        [switch]
        $FailOnDirectory,

        [switch]
        $Force,

        [switch]
        $ReturnItem
    )

    $statusCode = 404

    if (![string]::IsNullOrWhiteSpace($Path)) {
        try {
            $item = Get-Item $Path -Force:$Force -ErrorAction Stop
            if (($null -ne $item) -and (!$FailOnDirectory -or !$item.PSIsContainer)) {
                $statusCode = 200
            }
        }
        catch [System.Management.Automation.ItemNotFoundException] {
            $statusCode = 404
        }
        catch [System.UnauthorizedAccessException] {
            $statusCode = 401
        }
        catch {
            $statusCode = 400
        }

    }

    if ($statusCode -eq 200) {
        if ($ReturnItem.IsPresent) {
            return  $item
        }
        return $true
    }

    # if we failed to get the file, report back the status code and/or return true/false
    if (!$NoStatus.IsPresent) {
        Set-PodeResponseStatus -Code $statusCode
    }

    if ($ReturnItem.IsPresent) {
        return  $null
    }
    return $false
}

function Test-PodePathIsFile {
    param(
        [Parameter()]
        [string]
        $Path,

        [switch]
        $FailOnWildcard
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $false
    }

    if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
        return $false
    }

    return (![string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}

function Test-PodePathIsWildcard {
    param(
        [Parameter()]
        [string]
        $Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $false
    }

    return $Path.Contains('*')
}

function Test-PodePathIsDirectory {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [switch]
        $FailOnWildcard

    )

    if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
        return $false
    }

    return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}

function Convert-PodePathSeparators {
    param(
        [Parameter()]
        $Paths
    )

    return @($Paths | ForEach-Object {
            if (![string]::IsNullOrWhiteSpace($_)) {
                $_ -ireplace '[\\/]', [System.IO.Path]::DirectorySeparatorChar
            }
        })
}

function Convert-PodePathPatternToRegex {
    param(
        [Parameter()]
        [string]
        $Path,

        [switch]
        $NotSlashes,

        [switch]
        $NotStrict
    )

    if (!$NotSlashes) {
        if ($Path -match '[\\/]\*$') {
            $Path = $Path -replace '[\\/]\*$', '/{0,1}*'
        }

        $Path = $Path -ireplace '[\\/]', '[\\/]'
    }

    $Path = $Path -ireplace '\.', '\.'
    $Path = $Path -ireplace '\*', '.*?'

    if ($NotStrict) {
        return $Path
    }

    return "^$($Path)$"
}

function Convert-PodePathPatternsToRegex {
    param(
        [Parameter()]
        [string[]]
        $Paths,

        [switch]
        $NotSlashes,

        [switch]
        $NotStrict
    )

    # replace certain chars
    $Paths = @(foreach ($path in $Paths) {
            if (![string]::IsNullOrEmpty($path)) {
                Convert-PodePathPatternToRegex -Path $path -NotStrict -NotSlashes:$NotSlashes
            }
        })

    # if no paths, return null
    if (($null -eq $Paths) -or ($Paths.Length -eq 0)) {
        return $null
    }

    # join them all together
    $joined = "($($Paths -join '|'))"

    if ($NotStrict) {
        return $joined
    }

    return "^$($joined)$"
}

function Get-PodeDefaultSslProtocols {
    if (Test-PodeIsMacOS) {
        return (ConvertTo-PodeSslProtocols -Protocols Tls12)
    }

    return (ConvertTo-PodeSslProtocols -Protocols Ssl3, Tls12)
}

function ConvertTo-PodeSslProtocols {
    param(
        [Parameter()]
        [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')]
        [string[]]
        $Protocols
    )

    $protos = 0
    foreach ($protocol in $Protocols) {
        $protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$protocol)
    }

    return [System.Security.Authentication.SslProtocols]($protos)
}

function Get-PodeModuleDetails {
    # if there's 1 module imported already, use that
    $importedModule = @(Get-Module -Name Pode)
    if (($importedModule | Measure-Object).Count -eq 1) {
        return (Convert-PodeModuleDetails -Module @($importedModule)[0])
    }

    # if there's none or more, attempt to get the module used for 'engine'
    try {
        $usedModule = (Get-Command -Name 'Set-PodeViewEngine').Module
        if (($usedModule | Measure-Object).Count -eq 1) {
            return (Convert-PodeModuleDetails -Module $usedModule)
        }
    }
    catch {
    }

    # if there were multiple to begin with, use the newest version
    if (($importedModule | Measure-Object).Count -gt 1) {
        return (Convert-PodeModuleDetails -Module @($importedModule | Sort-Object -Property Version)[-1])
    }

    # otherwise there were none, use the latest installed
    return (Convert-PodeModuleDetails -Module @(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1])
}

function Convert-PodeModuleDetails {
    param(
        [Parameter(Mandatory = $true)]
        [psmoduleinfo]
        $Module
    )

    $details = @{
        Name         = $Module.Name
        Path         = $Module.Path
        BasePath     = $Module.ModuleBase
        DataPath     = (Find-PodeModuleFile -Module $Module -CheckVersion)
        InternalPath = $null
        InPath       = (Test-PodeModuleInPath -Module $Module)
    }

    $details.InternalPath = $details.DataPath -ireplace 'Pode\.(ps[md]1)', 'Pode.Internal.$1'
    return $details
}

function Test-PodeModuleInPath {
    param(
        [Parameter(Mandatory = $true)]
        [psmoduleinfo]
        $Module
    )

    $separator = ';'
    if (Test-PodeIsUnix) {
        $separator = ':'
    }

    $paths = @($env:PSModulePath -split $separator)

    foreach ($path in $paths) {
        if ($Module.Path.StartsWith($path)) {
            return $true
        }
    }

    return $false
}

function Get-PodeModuleDependencies {
    param(
        [Parameter(Mandatory = $true)]
        [psmoduleinfo]
        $Module
    )

    if (!$Module.RequiredModules) {
        return $Module
    }

    $mods = @()
    foreach ($mod in $Module.RequiredModules) {
        $mods += (Get-PodeModuleDependencies -Module $mod)
    }

    return ($mods + $module)
}

function Get-PodeModuleRootPath {
    return (Split-Path -Parent -Path $PodeContext.Server.PodeModule.Path)
}

function Get-PodeModuleMiscPath {
    return [System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Misc')
}

function Get-PodeUrl {
    return "$($WebEvent.Endpoint.Protocol)://$($WebEvent.Endpoint.Address)$($WebEvent.Path)"
}

function Find-PodeErrorPage {
    param(
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # if a defined content type is supplied, attempt to find an error page for that first
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $ContentType }
        }
    }

    # if a defined route error page content type is supplied, attempt to find an error page for that
    if (![string]::IsNullOrWhiteSpace($WebEvent.ErrorType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ErrorType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $WebEvent.ErrorType }
        }
    }

    # if route patterns have been defined, see if an error content type matches and attempt that
    if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Routes)) {
        # find type by pattern
        $matched = @(foreach ($key in $PodeContext.Server.Web.ErrorPages.Routes.Keys) {
                if ($WebEvent.Path -imatch $key) {
                    $key
                }
            })[0]

        # if we have a match, see if a page exists
        if (!(Test-PodeIsEmpty $matched)) {
            $type = $PodeContext.Server.Web.ErrorPages.Routes[$matched]
            $path = Get-PodeErrorPage -Code $Code -ContentType $type
            if (![string]::IsNullOrWhiteSpace($path)) {
                return @{ 'Path' = $path; 'ContentType' = $type }
            }
        }
    }

    # if we're using strict typing, attempt that, if we have a content type
    if ($PodeContext.Server.Web.ErrorPages.StrictContentTyping -and ![string]::IsNullOrWhiteSpace($WebEvent.ContentType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ContentType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $WebEvent.ContentType }
        }
    }

    # if we have a default defined, attempt that
    if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Default)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $PodeContext.Server.Web.ErrorPages.Default
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $PodeContext.Server.Web.ErrorPages.Default }
        }
    }

    # if there's still no error page, use default HTML logic
    $type = Get-PodeContentType -Extension 'html'
    $path = (Get-PodeErrorPage -Code $Code -ContentType $type)

    if (![string]::IsNullOrWhiteSpace($path)) {
        return @{ 'Path' = $path; 'ContentType' = $type }
    }

    return $null
}

function Get-PodeErrorPage {
    param(
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # parse the passed content type
    $ContentType = Split-PodeContentType -ContentType $ContentType

    # object for the page path
    $path = $null

    # attempt to find a custom error page
    $path = Find-PodeCustomErrorPage -Code $Code -ContentType $ContentType

    # if there's no custom page found, attempt to find an inbuilt page
    if ([string]::IsNullOrWhiteSpace($path)) {
        $podeRoot = Get-PodeModuleMiscPath
        $path = Find-PodeFileForContentType -Path $podeRoot -Name 'default-error-page' -ContentType $ContentType -Engine 'pode'
    }

    # if there's no path found, or it's inaccessible, return null
    if (!(Test-PodePath $path -NoStatus)) {
        return $null
    }

    return $path
}

function Find-PodeCustomErrorPage {
    param(
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # get the custom errors path
    $customErrPath = $PodeContext.Server.InbuiltDrives['errors']

    # if there's no custom error path, return
    if ([string]::IsNullOrWhiteSpace($customErrPath)) {
        return $null
    }

    # retrieve a status code page
    $path = (Find-PodeFileForContentType -Path $customErrPath -Name "$($Code)" -ContentType $ContentType)
    if (![string]::IsNullOrWhiteSpace($path)) {
        return $path
    }

    # retrieve default page
    $path = (Find-PodeFileForContentType -Path $customErrPath -Name 'default' -ContentType $ContentType)
    if (![string]::IsNullOrWhiteSpace($path)) {
        return $path
    }

    # no file was found
    return $null
}

function Find-PodeFileForContentType {
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $Engine = $null
    )

    # get all files at the path that start with the name
    $files = @(Get-ChildItem -Path ([System.IO.Path]::Combine($Path, "$($Name).*")))

    # if there are no files, return
    if ($null -eq $files -or $files.Length -eq 0) {
        return $null
    }

    # filter the files by the view engine extension (but only if the current engine is dynamic - non-html)
    if ([string]::IsNullOrWhiteSpace($Engine) -and $PodeContext.Server.ViewEngine.IsDynamic) {
        $Engine = $PodeContext.Server.ViewEngine.Extension
    }

    $Engine = (Protect-PodeValue -Value $Engine -Default 'pode')
    if ($Engine -ine 'pode') {
        $Engine = "($($Engine)|pode)"
    }

    $engineFiles = @(foreach ($file in $files) {
            if ($file.Name -imatch "\.$($Engine)$") {
                $file
            }
        })

    $files = @(foreach ($file in $files) {
            if ($file.Name -inotmatch "\.$($Engine)$") {
                $file
            }
        })

    # only attempt static files if we still have files after any engine filtering
    if ($null -ne $files -and $files.Length -gt 0) {
        # get files of the format '<name>.<type>'
        $file = @(foreach ($f in $files) {
                if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)$") {
                    if (($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext']))) {
                        $f.FullName
                    }
                }
            })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }
    }

    # only attempt these formats if we have a files for the view engine
    if ($null -ne $engineFiles -and $engineFiles.Length -gt 0) {
        # get files of the format '<name>.<type>.<engine>'
        $file = @(foreach ($f in $engineFiles) {
                if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)\.$($engine)$") {
                    if ($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext'])) {
                        $f.FullName
                    }
                }
            })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }

        # get files of the format '<name>.<engine>'
        $file = @(foreach ($f in $engineFiles) {
                if ($f.Name -imatch "^$($Name)\.$($engine)$") {
                    $f.FullName
                }
            })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }
    }

    # no file was found
    return $null
}

function Get-PodeRelativePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $RootPath,

        [switch]
        $JoinRoot,

        [switch]
        $Resolve,

        [switch]
        $TestPath
    )

    # if the path is relative, join to root if flagged
    if ($JoinRoot -and ($Path -match '^\.{1,2}([\\\/]|$)')) {
        if ([string]::IsNullOrWhiteSpace($RootPath)) {
            $RootPath = $PodeContext.Server.Root
        }

        $Path = [System.IO.Path]::Combine($RootPath, $Path)
    }

    # if flagged, resolve the path
    if ($Resolve) {
        $_rawPath = $Path
        $Path = [System.IO.Path]::GetFullPath($Path.Replace('\', '/'))
    }

    # if flagged, test the path and throw error if it doesn't exist
    if ($TestPath -and !(Test-PodePath $Path -NoStatus)) {
        throw "The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)"
    }

    return $Path
}

function Get-PodeWildcardFiles {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Wildcard = '*.*',

        [Parameter()]
        [string]
        $RootPath
    )

    # if the OriginalPath is a directory, add wildcard
    if (Test-PodePathIsDirectory -Path $Path) {
        $Path = [System.IO.Path]::Combine($Path, $Wildcard)
    }

    # if path has a *, assume wildcard
    if (Test-PodePathIsWildcard -Path $Path) {
        $Path = Get-PodeRelativePath -Path $Path -RootPath $RootPath -JoinRoot
        return @((Get-ChildItem $Path -Recurse -Force).FullName)
    }

    return $null
}

function Test-PodeIsServerless {
    param(
        [Parameter()]
        [string]
        $FunctionName,

        [switch]
        $ThrowError
    )

    if ($PodeContext.Server.IsServerless -and $ThrowError) {
        throw "The $($FunctionName) function is not supported in a serverless context"
    }

    if (!$ThrowError) {
        return $PodeContext.Server.IsServerless
    }
}

function Get-PodeEndpointUrl {
    param(
        [Parameter()]
        $Endpoint
    )

    # get the endpoint on which we're currently listening - use first http/https if there are many
    if ($null -eq $Endpoint) {
        $Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') -and $_.Default })[0]
        if ($null -eq $Endpoint) {
            $Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') })[0]
        }
    }

    $url = $Endpoint.Url
    if ([string]::IsNullOrWhiteSpace($url)) {
        $url = "$($Endpoint.Protocol)://$($Endpoint.FriendlyName):$($Endpoint.Port)"
    }

    return $url
}

function Get-PodeDefaultPort {
    param(
        [Parameter()]
        [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
        [string]
        $Protocol,

        [Parameter()]
        [ValidateSet('Implicit', 'Explicit')]
        [string]
        $TlsMode = 'Implicit',

        [switch]
        $Real
    )

    # are we after the real default ports?
    if ($Real) {
        return (@{
                Http  = @{ Implicit = 80 }
                Https = @{ Implicit = 443 }
                Smtp  = @{ Implicit = 25 }
                Smtps = @{ Implicit = 465; Explicit = 587 }
                Tcp   = @{ Implicit = 9001 }
                Tcps  = @{ Implicit = 9002; Explicit = 9003 }
                Ws    = @{ Implicit = 80 }
                Wss   = @{ Implicit = 443 }
            })[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()]
    }

    # if we running as iis, return the ASPNET port
    if ($PodeContext.Server.IsIIS) {
        return [int]$env:ASPNETCORE_PORT
    }

    # if we running as heroku, return the port
    if ($PodeContext.Server.IsHeroku) {
        return [int]$env:PORT
    }

    # otherwise, get the port for the protocol
    return (@{
            Http  = @{ Implicit = 8080 }
            Https = @{ Implicit = 8443 }
            Smtp  = @{ Implicit = 25 }
            Smtps = @{ Implicit = 465; Explicit = 587 }
            Tcp   = @{ Implicit = 9001 }
            Tcps  = @{ Implicit = 9002; Explicit = 9003 }
            Ws    = @{ Implicit = 9080 }
            Wss   = @{ Implicit = 9443 }
        })[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()]
}

function Set-PodeServerHeader {
    param(
        [Parameter()]
        [string]
        $Type,

        [switch]
        $AllowEmptyType
    )

    $name = 'Pode'
    if (![string]::IsNullOrWhiteSpace($Type) -or $AllowEmptyType) {
        $name += " - $($Type)"
    }

    Set-PodeHeader -Name 'Server' -Value $name
}

function Get-PodeHandler {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Service', 'Smtp')]
        [string]
        $Type,

        [Parameter()]
        [string]
        $Name
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $PodeContext.Server.Handlers[$Type]
    }

    return $PodeContext.Server.Handlers[$Type][$Name]
}

function Convert-PodeFileToScriptBlock {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $FilePath
    )

    # resolve for relative path
    $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot

    # if file doesn't exist, error
    if (!(Test-PodePath -Path $FilePath -NoStatus)) {
        throw "The FilePath supplied does not exist: $($FilePath)"
    }

    # if the path is a wildcard or directory, error
    if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) {
        throw "The FilePath supplied cannot be a wildcard or a directory: $($FilePath)"
    }

    return ([scriptblock](Use-PodeScript -Path $FilePath))
}

function Convert-PodeQueryStringToHashTable {
    param(
        [Parameter()]
        [string]
        $Uri
    )

    if ([string]::IsNullOrWhiteSpace($Uri)) {
        return @{}
    }

    $qmIndex = $Uri.IndexOf('?')
    if ($qmIndex -eq -1) {
        return @{}
    }

    if ($qmIndex -gt 0) {
        $Uri = $Uri.Substring($qmIndex)
    }

    $tmpQuery = [System.Web.HttpUtility]::ParseQueryString($Uri)
    return (ConvertFrom-PodeNameValueToHashTable -Collection $tmpQuery)
}

function Get-PodeDotSourcedFiles {
    param(
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Language.Ast]
        $Ast,

        [Parameter()]
        [string]
        $RootPath
    )

    # set default root path
    if ([string]::IsNullOrWhiteSpace($RootPath)) {
        $RootPath = $PodeContext.Server.Root
    }

    # get all dot-sourced files
    $cmdTypes = @('dot', 'ampersand')
    $files = ($Ast.FindAll({
        ($args[0] -is [System.Management.Automation.Language.CommandAst]) -and
        ($args[0].InvocationOperator -iin $cmdTypes) -and
        ($args[0].CommandElements.StaticType.Name -ieq 'string')
            }, $false)).CommandElements.Value

    $fileOrder = @()

    # no files found
    if (($null -eq $files) -or ($files.Length -eq 0)) {
        return $fileOrder
    }

    # get any sub sourced files
    foreach ($file in $files) {
        $file = Get-PodeRelativePath -Path $file -RootPath $RootPath -JoinRoot
        $fileOrder += $file

        $ast = Get-PodeAstFromFile -FilePath $file

        $result = Get-PodeDotSourcedFiles -Ast $ast -RootPath (Split-Path -Parent -Path $file)
        if (($null -ne $result) -and ($result.Length -gt 0)) {
            $fileOrder += $result
        }
    }

    # return all found files
    return $fileOrder
}

function Get-PodeAstFromFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $FilePath
    )

    if (!(Test-Path $FilePath)) {
        throw "Path to script file does not exist: $($FilePath)"
    }

    return [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null)
}

function Get-PodeFunctionsFromFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $FilePath
    )

    $ast = Get-PodeAstFromFile -FilePath $FilePath
    return @(Get-PodeFunctionsFromAst -Ast $ast)
}

function Get-PodeFunctionsFromAst {
    param(
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Language.Ast]
        $Ast
    )

    $funcs = @(($Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)))

    return @(foreach ($func in $funcs) {
            # skip null
            if ($null -eq $func) {
                continue
            }

            # skip pode funcs
            if ($func.Name -ilike '*-Pode*') {
                continue
            }

            # definition
            $def = "$($func.Body)".Trim('{}').Trim()
            if (($null -ne $func.Parameters) -and ($func.Parameters.Count -gt 0)) {
                $def = "param($($func.Parameters.Name -join ','))`n$($def)"
            }

            # the found func
            @{
                Name       = $func.Name
                Definition = $def
            }
        })
}

function Get-PodeFunctionsFromScriptBlock {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )

    # functions that have been found
    $foundFuncs = @()

    # get each function in the callstack
    $callstack = Get-PSCallStack
    if ($callstack.Count -gt 3) {
        $callstack = ($callstack | Select-Object -Skip 4)
        $bindingFlags = [System.Reflection.BindingFlags]'NonPublic, Instance, Static'

        foreach ($call in $callstack) {
            $_funcContext = $call.GetType().GetProperty('FunctionContext', $bindingFlags).GetValue($call, $null)
            $_scriptBlock = $_funcContext.GetType().GetField('_scriptBlock', $bindingFlags).GetValue($_funcContext)
            $foundFuncs += @(Get-PodeFunctionsFromAst -Ast $_scriptBlock.Ast)
        }
    }

    # get each function from the main script
    $foundFuncs += @(Get-PodeFunctionsFromAst -Ast $ScriptBlock.Ast)

    # return the found functions
    return $foundFuncs
}

function Read-PodeWebExceptionDetails {
    param(
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    switch ($ErrorRecord) {
        { $_.Exception -is [System.Net.WebException] } {
            $stream = $_.Exception.Response.GetResponseStream()
            $stream.Position = 0

            $body = [System.IO.StreamReader]::new($stream).ReadToEnd()
            $code = [int]$_.Exception.Response.StatusCode
            $desc = $_.Exception.Response.StatusDescription
        }

        { $_.Exception -is [System.Net.Http.HttpRequestException] } {
            $body = $_.ErrorDetails.Message
            $code = [int]$_.Exception.Response.StatusCode
            $desc = $_.Exception.Response.ReasonPhrase
        }

        default {
            throw "Exception is of an invalid type, should be either WebException or HttpRequestException, but got: $($_.Exception.GetType().Name)"
        }
    }

    return @{
        Status = @{
            Code        = $code
            Description = $desc
        }
        Body   = $body
    }
}

function Use-PodeFolder {
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [string]
        $DefaultPath
    )

    # use default, or custom path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        $Path = Join-PodeServerRoot -Folder $DefaultPath
    }
    else {
        $Path = Get-PodeRelativePath -Path $Path -JoinRoot
    }

    # fail if path not found
    if (!(Test-PodePath -Path $Path -NoStatus)) {
        throw "Path to load $($DefaultPath) not found: $($Path)"
    }

    # get .ps1 files and load them
    Get-ChildItem -Path $Path -Filter *.ps1 -Force -Recurse | ForEach-Object {
        Use-PodeScript -Path $_.FullName
    }
}

function Find-PodeModuleFile {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Module')]
        [psmoduleinfo]
        $Module,

        [switch]
        $ListAvailable,

        [switch]
        $DataOnly,

        [switch]
        $CheckVersion
    )

    # get module and check psd1, then psm1
    if ($null -eq $Module) {
        $Module = (Get-Module -Name $Name -ListAvailable:$ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1)
    }

    # if the path isn't already a psd1 do this
    $path = Join-Path $Module.ModuleBase "$($Module.Name).psd1"
    if (!(Test-Path $path)) {
        # if we only want a psd1, return null
        if ($DataOnly) {
            $path = $null
        }
        else {
            $path = $Module.Path
        }
    }

    # check the Version of the psd1
    elseif ($CheckVersion) {
        $data = Import-PowerShellDataFile -Path $path -ErrorAction Stop

        $version = $null
        if (![version]::TryParse($data.ModuleVersion, [ref]$version)) {
            if ($DataOnly) {
                $path = $null
            }
            else {
                $path = $Module.Path
            }
        }
    }

    return $path
}

function Clear-PodeHashtableInnerKeys {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $InputObject
    )

    if (Test-PodeIsEmpty $InputObject) {
        return
    }

    $InputObject.Keys.Clone() | ForEach-Object {
        $InputObject[$_].Clear()
    }
}

function Set-PodeCronInterval {
    param(
        [Parameter()]
        [hashtable]
        $Cron,

        [Parameter()]
        [string]
        $Type,

        [Parameter()]
        [int[]]
        $Value,

        [Parameter()]
        [int]
        $Interval
    )

    if ($Interval -le 0) {
        return $false
    }

    if ($Value.Length -gt 1) {
        throw "You can only supply a single $($Type) value when using intervals"
    }

    if ($Value.Length -eq 1) {
        $Cron[$Type] = "$(@($Value)[0])"
    }

    $Cron[$Type] += "/$($Interval)"
    return ($Value.Length -eq 1)
}

function Test-PodeModuleInstalled {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return ($null -ne (Get-Module -Name $Name -ListAvailable -ErrorAction Ignore -Verbose:$false))
}

function Get-PodePlaceholderRegex {
    return '\:(?<tag>[\w]+)'
}

function Resolve-PodePlaceholders {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [string]
        $Prepend = '(?<',

        [Parameter()]
        [string]
        $Append = '>[^\/]+?)',

        [switch]
        $Slashes
    )

    if ([string]::IsNullOrWhiteSpace($Pattern)) {
        $Pattern = Get-PodePlaceholderRegex
    }

    if ($Path -imatch $Pattern) {
        $Path = [regex]::Escape($Path)
    }

    if ($Slashes) {
        $Path = ($Path.TrimEnd('\/') -replace '(\\\\|\/)', '[\\\/]')
        $Path = "$($Path)[\\\/]"
    }

    return (Convert-PodePlaceholders -Path $Path -Pattern $Pattern -Prepend $Prepend -Append $Append)
}

function Convert-PodePlaceholders {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [string]
        $Prepend = '(?<',

        [Parameter()]
        [string]
        $Append = '>[^\/]+?)'
    )

    if ([string]::IsNullOrWhiteSpace($Pattern)) {
        $Pattern = Get-PodePlaceholderRegex
    }

    while ($Path -imatch $Pattern) {
        $Path = ($Path -ireplace $Matches[0], "$($Prepend)$($Matches['tag'])$($Append)")
    }

    return $Path
}

function Test-PodePlaceholders {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Placeholder
    )

    if ([string]::IsNullOrWhiteSpace($Placeholder)) {
        $Placeholder = Get-PodePlaceholderRegex
    }

    return ($Path -imatch $Placeholder)
}


<#
.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 if the Pode module is from the development branch.

.DESCRIPTION
The Test-PodeVersionDev function checks if the Pode module's version matches the placeholder value ('$version$'), which is used to indicate the development branch of the module. It returns $true if the version matches, indicating the module is from the development branch, and $false otherwise.

.PARAMETER None
This function does not accept any parameters.

.OUTPUTS
System.Boolean
Returns $true if the Pode module version is '$version$', indicating the development branch. Returns $false for any other version.

.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '$version$' }
PS> Test-PodeVersionDev

Returns $true, indicating the development branch.

.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '1.2.3' }
PS> Test-PodeVersionDev

Returns $false, indicating a specific release version.

.NOTES
This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion.

#>
function Test-PodeVersionDev {
    return (Get-PodeModuleManifest).ModuleVersion -eq '$version$'
}

<#
.SYNOPSIS
Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions.

.DESCRIPTION
The `Test-PodeVersionPwshEOL` function checks the current PowerShell version against a list of versions that were either supported or EOL at the time of the Pode release. It uses the module manifest to determine which PowerShell versions are considered EOL and which are officially supported. If the current version is EOL or was not tested with the current release of Pode, the function generates a warning. This function aids in maintaining best practices for using supported PowerShell versions with Pode.

.PARAMETER ReportUntested
If specified, the function will report if the current PowerShell version was not available and thus untested at the time of the Pode release. This is useful for identifying potential compatibility issues with newer versions of PowerShell.

.OUTPUTS
A hashtable containing two keys:
- `eol`: A boolean indicating if the current PowerShell version was EOL at the time of the Pode release.
- `supported`: A boolean indicating if the current PowerShell version was officially supported by Pode at the time of the release.

.EXAMPLE
Test-PodeVersionPwshEOL

Checks the current PowerShell version against Pode's supported and EOL versions list. Outputs a warning if the version is EOL or untested, and returns a hashtable indicating the compatibility status.

.EXAMPLE
Test-PodeVersionPwshEOL -ReportUntested

Similar to the basic usage, but also reports if the current PowerShell version was untested because it was not available at the time of the Pode release.

.NOTES
This function is part of the Pode module's utilities to ensure compatibility and encourage the use of supported PowerShell versions.

#>
function Test-PodeVersionPwshEOL {
    param(
        [switch] $ReportUntested
    )
    $moduleManifest = Get-PodeModuleManifest
    if ($moduleManifest.ModuleVersion -eq '$version$') {
        return @{
            eol       = $false
            supported = $true
        }
    }

    $psVersion = $PSVersionTable.PSVersion
    $eolVersions = $moduleManifest.PrivateData.PwshVersions.Untested -split ','
    $isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions

    if ($isEol) {
        Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it is EOL." -ForegroundColor Yellow
    }

    $SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ','
    $isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions

    if ((! $isSupported) -and (! $isEol) -and $ReportUntested) {
        Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it was not available when Pode was released." -ForegroundColor Yellow
    }

    return @{
        eol       = $isEol
        supported = $isSupported
    }
}


<#
.SYNOPSIS
creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml
.DESCRIPTION
This produces YAML from any object you pass to it. It isn't suitable for the huge objects produced by some of the cmdlets such as Get-Process, but fine for simple objects
.PARAMETER Object
the object that you want scripted out
.PARAMETER Depth
The depth that you want your object scripted to
.EXAMPLE
Get-PodeOpenApiDefinition|ConvertTo-PodeYaml
#>
function ConvertTo-PodeYaml {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [AllowNull()]
        $InputObject,

        [parameter()]
        [int]
        $Depth = 16
    )

    if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) {
        $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml'))
    }

    if ($PodeContext.Server.InternalCache.YamlModuleImported) {
        return ($InputObject | ConvertTo-Yaml)
    }
    else {
        return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine
    }
}

<#
.SYNOPSIS
  Converts PowerShell objects into a YAML-formatted string.

.DESCRIPTION
  This function takes PowerShell objects and converts them to a YAML string representation.
  It supports various data types including arrays, hashtables, strings, and more.
  The depth of conversion can be controlled, allowing for nested objects to be accurately represented.

.PARAMETER InputObject
  The PowerShell object to convert to YAML. This parameter accepts input via the pipeline.

.PARAMETER Depth
  Specifies the maximum depth of object nesting to convert. Default is 10 levels deep.

.PARAMETER NestingLevel
  Used internally to track the current depth of recursion. Generally not specified by the user.

.PARAMETER NoNewLine
  If specified, suppresses the newline characters in the output to create a single-line string.

.OUTPUTS
  System.String. Returns a string in YAML format.

.EXAMPLE
  $object | ConvertTo-PodeYamlInternal

  Converts the object piped to it into a YAML string.

.NOTES
  This is an internal function and may change in future releases of Pode.
  It converts only basic PowerShell types, such as strings, integers, booleans, arrays, hashtables, and ordered dictionaries into a YAML format.

#>

function ConvertTo-PodeYamlInternal {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowNull()]
        $InputObject,

        [parameter()]
        [int]
        $Depth = 10,

        [parameter()]
        [int]
        $NestingLevel = 0,

        [parameter()]
        [switch]
        $NoNewLine
    )

    process {
        # if it is null return null
        If ( !($InputObject) ) {
            if ($InputObject -is [Object[]]) {
                return '[]'
            }
            else {
                return ''
            }
        }

        $padding = [string]::new(' ', $NestingLevel * 2) # lets just create our left-padding for the block
        try {
            $Type = $InputObject.GetType().Name # we start by getting the object's type
            if ($InputObject -is [object[]]) {
                #what it really is
                $Type = "$($InputObject.GetType().BaseType.Name)"
            }

            #report the leaves in terms of object type
            if ($Depth -ilt $NestingLevel) {
                $Type = 'OutOfDepth'
            }
            # prevent these values being identified as an object
            if ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) {
                $Type = 'HashTable'
            }
            elseif ($Type -ieq 'List`1') {
                $Type = 'Array'
            }
            elseif ($InputObject -is [array]) {
                $Type = 'Array'
            } # whatever it thinks it is called
            elseif ($InputObject -is [hashtable]) {
                $Type = 'HashTable'
            } # for our purposes it is a hashtable

            $output += switch ($Type.ToLower()) {
                'string' {
                    $String = "$InputObject"
                    if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) {
                        $multiline = [System.Text.StringBuilder]::new("|`n")

                        $items = $string.Split("`n")
                        for ($i = 0; $i -lt $items.Length; $i++) {
                            $workingString = $items[$i] -replace '\r$'
                            $length = $workingString.Length
                            $index = 0
                            $wrap = 80

                            while ($index -lt $length) {
                                $breakpoint = $wrap
                                $linebreak = $false

                                if (($length - $index) -gt $wrap) {
                                    $lastSpaceIndex = $workingString.LastIndexOf(' ', $index + $wrap, $wrap)
                                    if ($lastSpaceIndex -ne -1) {
                                        $breakpoint = $lastSpaceIndex - $index
                                    }
                                    else {
                                        $linebreak = $true
                                        $breakpoint--
                                    }
                                }
                                else {
                                    $breakpoint = $length - $index
                                }

                                $null = $multiline.Append($padding).Append($workingString.Substring($index, $breakpoint).Trim())
                                if ($linebreak) {
                                    $null = $multiline.Append('\')
                                }

                                $index += $breakpoint
                                if ($index -lt $length) {
                                    $null = $multiline.Append([System.Environment]::NewLine)
                                }
                            }

                            if ($i -lt ($items.Length - 1)) {
                                $null = $multiline.Append([System.Environment]::NewLine)
                            }
                        }

                        $multiline.ToString().TrimEnd()
                        break
                    }
                    else {
                        if ($string -match '^[#\[\]@\{\}\!\*]') {
                            "'$($string -replace '''', '''''')'"
                        }
                        else {
                            $string
                        }
                        break
                    }
                    break
                }
                'hashtable' {
                    if ($InputObject.Count -gt 0 ) {
                        $index = 0
                        $string = [System.Text.StringBuilder]::new()
                        foreach ($item in $InputObject.Keys) {
                            if ($InputObject[$item] -is [string]) { $increment = 2 } else { $increment = 1 }
                            if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" }
                            $null = $string.Append( $NewPadding).Append( $item).Append(': ').Append((ConvertTo-PodeYamlInternal -InputObject $InputObject[$item] -Depth $Depth -NestingLevel ($NestingLevel + $increment)))
                        }
                        $string.ToString()
                    }
                    else { '{}' }
                    break
                }
                'boolean' {
                    if ($InputObject -eq $true) { 'true' } else { 'false' }
                    break
                }
                'array' {
                    $string = [System.Text.StringBuilder]::new()
                    $index = 0
                    foreach ($item in $InputObject ) {
                        if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" }
                        $null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine))
                    }
                    $string.ToString()
                    break
                }
                'int32' {
                    $InputObject
                }
                'double' {
                    $InputObject
                }
                default {
                    "'$InputObject'"
                }
            }
            return $Output
        }
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) "
        }
    }
}
src\Private\Logging.ps1
function Get-PodeLoggingTerminalMethod {
    return {
        param($item, $options)

        if ($PodeContext.Server.Quiet) {
            return
        }

        # check if it's an array from batching
        if ($item -is [array]) {
            $item = ($item -join [System.Environment]::NewLine)
        }

        # protect then write
        $item = ($item | Protect-PodeLogItem)
        $item.ToString() | Out-PodeHost
    }
}

function Get-PodeLoggingFileMethod {
    return {
        param($item, $options)

        # check if it's an array from batching
        if ($item -is [array]) {
            $item = ($item -join [System.Environment]::NewLine)
        }

        # mask values
        $item = ($item | Protect-PodeLogItem)

        # variables
        $date = [DateTime]::Now.ToString('yyyy-MM-dd')

        # do we need to reset the fileId?
        if ($options.Date -ine $date) {
            $options.Date = $date
            $options.FileId = 0
        }

        # get the fileId
        if ($options.FileId -eq 0) {
            $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_*.log")
            $options.FileId = (@(Get-ChildItem -Path $path)).Length
            if ($options.FileId -eq 0) {
                $options.FileId = 1
            }
        }

        $id = "$($options.FileId)".PadLeft(3, '0')
        if ($options.MaxSize -gt 0) {
            $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log")
            if ((Get-Item -Path $path -Force).Length -ge $options.MaxSize) {
                $options.FileId++
                $id = "$($options.FileId)".PadLeft(3, '0')
            }
        }

        # get the file to write to
        $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log")

        # write the item to the file
        $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)) {
            $date = [DateTime]::Now.Date.AddDays(-$options.MaxDays)

            $null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force |
                Where-Object { $_.CreationTime -lt $date } |
                Remove-Item $_ -Force

            $options.NextClearDown = [DateTime]::Now.Date.AddDays(1)
        }
    }
}

function Get-PodeLoggingEventViewerMethod {
    return {
        param($item, $options, $rawItem)

        if ($item -isnot [array]) {
            $item = @($item)
        }

        if ($rawItem -isnot [array]) {
            $rawItem = @($rawItem)
        }

        for ($i = 0; $i -lt $item.Length; $i++) {
            # convert log level - info if no level present
            $entryType = ConvertTo-PodeEventViewerLevel -Level $rawItem[$i].Level

            # create log instance
            $entryInstance = [System.Diagnostics.EventInstance]::new($options.ID, 0, $entryType)

            # create event log
            $entryLog = [System.Diagnostics.EventLog]::new()
            $entryLog.Log = $options.LogName
            $entryLog.Source = $options.Source

            try {
                $message = ($item[$i] | Protect-PodeLogItem)
                $entryLog.WriteEvent($entryInstance, $message)
            }
            catch {
            }
        }
    }
}

function ConvertTo-PodeEventViewerLevel {
    param(
        [Parameter()]
        [string]
        $Level
    )

    if ([string]::IsNullOrWhiteSpace($Level)) {
        return [System.Diagnostics.EventLogEntryType]::Information
    }

    if ($Level -ieq 'error') {
        return [System.Diagnostics.EventLogEntryType]::Error
    }

    if ($Level -ieq 'warning') {
        return [System.Diagnostics.EventLogEntryType]::Warning
    }

    return [System.Diagnostics.EventLogEntryType]::Information
}

function Get-PodeLoggingInbuiltType {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Errors', 'Requests')]
        [string]
        $Type
    )

    switch ($Type.ToLowerInvariant()) {
        'requests' {
            $script = {
                param($item, $options)

                # just return the item if Raw is set
                if ($options.Raw) {
                    return $item
                }

                function sg($value) {
                    if ([string]::IsNullOrWhiteSpace($value)) {
                        return '-'
                    }

                    return $value
                }

                # build the url with http method
                $url = "$(sg $item.Request.Method) $(sg $item.Request.Resource) $(sg $item.Request.Protocol)"

                # build and return the request row
                return "$(sg $item.Host) $(sg $item.RfcUserIdentity) $(sg $item.User) [$(sg $item.Date)] `"$($url)`" $(sg $item.Response.StatusCode) $(sg $item.Response.Size) `"$(sg $item.Request.Referrer)`" `"$(sg $item.Request.Agent)`""
            }
        }

        'errors' {
            $script = {
                param($item, $options)

                # do nothing if the error level isn't present
                if (@($options.Levels) -inotcontains $item.Level) {
                    return
                }

                # just return the item if Raw is set
                if ($options.Raw) {
                    return $item
                }

                # build the exception details
                $row = @(
                    "Date: $($item.Date.ToString('yyyy-MM-dd HH:mm:ss'))",
                    "Level: $($item.Level)",
                    "ThreadId: $($item.ThreadId)",
                    "Server: $($item.Server)",
                    "Category: $($item.Category)",
                    "Message: $($item.Message)",
                    "StackTrace: $($item.StackTrace)"
                )

                # join the details and return
                return "$($row -join "`n")`n"
            }
        }
    }

    return $script
}

function Get-PodeRequestLoggingName {
    return '__pode_log_requests__'
}

function Get-PodeErrorLoggingName {
    return '__pode_log_errors__'
}

function Get-PodeLogger {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Logging.Types[$Name]
}

function Test-PodeLoggerEnabled {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return ($PodeContext.Server.Logging.Enabled -and $PodeContext.Server.Logging.Types.ContainsKey($Name))
}

function Get-PodeErrorLoggingLevels {
    return (Get-PodeLogger -Name (Get-PodeErrorLoggingName)).Arguments.Levels
}

function Test-PodeErrorLoggingEnabled {
    return (Test-PodeLoggerEnabled -Name (Get-PodeErrorLoggingName))
}

function Test-PodeRequestLoggingEnabled {
    return (Test-PodeLoggerEnabled -Name (Get-PodeRequestLoggingName))
}

function Write-PodeRequestLog {
    param(
        [Parameter(Mandatory = $true)]
        $Request,

        [Parameter(Mandatory = $true)]
        $Response,

        [Parameter()]
        [string]
        $Path
    )

    # do nothing if logging is disabled, or request logging isn't setup
    $name = Get-PodeRequestLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # build a request object
    $item = @{
        Host            = $Request.RemoteEndPoint.Address.IPAddressToString
        RfcUserIdentity = '-'
        User            = '-'
        Date            = [DateTime]::Now.ToString('dd/MMM/yyyy:HH:mm:ss zzz')
        Request         = @{
            Method   = $Request.HttpMethod.ToUpperInvariant()
            Resource = $Path
            Protocol = "HTTP/$($Request.ProtocolVersion)"
            Referrer = $Request.UrlReferrer
            Agent    = $Request.UserAgent
        }
        Response        = @{
            StatusCode        = $Response.StatusCode
            StatusDescription = $Response.StatusDescription
            Size              = '-'
        }
    }

    # set size if >0
    if ($Response.ContentLength64 -gt 0) {
        $item.Response.Size = $Response.ContentLength64
    }

    # set username - dot spaces
    if (Test-PodeAuthUser -IgnoreSession) {
        $userProps = (Get-PodeLogger -Name $name).Properties.Username.Split('.')

        $user = $WebEvent.Auth.User
        foreach ($atom in $userProps) {
            $user = $user.($atom)
        }

        if (![string]::IsNullOrWhiteSpace($user)) {
            $item.User = $user -ireplace '\s+', '.'
        }
    }

    # add the item to be processed
    $null = $PodeContext.LogsToProcess.Add(@{
            Name = $name
            Item = $item
        })
}

function Add-PodeRequestLogEndware {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $WebEvent
    )

    # do nothing if logging is disabled, or request logging isn't setup
    $name = Get-PodeRequestLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # add the request logging endware
    $WebEvent.OnEnd += @{
        Logic = {
            Write-PodeRequestLog -Request $WebEvent.Request -Response $WebEvent.Response -Path $WebEvent.Path
        }
    }
}

function Test-PodeLoggersExist {
    if (($null -eq $PodeContext.Server.Logging) -or ($null -eq $PodeContext.Server.Logging.Types)) {
        return $false
    }

    return (($PodeContext.Server.Logging.Types.Count -gt 0) -or ($PodeContext.Server.Logging.Enabled))
}

function Start-PodeLoggingRunspace {
    # skip if there are no loggers configured, or logging is disabled
    if (!(Test-PodeLoggersExist)) {
        return
    }

    $script = {
        while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
            # if there are no logs to process, just sleep for a few seconds - but after checking the batch
            if ($PodeContext.LogsToProcess.Count -eq 0) {
                Test-PodeLoggerBatches
                Start-Sleep -Seconds 5
                continue
            }

            # safely pop off the first log from the array
            $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock {
                    $log = $PodeContext.LogsToProcess[0]
                    $null = $PodeContext.LogsToProcess.RemoveAt(0)
                    return $log
                })

            # run the log item through the appropriate method
            $logger = Get-PodeLogger -Name $log.Name
            $now = [datetime]::Now

            # if the log is null, check batch then sleep and skip
            if ($null -eq $log) {
                Start-Sleep -Milliseconds 100
                continue
            }

            # convert to log item into a writable format
            $rawItems = $log.Item
            $_args = @($log.Item) + @($logger.Arguments)
            $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat)

            # check batching
            $batch = $logger.Method.Batch
            if ($batch.Size -gt 1) {
                # add current item to batch
                $batch.Items += $result
                $batch.RawItems += $log.Item
                $batch.LastUpdate = $now

                # if the current amount of items matches the batch, write
                $result = $null
                if ($batch.Items.Length -ge $batch.Size) {
                    $result = $batch.Items
                    $rawItems = $batch.RawItems
                }

                # if we're writing, reset the items
                if ($null -ne $result) {
                    $batch.Items = @()
                    $batch.RawItems = @()
                }
            }

            # send the writable log item off to the log writer
            if ($null -ne $result) {
                $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems)
                $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat
            }

            # small sleep to lower cpu usage
            Start-Sleep -Milliseconds 100
        }
    }

    Add-PodeRunspace -Type Main -ScriptBlock $script
}

function Test-PodeLoggerBatches {
    $now = [datetime]::Now

    # check each logger, and see if its batch needs to be written
    foreach ($logger in $PodeContext.Server.Logging.Types.Values) {
        $batch = $logger.Method.Batch
        if (($batch.Size -gt 1) -and ($batch.Items.Length -gt 0) -and ($batch.Timeout -gt 0) -and ($null -ne $batch.LastUpdate) -and ($batch.LastUpdate.AddSeconds($batch.Timeout) -le $now)) {
            $result = $batch.Items
            $rawItems = $batch.RawItems

            $batch.Items = @()
            $batch.RawItems = @()

            $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems)
            $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat
        }
    }
}
src\Private\Mappers.ps1
function Get-PodeContentType {
    param(
        [Parameter()]
        [string]
        $Extension,

        [switch]
        $DefaultIsNull
    )

    if ([string]::IsNullOrWhiteSpace($Extension)) {
        $Extension = [string]::Empty
    }

    if (!$Extension.StartsWith('.')) {
        $Extension = ".$($Extension)"
    }

    # Sourced from https://github.com/samuelneff/MimeTypeMap
    switch ($Extension.ToLowerInvariant()) {
        '.323' { return 'text/h323' }
        '.3g2' { return 'video/3gpp2' }
        '.3gp' { return 'video/3gpp' }
        '.3gp2' { return 'video/3gpp2' }
        '.3gpp' { return 'video/3gpp' }
        '.7z' { return 'application/x-7z-compressed' }
        '.aa' { return 'audio/audible' }
        '.aac' { return 'audio/aac' }
        '.aaf' { return 'application/octet-stream' }
        '.aax' { return 'audio/vnd.audible.aax' }
        '.ac3' { return 'audio/ac3' }
        '.aca' { return 'application/octet-stream' }
        '.accda' { return 'application/msaccess.addin' }
        '.accdb' { return 'application/msaccess' }
        '.accdc' { return 'application/msaccess.cab' }
        '.accde' { return 'application/msaccess' }
        '.accdr' { return 'application/msaccess.runtime' }
        '.accdt' { return 'application/msaccess' }
        '.accdw' { return 'application/msaccess.webapplication' }
        '.accft' { return 'application/msaccess.ftemplate' }
        '.acx' { return 'application/internet-property-stream' }
        '.addin' { return 'text/xml' }
        '.ade' { return 'application/msaccess' }
        '.adobebridge' { return 'application/x-bridge-url' }
        '.adp' { return 'application/msaccess' }
        '.adt' { return 'audio/vnd.dlna.adts' }
        '.adts' { return 'audio/aac' }
        '.afm' { return 'application/octet-stream' }
        '.ai' { return 'application/postscript' }
        '.aif' { return 'audio/aiff' }
        '.aifc' { return 'audio/aiff' }
        '.aiff' { return 'audio/aiff' }
        '.air' { return 'application/vnd.adobe.air-application-installer-package+zip' }
        '.amc' { return 'application/mpeg' }
        '.anx' { return 'application/annodex' }
        '.apk' { return 'application/vnd.android.package-archive' }
        '.application' { return 'application/x-ms-application' }
        '.art' { return 'image/x-jg' }
        '.asa' { return 'application/xml' }
        '.asax' { return 'application/xml' }
        '.ascx' { return 'application/xml' }
        '.asd' { return 'application/octet-stream' }
        '.asf' { return 'video/x-ms-asf' }
        '.ashx' { return 'application/xml' }
        '.asi' { return 'application/octet-stream' }
        '.asm' { return 'text/plain' }
        '.asmx' { return 'application/xml' }
        '.aspx' { return 'application/xml' }
        '.asr' { return 'video/x-ms-asf' }
        '.asx' { return 'video/x-ms-asf' }
        '.atom' { return 'application/atom+xml' }
        '.au' { return 'audio/basic' }
        '.avi' { return 'video/x-msvideo' }
        '.axa' { return 'audio/annodex' }
        '.axs' { return 'application/olescript' }
        '.axv' { return 'video/annodex' }
        '.bas' { return 'text/plain' }
        '.bcpio' { return 'application/x-bcpio' }
        '.bin' { return 'application/octet-stream' }
        '.bmp' { return 'image/bmp' }
        '.c' { return 'text/plain' }
        '.cab' { return 'application/octet-stream' }
        '.caf' { return 'audio/x-caf' }
        '.calx' { return 'application/vnd.ms-office.calx' }
        '.cat' { return 'application/vnd.ms-pki.seccat' }
        '.cc' { return 'text/plain' }
        '.cd' { return 'text/plain' }
        '.cdda' { return 'audio/aiff' }
        '.cdf' { return 'application/x-cdf' }
        '.cer' { return 'application/x-x509-ca-cert' }
        '.cfg' { return 'text/plain' }
        '.chm' { return 'application/octet-stream' }
        '.class' { return 'application/x-java-applet' }
        '.clp' { return 'application/x-msclip' }
        '.cmd' { return 'text/plain' }
        '.cmx' { return 'image/x-cmx' }
        '.cnf' { return 'text/plain' }
        '.cod' { return 'image/cis-cod' }
        '.config' { return 'application/xml' }
        '.contact' { return 'text/x-ms-contact' }
        '.coverage' { return 'application/xml' }
        '.cpio' { return 'application/x-cpio' }
        '.cpp' { return 'text/plain' }
        '.crd' { return 'application/x-mscardfile' }
        '.crl' { return 'application/pkix-crl' }
        '.crt' { return 'application/x-x509-ca-cert' }
        '.cs' { return 'text/plain' }
        '.csdproj' { return 'text/plain' }
        '.csh' { return 'application/x-csh' }
        '.csproj' { return 'text/plain' }
        '.css' { return 'text/css' }
        '.csv' { return 'text/csv' }
        '.cur' { return 'application/octet-stream' }
        '.cxx' { return 'text/plain' }
        '.dat' { return 'application/octet-stream' }
        '.datasource' { return 'application/xml' }
        '.dbproj' { return 'text/plain' }
        '.dcr' { return 'application/x-director' }
        '.def' { return 'text/plain' }
        '.deploy' { return 'application/octet-stream' }
        '.der' { return 'application/x-x509-ca-cert' }
        '.dgml' { return 'application/xml' }
        '.dib' { return 'image/bmp' }
        '.dif' { return 'video/x-dv' }
        '.dir' { return 'application/x-director' }
        '.disco' { return 'text/xml' }
        '.divx' { return 'video/divx' }
        '.dll' { return 'application/x-msdownload' }
        '.dll.config' { return 'text/xml' }
        '.dlm' { return 'text/dlm' }
        '.doc' { return 'application/msword' }
        '.docm' { return 'application/vnd.ms-word.document.macroEnabled.12' }
        '.docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
        '.dot' { return 'application/msword' }
        '.dotm' { return 'application/vnd.ms-word.template.macroEnabled.12' }
        '.dotx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' }
        '.dsp' { return 'application/octet-stream' }
        '.dsw' { return 'text/plain' }
        '.dtd' { return 'text/xml' }
        '.dtsconfig' { return 'text/xml' }
        '.dv' { return 'video/x-dv' }
        '.dvi' { return 'application/x-dvi' }
        '.dwf' { return 'drawing/x-dwf' }
        '.dwg' { return 'application/acad' }
        '.dwp' { return 'application/octet-stream' }
        '.dxf' { return 'application/x-dxf' }
        '.dxr' { return 'application/x-director' }
        '.eml' { return 'message/rfc822' }
        '.emz' { return 'application/octet-stream' }
        '.eot' { return 'application/vnd.ms-fontobject' }
        '.eps' { return 'application/postscript' }
        '.etl' { return 'application/etl' }
        '.etx' { return 'text/x-setext' }
        '.evy' { return 'application/envoy' }
        '.exe' { return 'application/octet-stream' }
        '.exe.config' { return 'text/xml' }
        '.fdf' { return 'application/vnd.fdf' }
        '.fif' { return 'application/fractals' }
        '.filters' { return 'application/xml' }
        '.fla' { return 'application/octet-stream' }
        '.flac' { return 'audio/flac' }
        '.flr' { return 'x-world/x-vrml' }
        '.flv' { return 'video/x-flv' }
        '.fsscript' { return 'application/fsharp-script' }
        '.fsx' { return 'application/fsharp-script' }
        '.generictest' { return 'application/xml' }
        '.gif' { return 'image/gif' }
        '.gpx' { return 'application/gpx+xml' }
        '.group' { return 'text/x-ms-group' }
        '.gsm' { return 'audio/x-gsm' }
        '.gtar' { return 'application/x-gtar' }
        '.gz' { return 'application/x-gzip' }
        '.gzip' { return 'application/x-gzip' }
        '.h' { return 'text/plain' }
        '.hdf' { return 'application/x-hdf' }
        '.hdml' { return 'text/x-hdml' }
        '.hhc' { return 'application/x-oleobject' }
        '.hhk' { return 'application/octet-stream' }
        '.hhp' { return 'application/octet-stream' }
        '.hlp' { return 'application/winhlp' }
        '.hpp' { return 'text/plain' }
        '.hqx' { return 'application/mac-binhex40' }
        '.hta' { return 'application/hta' }
        '.htc' { return 'text/x-component' }
        '.htm' { return 'text/html' }
        '.html' { return 'text/html' }
        '.htt' { return 'text/webviewhtml' }
        '.hxa' { return 'application/xml' }
        '.hxc' { return 'application/xml' }
        '.hxd' { return 'application/octet-stream' }
        '.hxe' { return 'application/xml' }
        '.hxf' { return 'application/xml' }
        '.hxh' { return 'application/octet-stream' }
        '.hxi' { return 'application/octet-stream' }
        '.hxk' { return 'application/xml' }
        '.hxq' { return 'application/octet-stream' }
        '.hxr' { return 'application/octet-stream' }
        '.hxs' { return 'application/octet-stream' }
        '.hxt' { return 'text/html' }
        '.hxv' { return 'application/xml' }
        '.hxw' { return 'application/octet-stream' }
        '.hxx' { return 'text/plain' }
        '.i' { return 'text/plain' }
        '.ico' { return 'image/x-icon' }
        '.ics' { return 'application/octet-stream' }
        '.idl' { return 'text/plain' }
        '.ief' { return 'image/ief' }
        '.iii' { return 'application/x-iphone' }
        '.inc' { return 'text/plain' }
        '.inf' { return 'application/octet-stream' }
        '.ini' { return 'text/plain' }
        '.inl' { return 'text/plain' }
        '.ins' { return 'application/x-internet-signup' }
        '.ipa' { return 'application/x-itunes-ipa' }
        '.ipg' { return 'application/x-itunes-ipg' }
        '.ipproj' { return 'text/plain' }
        '.ipsw' { return 'application/x-itunes-ipsw' }
        '.iqy' { return 'text/x-ms-iqy' }
        '.isp' { return 'application/x-internet-signup' }
        '.ite' { return 'application/x-itunes-ite' }
        '.itlp' { return 'application/x-itunes-itlp' }
        '.itms' { return 'application/x-itunes-itms' }
        '.itpc' { return 'application/x-itunes-itpc' }
        '.ivf' { return 'video/x-ivf' }
        '.jar' { return 'application/java-archive' }
        '.java' { return 'application/octet-stream' }
        '.jck' { return 'application/liquidmotion' }
        '.jcz' { return 'application/liquidmotion' }
        '.jfif' { return 'image/pjpeg' }
        '.jnlp' { return 'application/x-java-jnlp-file' }
        '.jpb' { return 'application/octet-stream' }
        '.jpe' { return 'image/jpeg' }
        '.jpeg' { return 'image/jpeg' }
        '.jpg' { return 'image/jpeg' }
        '.js' { return 'application/javascript' }
        '.json' { return 'application/json' }
        '.jsx' { return 'text/jscript' }
        '.jsxbin' { return 'text/plain' }
        '.jwt' { return 'application/jwt' }
        '.latex' { return 'application/x-latex' }
        '.library-ms' { return 'application/windows-library+xml' }
        '.lit' { return 'application/x-ms-reader' }
        '.loadtest' { return 'application/xml' }
        '.lpk' { return 'application/octet-stream' }
        '.lsf' { return 'video/x-la-asf' }
        '.lst' { return 'text/plain' }
        '.lsx' { return 'video/x-la-asf' }
        '.lzh' { return 'application/octet-stream' }
        '.m13' { return 'application/x-msmediaview' }
        '.m14' { return 'application/x-msmediaview' }
        '.m1v' { return 'video/mpeg' }
        '.m2t' { return 'video/vnd.dlna.mpeg-tts' }
        '.m2ts' { return 'video/vnd.dlna.mpeg-tts' }
        '.m2v' { return 'video/mpeg' }
        '.m3u' { return 'audio/x-mpegurl' }
        '.m3u8' { return 'audio/x-mpegurl' }
        '.m4a' { return 'audio/m4a' }
        '.m4b' { return 'audio/m4b' }
        '.m4p' { return 'audio/m4p' }
        '.m4r' { return 'audio/x-m4r' }
        '.m4v' { return 'video/x-m4v' }
        '.mac' { return 'image/x-macpaint' }
        '.mak' { return 'text/plain' }
        '.man' { return 'application/x-troff-man' }
        '.manifest' { return 'application/x-ms-manifest' }
        '.map' { return 'text/plain' }
        '.markdown' { return 'text/markdown' }
        '.master' { return 'application/xml' }
        '.mbox' { return 'application/mbox' }
        '.md' { return 'text/markdown' }
        '.mda' { return 'application/msaccess' }
        '.mdb' { return 'application/x-msaccess' }
        '.mde' { return 'application/msaccess' }
        '.mdp' { return 'application/octet-stream' }
        '.me' { return 'application/x-troff-me' }
        '.mfp' { return 'application/x-shockwave-flash' }
        '.mht' { return 'message/rfc822' }
        '.mhtml' { return 'message/rfc822' }
        '.mid' { return 'audio/mid' }
        '.midi' { return 'audio/mid' }
        '.mix' { return 'application/octet-stream' }
        '.mk' { return 'text/plain' }
        '.mk3d' { return 'video/x-matroska-3d' }
        '.mka' { return 'audio/x-matroska' }
        '.mkv' { return 'video/x-matroska' }
        '.mmf' { return 'application/x-smaf' }
        '.mno' { return 'text/xml' }
        '.mny' { return 'application/x-msmoney' }
        '.mod' { return 'video/mpeg' }
        '.mov' { return 'video/quicktime' }
        '.movie' { return 'video/x-sgi-movie' }
        '.mp2' { return 'video/mpeg' }
        '.mp2v' { return 'video/mpeg' }
        '.mp3' { return 'audio/mpeg' }
        '.mp4' { return 'video/mp4' }
        '.mp4v' { return 'video/mp4' }
        '.mpa' { return 'video/mpeg' }
        '.mpe' { return 'video/mpeg' }
        '.mpeg' { return 'video/mpeg' }
        '.mpf' { return 'application/vnd.ms-mediapackage' }
        '.mpg' { return 'video/mpeg' }
        '.mpp' { return 'application/vnd.ms-project' }
        '.mpv2' { return 'video/mpeg' }
        '.mqv' { return 'video/quicktime' }
        '.ms' { return 'application/x-troff-ms' }
        '.msg' { return 'application/vnd.ms-outlook' }
        '.msi' { return 'application/octet-stream' }
        '.mso' { return 'application/octet-stream' }
        '.mts' { return 'video/vnd.dlna.mpeg-tts' }
        '.mtx' { return 'application/xml' }
        '.mvb' { return 'application/x-msmediaview' }
        '.mvc' { return 'application/x-miva-compiled' }
        '.mxp' { return 'application/x-mmxp' }
        '.nc' { return 'application/x-netcdf' }
        '.nsc' { return 'video/x-ms-asf' }
        '.nws' { return 'message/rfc822' }
        '.ocx' { return 'application/octet-stream' }
        '.oda' { return 'application/oda' }
        '.odb' { return 'application/vnd.oasis.opendocument.database' }
        '.odc' { return 'application/vnd.oasis.opendocument.chart' }
        '.odf' { return 'application/vnd.oasis.opendocument.formula' }
        '.odg' { return 'application/vnd.oasis.opendocument.graphics' }
        '.odh' { return 'text/plain' }
        '.odi' { return 'application/vnd.oasis.opendocument.image' }
        '.odl' { return 'text/plain' }
        '.odm' { return 'application/vnd.oasis.opendocument.text-master' }
        '.odp' { return 'application/vnd.oasis.opendocument.presentation' }
        '.ods' { return 'application/vnd.oasis.opendocument.spreadsheet' }
        '.odt' { return 'application/vnd.oasis.opendocument.text' }
        '.oga' { return 'audio/ogg' }
        '.ogg' { return 'audio/ogg' }
        '.ogv' { return 'video/ogg' }
        '.ogx' { return 'application/ogg' }
        '.one' { return 'application/onenote' }
        '.onea' { return 'application/onenote' }
        '.onepkg' { return 'application/onenote' }
        '.onetmp' { return 'application/onenote' }
        '.onetoc' { return 'application/onenote' }
        '.onetoc2' { return 'application/onenote' }
        '.opus' { return 'audio/ogg' }
        '.orderedtest' { return 'application/xml' }
        '.osdx' { return 'application/opensearchdescription+xml' }
        '.otf' { return 'application/font-sfnt' }
        '.otg' { return 'application/vnd.oasis.opendocument.graphics-template' }
        '.oth' { return 'application/vnd.oasis.opendocument.text-web' }
        '.otp' { return 'application/vnd.oasis.opendocument.presentation-template' }
        '.ots' { return 'application/vnd.oasis.opendocument.spreadsheet-template' }
        '.ott' { return 'application/vnd.oasis.opendocument.text-template' }
        '.oxt' { return 'application/vnd.openofficeorg.extension' }
        '.p10' { return 'application/pkcs10' }
        '.p12' { return 'application/x-pkcs12' }
        '.p7b' { return 'application/x-pkcs7-certificates' }
        '.p7c' { return 'application/pkcs7-mime' }
        '.p7m' { return 'application/pkcs7-mime' }
        '.p7r' { return 'application/x-pkcs7-certreqresp' }
        '.p7s' { return 'application/pkcs7-signature' }
        '.pbm' { return 'image/x-portable-bitmap' }
        '.pcast' { return 'application/x-podcast' }
        '.pct' { return 'image/pict' }
        '.pcx' { return 'application/octet-stream' }
        '.pcz' { return 'application/octet-stream' }
        '.pdf' { return 'application/pdf' }
        '.pfb' { return 'application/octet-stream' }
        '.pfm' { return 'application/octet-stream' }
        '.pfx' { return 'application/x-pkcs12' }
        '.pgm' { return 'image/x-portable-graymap' }
        '.pic' { return 'image/pict' }
        '.pict' { return 'image/pict' }
        '.pkgdef' { return 'text/plain' }
        '.pkgundef' { return 'text/plain' }
        '.pko' { return 'application/vnd.ms-pki.pko' }
        '.pls' { return 'audio/scpls' }
        '.pma' { return 'application/x-perfmon' }
        '.pmc' { return 'application/x-perfmon' }
        '.pml' { return 'application/x-perfmon' }
        '.pmr' { return 'application/x-perfmon' }
        '.pmw' { return 'application/x-perfmon' }
        '.png' { return 'image/png' }
        '.pnm' { return 'image/x-portable-anymap' }
        '.pnt' { return 'image/x-macpaint' }
        '.pntg' { return 'image/x-macpaint' }
        '.pnz' { return 'image/png' }
        '.pode' { return 'application/PowerShell' }
        '.pot' { return 'application/vnd.ms-powerpoint' }
        '.potm' { return 'application/vnd.ms-powerpoint.template.macroEnabled.12' }
        '.potx' { return 'application/vnd.openxmlformats-officedocument.presentationml.template' }
        '.ppa' { return 'application/vnd.ms-powerpoint' }
        '.ppam' { return 'application/vnd.ms-powerpoint.addin.macroEnabled.12' }
        '.ppm' { return 'image/x-portable-pixmap' }
        '.pps' { return 'application/vnd.ms-powerpoint' }
        '.ppsm' { return 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' }
        '.ppsx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' }
        '.ppt' { return 'application/vnd.ms-powerpoint' }
        '.pptm' { return 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' }
        '.pptx' { return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
        '.prf' { return 'application/pics-rules' }
        '.prm' { return 'application/octet-stream' }
        '.prx' { return 'application/octet-stream' }
        '.ps' { return 'application/postscript' }
        '.ps1' { return 'application/PowerShell' }
        '.psc1' { return 'application/PowerShell' }
        '.psd1' { return 'application/PowerShell' }
        '.psm1' { return 'application/PowerShell' }
        '.psd' { return 'application/octet-stream' }
        '.psess' { return 'application/xml' }
        '.psm' { return 'application/octet-stream' }
        '.psp' { return 'application/octet-stream' }
        '.pst' { return 'application/vnd.ms-outlook' }
        '.pub' { return 'application/x-mspublisher' }
        '.pwz' { return 'application/vnd.ms-powerpoint' }
        '.qht' { return 'text/x-html-insertion' }
        '.qhtm' { return 'text/x-html-insertion' }
        '.qt' { return 'video/quicktime' }
        '.qti' { return 'image/x-quicktime' }
        '.qtif' { return 'image/x-quicktime' }
        '.qtl' { return 'application/x-quicktimeplayer' }
        '.qxd' { return 'application/octet-stream' }
        '.ra' { return 'audio/x-pn-realaudio' }
        '.ram' { return 'audio/x-pn-realaudio' }
        '.rar' { return 'application/x-rar-compressed' }
        '.ras' { return 'image/x-cmu-raster' }
        '.rat' { return 'application/rat-file' }
        '.rc' { return 'text/plain' }
        '.rc2' { return 'text/plain' }
        '.rct' { return 'text/plain' }
        '.rdlc' { return 'application/xml' }
        '.reg' { return 'text/plain' }
        '.resx' { return 'application/xml' }
        '.rf' { return 'image/vnd.rn-realflash' }
        '.rgb' { return 'image/x-rgb' }
        '.rgs' { return 'text/plain' }
        '.rm' { return 'application/vnd.rn-realmedia' }
        '.rmi' { return 'audio/mid' }
        '.rmp' { return 'application/vnd.rn-rn_music_package' }
        '.roff' { return 'application/x-troff' }
        '.rpm' { return 'audio/x-pn-realaudio-plugin' }
        '.rqy' { return 'text/x-ms-rqy' }
        '.rtf' { return 'application/rtf' }
        '.rtx' { return 'text/richtext' }
        '.rvt' { return 'application/octet-stream' }
        '.ruleset' { return 'application/xml' }
        '.s' { return 'text/plain' }
        '.safariextz' { return 'application/x-safari-safariextz' }
        '.scd' { return 'application/x-msschedule' }
        '.scr' { return 'text/plain' }
        '.sct' { return 'text/scriptlet' }
        '.sd2' { return 'audio/x-sd2' }
        '.sdp' { return 'application/sdp' }
        '.sea' { return 'application/octet-stream' }
        '.searchconnector-ms' { return 'application/windows-search-connector+xml' }
        '.setpay' { return 'application/set-payment-initiation' }
        '.setreg' { return 'application/set-registration-initiation' }
        '.settings' { return 'application/xml' }
        '.sgimb' { return 'application/x-sgimb' }
        '.sgml' { return 'text/sgml' }
        '.sh' { return 'application/x-sh' }
        '.shar' { return 'application/x-shar' }
        '.shtml' { return 'text/html' }
        '.sit' { return 'application/x-stuffit' }
        '.sitemap' { return 'application/xml' }
        '.skin' { return 'application/xml' }
        '.skp' { return 'application/x-koan' }
        '.sldm' { return 'application/vnd.ms-powerpoint.slide.macroEnabled.12' }
        '.sldx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slide' }
        '.slk' { return 'application/vnd.ms-excel' }
        '.sln' { return 'text/plain' }
        '.slupkg-ms' { return 'application/x-ms-license' }
        '.smd' { return 'audio/x-smd' }
        '.smi' { return 'application/octet-stream' }
        '.smx' { return 'audio/x-smd' }
        '.smz' { return 'audio/x-smd' }
        '.snd' { return 'audio/basic' }
        '.snippet' { return 'application/xml' }
        '.snp' { return 'application/octet-stream' }
        '.sol' { return 'text/plain' }
        '.sor' { return 'text/plain' }
        '.spc' { return 'application/x-pkcs7-certificates' }
        '.spl' { return 'application/futuresplash' }
        '.spx' { return 'audio/ogg' }
        '.src' { return 'application/x-wais-source' }
        '.srf' { return 'text/plain' }
        '.ssisdeploymentmanifest' { return 'text/xml' }
        '.ssm' { return 'application/streamingmedia' }
        '.sst' { return 'application/vnd.ms-pki.certstore' }
        '.stl' { return 'application/vnd.ms-pki.stl' }
        '.sv4cpio' { return 'application/x-sv4cpio' }
        '.sv4crc' { return 'application/x-sv4crc' }
        '.svc' { return 'application/xml' }
        '.svg' { return 'image/svg+xml' }
        '.swf' { return 'application/x-shockwave-flash' }
        '.step' { return 'application/step' }
        '.stp' { return 'application/step' }
        '.t' { return 'application/x-troff' }
        '.tar' { return 'application/x-tar' }
        '.tcl' { return 'application/x-tcl' }
        '.testrunconfig' { return 'application/xml' }
        '.testsettings' { return 'application/xml' }
        '.tex' { return 'application/x-tex' }
        '.texi' { return 'application/x-texinfo' }
        '.texinfo' { return 'application/x-texinfo' }
        '.tgz' { return 'application/x-compressed' }
        '.thmx' { return 'application/vnd.ms-officetheme' }
        '.thn' { return 'application/octet-stream' }
        '.tif' { return 'image/tiff' }
        '.tiff' { return 'image/tiff' }
        '.tlh' { return 'text/plain' }
        '.tli' { return 'text/plain' }
        '.toc' { return 'application/octet-stream' }
        '.tr' { return 'application/x-troff' }
        '.trm' { return 'application/x-msterminal' }
        '.trx' { return 'application/xml' }
        '.ts' { return 'video/vnd.dlna.mpeg-tts' }
        '.tsv' { return 'text/tab-separated-values' }
        '.ttf' { return 'application/font-sfnt' }
        '.tts' { return 'video/vnd.dlna.mpeg-tts' }
        '.txt' { return 'text/plain' }
        '.u32' { return 'application/octet-stream' }
        '.uls' { return 'text/iuls' }
        '.user' { return 'text/plain' }
        '.ustar' { return 'application/x-ustar' }
        '.vb' { return 'text/plain' }
        '.vbdproj' { return 'text/plain' }
        '.vbk' { return 'video/mpeg' }
        '.vbproj' { return 'text/plain' }
        '.vbs' { return 'text/vbscript' }
        '.vcf' { return 'text/x-vcard' }
        '.vcproj' { return 'application/xml' }
        '.vcs' { return 'text/plain' }
        '.vcxproj' { return 'application/xml' }
        '.vddproj' { return 'text/plain' }
        '.vdp' { return 'text/plain' }
        '.vdproj' { return 'text/plain' }
        '.vdx' { return 'application/vnd.ms-visio.viewer' }
        '.vml' { return 'text/xml' }
        '.vscontent' { return 'application/xml' }
        '.vsct' { return 'text/xml' }
        '.vsd' { return 'application/vnd.visio' }
        '.vsi' { return 'application/ms-vsi' }
        '.vsix' { return 'application/vsix' }
        '.vsixlangpack' { return 'text/xml' }
        '.vsixmanifest' { return 'text/xml' }
        '.vsmdi' { return 'application/xml' }
        '.vspscc' { return 'text/plain' }
        '.vss' { return 'application/vnd.visio' }
        '.vsscc' { return 'text/plain' }
        '.vssettings' { return 'text/xml' }
        '.vssscc' { return 'text/plain' }
        '.vst' { return 'application/vnd.visio' }
        '.vstemplate' { return 'text/xml' }
        '.vsto' { return 'application/x-ms-vsto' }
        '.vsw' { return 'application/vnd.visio' }
        '.vsx' { return 'application/vnd.visio' }
        '.vtx' { return 'application/vnd.visio' }
        '.wasm' { return 'application/wasm' }
        '.wav' { return 'audio/wav' }
        '.wave' { return 'audio/wav' }
        '.wax' { return 'audio/x-ms-wax' }
        '.wbk' { return 'application/msword' }
        '.wbmp' { return 'image/vnd.wap.wbmp' }
        '.wcm' { return 'application/vnd.ms-works' }
        '.wdb' { return 'application/vnd.ms-works' }
        '.wdp' { return 'image/vnd.ms-photo' }
        '.webarchive' { return 'application/x-safari-webarchive' }
        '.webm' { return 'video/webm' }
        '.webp' { return 'image/webp' }
        '.webtest' { return 'application/xml' }
        '.wiq' { return 'application/xml' }
        '.wiz' { return 'application/msword' }
        '.wks' { return 'application/vnd.ms-works' }
        '.wlmp' { return 'application/wlmoviemaker' }
        '.wlpginstall' { return 'application/x-wlpg-detect' }
        '.wlpginstall3' { return 'application/x-wlpg3-detect' }
        '.wm' { return 'video/x-ms-wm' }
        '.wma' { return 'audio/x-ms-wma' }
        '.wmd' { return 'application/x-ms-wmd' }
        '.wmf' { return 'application/x-msmetafile' }
        '.wml' { return 'text/vnd.wap.wml' }
        '.wmlc' { return 'application/vnd.wap.wmlc' }
        '.wmls' { return 'text/vnd.wap.wmlscript' }
        '.wmlsc' { return 'application/vnd.wap.wmlscriptc' }
        '.wmp' { return 'video/x-ms-wmp' }
        '.wmv' { return 'video/x-ms-wmv' }
        '.wmx' { return 'video/x-ms-wmx' }
        '.wmz' { return 'application/x-ms-wmz' }
        '.woff' { return 'application/font-woff' }
        '.woff2' { return 'application/font-woff2' }
        '.wpl' { return 'application/vnd.ms-wpl' }
        '.wps' { return 'application/vnd.ms-works' }
        '.wri' { return 'application/x-mswrite' }
        '.wrl' { return 'x-world/x-vrml' }
        '.wrz' { return 'x-world/x-vrml' }
        '.wsc' { return 'text/scriptlet' }
        '.wsdl' { return 'text/xml' }
        '.wvx' { return 'video/x-ms-wvx' }
        '.x' { return 'application/directx' }
        '.xaf' { return 'x-world/x-vrml' }
        '.xaml' { return 'application/xaml+xml' }
        '.xap' { return 'application/x-silverlight-app' }
        '.xbap' { return 'application/x-ms-xbap' }
        '.xbm' { return 'image/x-xbitmap' }
        '.xdr' { return 'text/plain' }
        '.xht' { return 'application/xhtml+xml' }
        '.xhtml' { return 'application/xhtml+xml' }
        '.xla' { return 'application/vnd.ms-excel' }
        '.xlam' { return 'application/vnd.ms-excel.addin.macroEnabled.12' }
        '.xlc' { return 'application/vnd.ms-excel' }
        '.xld' { return 'application/vnd.ms-excel' }
        '.xlk' { return 'application/vnd.ms-excel' }
        '.xll' { return 'application/vnd.ms-excel' }
        '.xlm' { return 'application/vnd.ms-excel' }
        '.xls' { return 'application/vnd.ms-excel' }
        '.xlsb' { return 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' }
        '.xlsm' { return 'application/vnd.ms-excel.sheet.macroEnabled.12' }
        '.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
        '.xlt' { return 'application/vnd.ms-excel' }
        '.xltm' { return 'application/vnd.ms-excel.template.macroEnabled.12' }
        '.xltx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' }
        '.xlw' { return 'application/vnd.ms-excel' }
        '.xml' { return 'text/xml' }
        '.xmp' { return 'application/octet-stream' }
        '.xmta' { return 'application/xml' }
        '.xof' { return 'x-world/x-vrml' }
        '.xoml' { return 'text/plain' }
        '.xpm' { return 'image/x-xpixmap' }
        '.xps' { return 'application/vnd.ms-xpsdocument' }
        '.xrm-ms' { return 'text/xml' }
        '.xsc' { return 'application/xml' }
        '.xsd' { return 'text/xml' }
        '.xsf' { return 'text/xml' }
        '.xsl' { return 'text/xml' }
        '.xslt' { return 'text/xml' }
        '.xsn' { return 'application/octet-stream' }
        '.xss' { return 'application/xml' }
        '.xspf' { return 'application/xspf+xml' }
        '.xtp' { return 'application/octet-stream' }
        '.xwd' { return 'image/x-xwindowdump' }
        '.yaml' { return 'application/x-yaml' }
        '.yml' { return 'application/x-yaml' }
        '.z' { return 'application/x-compress' }
        '.zip' { return 'application/zip' }
        default { return (Resolve-PodeValue -Check $DefaultIsNull -TrueValue $null -FalseValue 'text/plain') }
    }
}

function Get-PodeStatusDescription {
    param(
        [Parameter()]
        [int]
        $StatusCode
    )

    switch ($StatusCode) {
        100 { return 'Continue' }
        101 { return 'Switching Protocols' }
        102 { return 'Processing' }
        103 { return 'Early Hints' }
        200 { return 'OK' }
        201 { return 'Created' }
        202 { return 'Accepted' }
        203 { return 'Non-Authoritative Information' }
        204 { return 'No Content' }
        205 { return 'Reset Content' }
        206 { return 'Partial Content' }
        207 { return 'Multi-Status' }
        208 { return 'Already Reported' }
        226 { return 'IM Used' }
        300 { return 'Multiple Choices' }
        301 { return 'Moved Permanently' }
        302 { return 'Found' }
        303 { return 'See Other' }
        304 { return 'Not Modified' }
        305 { return 'Use Proxy' }
        306 { return 'Switch Proxy' }
        307 { return 'Temporary Redirect' }
        308 { return 'Permanent Redirect' }
        400 { return 'Bad Request' }
        401 { return 'Unauthorized' }
        402 { return 'Payment Required' }
        403 { return 'Forbidden' }
        404 { return 'Not Found' }
        405 { return 'Method Not Allowed' }
        406 { return 'Not Acceptable' }
        407 { return 'Proxy Authentication Required' }
        408 { return 'Request Timeout' }
        409 { return 'Conflict' }
        410 { return 'Gone' }
        411 { return 'Length Required' }
        412 { return 'Precondition Failed' }
        413 { return 'Payload Too Large' }
        414 { return 'URI Too Long' }
        415 { return 'Unsupported Media Type' }
        416 { return 'Range Not Satisfiable' }
        417 { return 'Expectation Failed' }
        418 { return "I'm a Teapot" }
        419 { return 'Page Expired' }
        420 { return 'Enhance Your Calm' }
        421 { return 'Misdirected Request' }
        422 { return 'Unprocessable Entity' }
        423 { return 'Locked' }
        424 { return 'Failed Dependency' }
        425 { return 'Too Early' }
        426 { return 'Upgrade Required' }
        428 { return 'Precondition Required' }
        429 { return 'Too Many Requests' }
        431 { return 'Request Header Fields Too Large' }
        440 { return 'Login Time-out' }
        450 { return 'Blocked by Windows Parental Controls' }
        451 { return 'Unavailable For Legal Reasons' }
        500 { return 'Internal Server Error' }
        501 { return 'Not Implemented' }
        502 { return 'Bad Gateway' }
        503 { return 'Service Unavailable' }
        504 { return 'Gateway Timeout' }
        505 { return 'HTTP Version Not Supported' }
        506 { return 'Variant Also Negotiates' }
        507 { return 'Insufficient Storage' }
        508 { return 'Loop Detected' }
        510 { return 'Not Extended' }
        511 { return 'Network Authentication Required' }
        526 { return 'Invalid SSL Certificate' }
        default { return ([string]::Empty) }
    }
}
src\Private\Metrics.ps1
function Update-PodeServerRequestMetrics {
    param(
        [Parameter()]
        [hashtable]
        $WebEvent
    )

    if ($null -eq $WebEvent) {
        return
    }

    # status code
    $status = "$($WebEvent.Response.StatusCode)"

    # metrics to update
    $metrics = @($PodeContext.Metrics.Requests)
    if ($null -ne $WebEvent.Route) {
        $metrics += $WebEvent.Route.Metrics.Requests
    }

    # increment the request metrics
    foreach ($metric in $metrics) {
        Lock-PodeObject -Object $metric -ScriptBlock {
            $metric.Total++

            if (!$metric.StatusCodes.ContainsKey($status)) {
                $metric.StatusCodes[$status] = 0
            }

            $metric.StatusCodes[$status]++
        }
    }
}

function Update-PodeServerSignalMetrics {
    param(
        [Parameter()]
        [hashtable]
        $SignalEvent
    )

    if ($null -eq $SignalEvent) {
        return
    }

    # metrics to update
    $metrics = @($PodeContext.Metrics.Signals)
    if ($null -ne $SignalEvent.Route) {
        $metrics += $SignalEvent.Route.Metrics.Requests
    }

    # increment the request metrics
    foreach ($metric in $metrics) {
        Lock-PodeObject -Object $metric -ScriptBlock {
            $metric.Total++
        }
    }
}
src\Private\Middleware.ps1
using namespace System.Security.Cryptography

function Invoke-PodeMiddleware {
    param(
        [Parameter()]
        $Middleware,

        [Parameter()]
        [string]
        $Route
    )

    # if there's no middleware, do nothing
    if (($null -eq $Middleware) -or ($Middleware.Length -eq 0)) {
        return $true
    }

    # filter the middleware down by route (retaining order)
    if (![string]::IsNullOrWhiteSpace($Route)) {
        $Middleware = @(foreach ($mware in $Middleware) {
                if ($null -eq $mware) {
                    continue
                }

                if ([string]::IsNullOrWhiteSpace($mware.Route) -or ($mware.Route -ieq '/') -or ($mware.Route -ieq $Route) -or ($Route -imatch "^$($mware.Route)$")) {
                    $mware
                }
            })
    }

    # continue or halt?
    $continue = $true

    # loop through each of the middleware, invoking the next if it returns true
    foreach ($midware in @($Middleware)) {
        if (($null -eq $midware) -or ($null -eq $midware.Logic)) {
            continue
        }

        try {
            $continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments $midware.Arguments -UsingVariables $midware.UsingVariables -Return -Scoped -Splat
            if ($null -eq $continue) {
                $continue = $true
            }
        }
        catch {
            Set-PodeResponseStatus -Code 500 -Exception $_
            $continue = $false
            $_ | Write-PodeErrorLog
        }

        if (!$continue) {
            break
        }
    }

    return $continue
}

function New-PodeMiddlewareInternal {
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    if (Test-PodeIsEmpty $ScriptBlock) {
        throw '[Middleware]: No ScriptBlock supplied'
    }

    # if route is empty, set it to root
    $Route = ConvertTo-PodeRouteRegex -Path $Route

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSSession

    # create the middleware hashtable from a scriptblock
    $HashTable = @{
        Route          = $Route
        Logic          = $ScriptBlock
        Arguments      = $ArgumentList
        UsingVariables = $usingVars
    }

    # return the middleware, so it can be cached/added at a later date
    return $HashTable
}

function Get-PodeInbuiltMiddleware {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock
    )

    # check if middleware contains an override
    $override = ($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name })

    # if override there, remove it from middleware
    if ($override) {
        $PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
        $ScriptBlock = $override.Logic
    }

    # return the script
    return @{
        Name  = $Name
        Logic = $ScriptBlock
    }
}

function Get-PodeAccessMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_access__' -ScriptBlock {
            # are there any rules?
            if (($PodeContext.Server.Access.Allow.Count -eq 0) -and ($PodeContext.Server.Access.Deny.Count -eq 0)) {
                return $true
            }

            # ensure the request IP address is allowed
            if (!(Test-PodeIPAccess -IP $WebEvent.Request.RemoteEndPoint.Address)) {
                Set-PodeResponseStatus -Code 403
                return $false
            }

            # request is allowed
            return $true
        })
}

function Get-PodeLimitMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_rate_limit__' -ScriptBlock {
            # are there any rules?
            if ($PodeContext.Server.Limits.Rules.Count -eq 0) {
                return $true
            }

            # check the request IP address has not hit a rate limit
            if (!(Test-PodeIPLimit -IP $WebEvent.Request.RemoteEndPoint.Address)) {
                Set-PodeResponseStatus -Code 429
                return $false
            }

            # check the route
            if (!(Test-PodeRouteLimit -Path $WebEvent.Path)) {
                Set-PodeResponseStatus -Code 429
                return $false
            }

            # check the endpoint
            if (!(Test-PodeEndpointLimit -EndpointName $WebEvent.Endpoint.Name)) {
                Set-PodeResponseStatus -Code 429
                return $false
            }

            # request is allowed
            return $true
        })
}

<#
.SYNOPSIS
    Retrieves middleware for serving public static content in a Pode web server.
.DESCRIPTION
    This function retrieves middleware for serving public static content in a Pode web server.
    It searches for static content based on the requested path and serves it if found.
.PARAMETER WebEvent
    The PodeWebEvent object representing the incoming web request.
.PARAMETER PodeContext
    The PodeContext object representing the current Pode server context.
.EXAMPLE
    Get-PodePublicMiddleware
    Retrieves middleware for serving public static content.
#>
function Get-PodePublicMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_static_content__' -ScriptBlock {
            # only find public static content here
            $path = Find-PodePublicRoute -Path $WebEvent.Path
            if ([string]::IsNullOrWhiteSpace($path)) {
                return $true
            }

            # check current state of caching
            $cachable = Test-PodeRouteValidForCaching -Path $WebEvent.Path

            # write the file to the response
            Write-PodeFileResponse -Path $path -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable

            # public static content found, stop
            return $false
        })
}

<#
.SYNOPSIS
    Middleware function to validate the route for an incoming web request.

.DESCRIPTION
    This function is used as middleware to validate the route for an incoming web request. It checks if the route exists for the requested method and path. If the route does not exist, it sets the appropriate response status code (404 for not found, 405 for method not allowed) and returns false to halt further processing. If the route exists, it sets various properties on the WebEvent object, such as parameters, content type, and transfer encoding, and returns true to continue processing.

.PARAMETER None

.EXAMPLE
    $middleware = Get-PodeRouteValidateMiddleware
    Add-PodeMiddleware -Middleware $middleware

.NOTES
    This function is part of the internal Pode server logic and is typically not called directly by users.

#>
function Get-PodeRouteValidateMiddleware {
    return @{
        Name  = '__pode_mw_route_validation__'
        Logic = {
            if ($PodeContext.Server.Web.Static.ValidateLast) {
                #  check the main routes and check the static routes
                $route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod
                if ($null -eq $route) {
                    $route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
                }
            }
            else {
                # check if the path is static route first, then check the main routes
                $route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
                if ($null -eq $route) {
                    $route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod
                }
            }

            # if there's no route defined, it's a 404 - or a 405 if a route exists for any other method
            if ($null -eq $route) {
                # check if a route exists for another method
                $methods = @('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE')
                $diff_route = @(foreach ($method in $methods) {
                        $r = Find-PodeRoute -Method $method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
                        if ($null -ne $r) {
                            $r
                            break
                        }
                    })[0]

                if ($null -ne $diff_route) {
                    Set-PodeResponseStatus -Code 405
                    return $false
                }

                # otheriwse, it's a 404
                Set-PodeResponseStatus -Code 404
                return $false
            }

            # check if static and split
            if ($null -ne $route.Content) {
                $WebEvent.StaticContent = $route.Content
                $route = $route.Route
            }

            # set the route parameters
            $WebEvent.Parameters = @{}
            if ($WebEvent.Path -imatch "$($route.Path)$") {
                $WebEvent.Parameters = $Matches
            }

            # set the route on the WebEvent
            $WebEvent.Route = $route

            # override the content type from the route if it's not empty
            if (![string]::IsNullOrWhiteSpace($route.ContentType)) {
                $WebEvent.ContentType = $route.ContentType
            }

            # override the transfer encoding from the route if it's not empty
            if (![string]::IsNullOrWhiteSpace($route.TransferEncoding)) {
                $WebEvent.TransferEncoding = $route.TransferEncoding
            }

            # set the content type for any pages for the route if it's not empty
            $WebEvent.ErrorType = $route.ErrorType

            # route exists
            return $true
        }
    }
}

function Get-PodeBodyMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_body_parsing__' -ScriptBlock {
            try {
                # attempt to parse that data
                $result = ConvertFrom-PodeRequestContent -Request $WebEvent.Request -ContentType $WebEvent.ContentType -TransferEncoding $WebEvent.TransferEncoding

                # set session data
                $WebEvent.Data = $result.Data
                $WebEvent.Files = $result.Files

                # payload parsed
                return $true
            }
            catch {
                Set-PodeResponseStatus -Code 400 -Exception $_
                return $false
            }
        })
}

function Get-PodeQueryMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_query_parsing__' -ScriptBlock {
            try {
                # set the query string from the request
                $WebEvent.Query = (ConvertFrom-PodeNameValueToHashTable -Collection $WebEvent.Request.QueryString)
                return $true
            }
            catch {
                Set-PodeResponseStatus -Code 400 -Exception $_
                return $false
            }
        })
}

function Get-PodeCookieMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_cookie_parsing__' -ScriptBlock {
            # if cookies already set, return
            if ($WebEvent.Cookies.Count -gt 0) {
                return $true
            }

            # if the request's header has no cookies, return
            $h_cookie = (Get-PodeHeader -Name 'Cookie')
            if ([string]::IsNullOrWhiteSpace($h_cookie)) {
                return $true
            }

            # parse the cookies from the header
            $cookies = @($h_cookie -split '; ')
            $WebEvent.Cookies = @{}

            foreach ($cookie in $cookies) {
                $atoms = $cookie.Split('=', 2)

                $value = [string]::Empty
                if ($atoms.Length -gt 1) {
                    foreach ($atom in $atoms[1..($atoms.Length - 1)]) {
                        $value += $atom
                    }
                }

                $WebEvent.Cookies[$atoms[0]] = [System.Net.Cookie]::new($atoms[0], $value)
            }

            return $true
        })
}

function Get-PodeSecurityMiddleware {
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_security__' -ScriptBlock {
            # are there any security headers setup?
            if ($PodeContext.Server.Security.Headers.Count -eq 0) {
                return $true
            }

            # add security headers
            Set-PodeHeaderBulk -Value $PodeContext.Server.Security.Headers

            # continue to next middleware/route
            return $true
        })
}

function Initialize-PodeIISMiddleware {
    # do nothing if not iis
    if (!$PodeContext.Server.IsIIS) {
        return
    }

    # fail if no iis token - because there should be!
    if ([string]::IsNullOrWhiteSpace($PodeContext.Server.IIS.Token)) {
        throw 'IIS ASPNETCORE_TOKEN is missing'
    }

    # add middleware to check every request has the token
    Add-PodeMiddleware -Name '__pode_iis_token_check__' -ScriptBlock {
        $token = Get-PodeHeader -Name 'MS-ASPNETCORE-TOKEN'
        if ($token -ne $PodeContext.Server.IIS.Token) {
            Set-PodeResponseStatus -Code 400 -Description 'MS-ASPNETCORE-TOKEN header missing'
            return $false
        }

        return $true
    }

    # add middleware to check if there's a client cert
    Add-PodeMiddleware -Name '__pode_iis_clientcert_check__' -ScriptBlock {
        if (!$WebEvent.Request.AllowClientCertificate -or ($null -ne $WebEvent.Request.ClientCertificate)) {
            return $true
        }

        $headers = @('MS-ASPNETCORE-CLIENTCERT', 'X-ARR-ClientCert')
        foreach ($header in $headers) {
            if (!(Test-PodeHeader -Name $header)) {
                continue
            }

            try {
                $value = Get-PodeHeader -Name $header
                $WebEvent.Request.ClientCertificate = [X509Certificates.X509Certificate2]::new([Convert]::FromBase64String($value))
            }
            catch {
                $WebEvent.Request.ClientCertificateErrors = [System.Net.Security.SslPolicyErrors]::RemoteCertificateNotAvailable
            }
        }

        return $true
    }

    # add route to gracefully shutdown server for iis
    Add-PodeRoute -Method Post -Path '/iisintegration' -ScriptBlock {
        $eventType = Get-PodeHeader -Name 'MS-ASPNETCORE-EVENT'

        # no x-forward
        if (Test-PodeHeader -Name 'X-Forwarded-For') {
            Set-PodeResponseStatus -Code 400
            return
        }

        # no user-agent
        if (Test-PodeHeader -Name 'User-Agent') {
            Set-PodeResponseStatus -Code 400
            return
        }

        # valid local Host
        $hostValue = Get-PodeHeader -Name 'Host'
        if ($hostValue -ine "127.0.0.1:$($PodeContext.Server.IIS.Port)") {
            Set-PodeResponseStatus -Code 400
            return
        }

        # no content-length
        if ($WebEvent.Request.ContentLength -gt 0) {
            Set-PodeResponseStatus -Code 400
            return
        }

        # valid event type
        if ($eventType -ine 'shutdown') {
            Set-PodeResponseStatus -Code 400
            return
        }

        # shutdown
        $PodeContext.Server.IIS.Shutdown = $true
        Close-PodeServer
        Set-PodeResponseStatus -Code 202
    }
}
src\Private\NameGenerator.ps1
function Get-PodeRandomName {
    $adjs = @(
        'admiring',
        'agitated',
        'blissful',
        'dazzling',
        'ecstatic',
        'eloquent',
        'friendly',
        'gracious',
        'hardcore',
        'laughing',
        'peaceful',
        'pedantic',
        'reverent',
        'romantic',
        'trusting',
        'vigilant',
        'vigorous',
        'wizardly',
        'youthful'
    )

    $names = @(
        'almeida',
        'babbage',
        'bardeen',
        'shannon',
        'davinci',
        'feynman',
        'galileo',
        'goodall',
        'hawking',
        'hermann',
        'hodgkin',
        'hypatia',
        'jackson',
        'johnson',
        'kapitsa',
        'keldysh',
        'khorana',
        'lalande',
        'lamport',
        'leavitt',
        'lumiere',
        'mcnulty',
        'meitner',
        'mestorf',
        'murdock',
        'neumann',
        'noether',
        'pasteur',
        'perlman',
        'poitras',
        'ptolemy',
        'ritchie',
        'shirley',
        'swanson',
        'swirles',
        'vaughan',
        'volhard',
        'villani',
        'wescoff',
        'wozniak'
    )

    $adjsRand = (Get-Random -Minimum 0 -Maximum $adjs.Length)
    $namesRand = (Get-Random -Minimum 0 -Maximum $names.Length)

    return "$($adjs[$adjsRand])_$($names[$namesRand])"
}
src\Private\OpenApi.ps1
<#
.SYNOPSIS
Converts content into an OpenAPI schema object format.

.DESCRIPTION
The ConvertTo-PodeOAObjectSchema function takes a hashtable representing content and converts it into a format suitable for OpenAPI schema objects.
It validates the content types, processes array structures, and converts each property or reference into the appropriate OpenAPI schema format.
The function is designed to handle complex content structures for OpenAPI documentation within the Pode framework.

.PARAMETER Content
A hashtable representing the content to be converted into an OpenAPI schema object. The content can include various types and structures.

.PARAMETER Properties
A switch to indicate if the content represents properties of an object schema.

.PARAMETER DefinitionTag
A string representing the definition tag to be used in the conversion process. This tag is essential for correctly formatting the content according to OpenAPI specifications.

.EXAMPLE
$schemaObject = ConvertTo-PodeOAObjectSchema -Content $myContent -DefinitionTag 'myTag'

Converts a hashtable of content into an OpenAPI schema object using the definition tag 'myTag'.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAObjectSchema {
    param(
        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $Content,

        [Parameter(ValueFromPipeline = $false)]
        [switch]
        $Properties,

        [Parameter(Mandatory = $true)]
        [string ]
        $DefinitionTag

    )

    # Ensure all content types are valid MIME types
    foreach ($type in $Content.Keys) {
        if ($type -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$|^"\*\/\*"$') {
            throw "Invalid content-type found for schema: $($type)"
        }
    }
    # manage generic schema json conversion issue
    if ( $Content.ContainsKey('*/*')) {
        $Content['"*/*"'] = $Content['*/*']
        $Content.Remove('*/*')
    }
    # convert each schema to OpenAPI format
    # Initialize an empty hashtable for the schema
    $obj = @{}

    # Process each content type
    $types = [string[]]$Content.Keys
    foreach ($type in $types) {
        # Initialize schema structure for the type
        $obj[$type] = @{ }

        # Handle upload content, array structures, and shared component schema references
        if ($Content[$type].__upload) {
            if ($Content[$type].__array) {
                $upload = $Content[$type].__content.__upload
            }
            else {
                $upload = $Content[$type].__upload
            }

            if ($type -ieq 'multipart/form-data' -and $upload.content ) {
                if ((Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) -and $upload.partContentMediaType) {
                    foreach ($key in $upload.content.Properties ) {
                        if ($key.type -eq 'string' -and $key.format -and $key.format -ieq 'binary' -or $key.format -ieq 'base64') {
                            $key.ContentMediaType = $PartContentMediaType
                            $key.remove('format')
                            break
                        }
                    }
                }
                $newContent = $upload.content
            }
            else {
                if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
                    $newContent = [ordered]@{
                        'type'   = 'string'
                        'format' = $upload.contentEncoding
                    }
                }
                else {
                    if ($ContentEncoding -ieq 'Base64') {
                        $newContent = [ordered]@{
                            'type'            = 'string'
                            'contentEncoding' = $upload.contentEncoding
                        }
                    }
                }
            }
            if ($Content[$type].__array) {
                $Content[$type].__content = $newContent
            }
            else {
                $Content[$type] = $newContent
            }
        }

        if ($Content[$type].__array) {
            $isArray = $true
            $item = $Content[$type].__content
            $obj[$type].schema = [ordered]@{
                'type'  = 'array'
                'items' = $null
            }
            if ( $Content[$type].__title) {
                $obj[$type].schema.title = $Content[$type].__title
            }
            if ( $Content[$type].__uniqueItems) {
                $obj[$type].schema.uniqueItems = $Content[$type].__uniqueItems
            }
            if ( $Content[$type].__maxItems) {
                $obj[$type].schema.__maxItems = $Content[$type].__maxItems
            }
            if ( $Content[$type].minItems) {
                $obj[$type].schema.minItems = $Content[$type].__minItems
            }
        }
        else {
            $item = $Content[$type]
            $isArray = $false
        }
        # Add set schema objects or empty content
        if ($item -is [string]) {
            if (![string]::IsNullOrEmpty($item )) {
                #Check for empty reference
                if (@('string', 'integer' , 'number', 'boolean' ) -icontains $item) {
                    if ($isArray) {
                        $obj[$type].schema.items = @{
                            'type' = $item.ToLower()
                        }
                    }
                    else {
                        $obj[$type].schema = @{
                            'type' = $item.ToLower()
                        }
                    }
                }
                else {
                    Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $item -PostValidation
                    if ($isArray) {
                        $obj[$type].schema.items = @{
                            '$ref' = "#/components/schemas/$($item)"
                        }
                    }
                    else {
                        $obj[$type].schema = @{
                            '$ref' = "#/components/schemas/$($item)"
                        }
                    }
                }
            }
            else {
                # Create an empty content
                $obj[$type] = @{}
            }
        }
        else {
            if ($item.Count -eq 0) {
                $result = @{}
            }
            else {
                $result = ($item | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
            }
            if ($Properties) {
                if ($item.Name) {
                    $obj[$type].schema = @{
                        'properties' = @{
                            $item.Name = $result
                        }
                    }
                }
                else {
                    Throw 'The Properties parameters cannot be used if the Property has no name'
                }
            }
            else {
                if ($isArray) {
                    $obj[$type].schema.items = $result
                }
                else {
                    $obj[$type].schema = $result
                }
            }
        }
    }

    return $obj
}

<#
.SYNOPSIS
Check if an ComponentSchemaJson reference exist.

.DESCRIPTION
Check if an ComponentSchemaJson reference with a given name exist.

.PARAMETER Name
The Name of the ComponentSchemaJson reference.

.NOTES
This is an internal function and may change in future releases of Pode.
#>


function Test-PodeOAComponentSchemaJson {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string[]]
        $DefinitionTag
    )

    foreach ($tag in $DefinitionTag) {
        if (!($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson.keys -ccontains $Name)) {
            # If $Name is not found in the current $tag, return $false
            return $false
        }
    }
    return $true
}

<#
.SYNOPSIS
Tests if a given name exists in the external path keys of OpenAPI definitions for specified definition tags.

.DESCRIPTION
The Test-PodeOAComponentExternalPath function iterates over a list of definition tags and checks if a given name
is present in the external path keys of OpenAPI definitions within the Pode server context. This function is typically
used to validate if a specific component name is already defined in the external paths of the OpenAPI documentation.

.PARAMETER Name
The name of the external path component to be checked within the OpenAPI definitions.

.PARAMETER DefinitionTag
An array of definition tags against which the existence of the name will be checked in the OpenAPI definitions.

.EXAMPLE
$exists = Test-PodeOAComponentExternalPath -Name 'MyComponentName' -DefinitionTag @('tag1', 'tag2')

Checks if 'MyComponentName' exists in the external path keys of OpenAPI definitions for 'tag1' and 'tag2'.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOAComponentExternalPath {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string[]]
        $DefinitionTag
    )

    # Iterate over each definition tag
    foreach ($tag in $DefinitionTag) {
        # Check if the name exists in the external path keys of the current definition tag
        if (!($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.keys -ccontains $Name)) {
            # If the name is not found in the current tag, return false
            return $false
        }
    }
    # If the name exists in all specified tags, return true
    return $true
}


<#
.SYNOPSIS
Converts a property into an OpenAPI 'Of' property structure based on a given definition tag.

.DESCRIPTION
The ConvertTo-PodeOAOfProperty function is used to convert a given property into one of the OpenAPI 'Of' properties:
allOf, oneOf, or anyOf. These structures are used in OpenAPI documentation to define complex types. The function
constructs the appropriate structure based on the type of the property and the definition tag provided.

.PARAMETER Property
A hashtable representing the property to be converted. It should contain the type (allOf, oneOf, or anyOf) and
potentially a list of schemas.

.PARAMETER DefinitionTag
A mandatory string parameter specifying the definition tag in OpenAPI documentation, used for validating components.

.EXAMPLE
$ofProperty = ConvertTo-PodeOAOfProperty -Property $myProperty -DefinitionTag 'myTag'

Converts a given property into an OpenAPI 'Of' structure using the specified definition tag.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAOfProperty {
    param (
        [hashtable]
        $Property,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    # Check if the property type is one of the supported 'Of' types
    if (@('allOf', 'oneOf', 'anyOf') -inotcontains $Property.type) {
        return @{}
    }

    # Initialize the schema with the 'Of' type
    $schema = [ordered]@{
        $Property.type = @()
    }

    # Process each schema defined in the property
    if ($Property.schemas) {
        foreach ($prop in $Property.schemas) {
            if ($prop -is [string]) {
                # Validate the schema component and add a reference to it
                Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $prop -PostValidation
                $schema[$Property.type] += @{ '$ref' = "#/components/schemas/$prop" }
            }
            else {
                # Convert the property to an OpenAPI schema property
                $schema[$Property.type] += $prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag
            }
        }
    }

    # Add discriminator if present
    if ($Property.discriminator) {
        $schema['discriminator'] = $Property.discriminator
    }

    # Return the constructed 'Of' property schema
    return $schema
}


function ConvertTo-PodeOASchemaProperty {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Property,

        [switch]
        $NoDescription,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    if ( @('allof', 'oneof', 'anyof') -icontains $Property.type) {
        $schema = ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $Property
    }
    else {
        # base schema type
        $schema = [ordered]@{ }
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
            if ($Property.type -is [string[]]) {
                throw 'Multi type properties requeired OpenApi Version 3.1 or above'
            }
            $schema['type'] = $Property.type.ToLower()
        }
        else {
            $schema.type = @($Property.type.ToLower())
            if ($Property.nullable) {
                $schema.type += 'null'
            }
        }
    }

    if ($Property.externalDocs) {
        $schema['externalDocs'] = $Property.externalDocs
    }

    if (!$NoDescription -and $Property.description) {
        $schema['description'] = $Property.description
    }

    if ($Property.default) {
        $schema['default'] = $Property.default
    }

    if ($Property.deprecated) {
        $schema['deprecated'] = $Property.deprecated
    }
    if ($Property.nullable -and (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag )) {
        $schema['nullable'] = $Property.nullable
    }

    if ($Property.writeOnly) {
        $schema['writeOnly'] = $Property.writeOnly
    }

    if ($Property.readOnly) {
        $schema['readOnly'] = $Property.readOnly
    }

    if ($Property.example) {
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
            $schema['example'] = $Property.example
        }
        else {
            if ($Property.example -is [Array]) {
                $schema['examples'] = $Property.example
            }
            else {
                $schema['examples'] = @( $Property.example)
            }
        }
    }
    if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
        if ($Property.minimum) {
            $schema['minimum'] = $Property.minimum
        }

        if ($Property.maximum) {
            $schema['maximum'] = $Property.maximum
        }

        if ($Property.exclusiveMaximum) {
            $schema['exclusiveMaximum'] = $Property.exclusiveMaximum
        }

        if ($Property.exclusiveMinimum) {
            $schema['exclusiveMinimum'] = $Property.exclusiveMinimum
        }
    }
    else {
        if ($Property.maximum) {
            if ($Property.exclusiveMaximum) {
                $schema['exclusiveMaximum'] = $Property.maximum
            }
            else {
                $schema['maximum'] = $Property.maximum
            }
        }
        if ($Property.minimum) {
            if ($Property.exclusiveMinimum) {
                $schema['exclusiveMinimum'] = $Property.minimum
            }
            else {
                $schema['minimum'] = $Property.minimum
            }
        }
    }
    if ($Property.multipleOf) {
        $schema['multipleOf'] = $Property.multipleOf
    }

    if ($Property.pattern) {
        $schema['pattern'] = $Property.pattern
    }

    if ($Property.minLength) {
        $schema['minLength'] = $Property.minLength
    }

    if ($Property.maxLength) {
        $schema['maxLength'] = $Property.maxLength
    }

    if ($Property.xml ) {
        $schema['xml'] = $Property.xml
    }

    if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) {
        if ($Property.ContentMediaType) {
            $schema['contentMediaType'] = $Property.ContentMediaType
        }
        if ($Property.ContentEncoding) {
            $schema['contentEncoding'] = $Property.ContentEncoding
        }
    }

    # are we using an array?
    if ($Property.array) {
        if ($Property.maxItems ) {
            $schema['maxItems'] = $Property.maxItems
        }

        if ($Property.minItems ) {
            $schema['minItems'] = $Property.minItems
        }

        if ($Property.uniqueItems ) {
            $schema['uniqueItems'] = $Property.uniqueItems
        }

        $schema['type'] = 'array'
        if ($Property.type -ieq 'schema') {
            Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation
            $schema['items'] = @{ '$ref' = "#/components/schemas/$($Property['schema'])" }
        }
        else {
            $Property.array = $false
            if ($Property.xml) {
                $xmlFromProperties = $Property.xml
                $Property.Remove('xml')
            }
            $schema['items'] = ($Property | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
            $Property.array = $true
            if ($xmlFromProperties) {
                $Property.xml = $xmlFromProperties
            }

            if ($Property.xmlItemName) {
                $schema.items.xml = @{'name' = $Property.xmlItemName }
            }
        }
        return $schema
    }
    else {
        #format is not applicable to array
        if ($Property.format) {
            $schema['format'] = $Property.format
        }

        # schema refs
        if ($Property.type -ieq 'schema') {
            Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation
            $schema = @{
                '$ref' = "#/components/schemas/$($Property['schema'])"
            }
        }
        #only if it's not an array
        if ($Property.enum ) {
            $schema['enum'] = $Property.enum
        }
    }

    if ($Property.object) {
        # are we using an object?
        $Property.object = $false

        $schema = @{
            type       = 'object'
            properties = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property)
        }
        $Property.object = $true
        if ($Property.required) {
            $schema['required'] = @($Property.name)
        }
    }

    if ($Property.type -ieq 'object') {
        foreach ($prop in $Property.properties) {
            if ( @('allOf', 'oneOf', 'anyOf') -icontains $prop.type) {
                switch ($prop.type.ToLower()) {
                    'allof' { $prop.type = 'allOf' }
                    'oneof' { $prop.type = 'oneOf' }
                    'anyof' { $prop.type = 'anyOf' }
                }
                $schema += ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $prop

            }
        }
        if ($Property.properties) {
            $schema['properties'] = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property.properties)
            $RequiredList = @(($Property.properties | Where-Object { $_.required }) )
            if ( $RequiredList.Count -gt 0) {
                $schema['required'] = @($RequiredList.name)
            }
        }
        else {
            #if noproperties parameter create an empty properties
            if ( $Property.properties.Count -eq 1 -and $null -eq $Property.properties[0]) {
                $schema['properties'] = @{}
            }
        }


        if ($Property.minProperties) {
            $schema['minProperties'] = $Property.minProperties
        }

        if ($Property.maxProperties) {
            $schema['maxProperties'] = $Property.maxProperties
        }

        if ($Property.additionalProperties) {
            $schema['additionalProperties'] = $Property.additionalProperties
        }

        if ($Property.discriminator) {
            $schema['discriminator'] = $Property.discriminator
        }
    }

    return $schema

}

<#
.SYNOPSIS
Converts a collection of properties into an OpenAPI schema object format.

.DESCRIPTION
The ConvertTo-PodeOASchemaObjectProperty function takes an array of property hashtables and converts them into
a format suitable for OpenAPI schema objects. It specifically processes properties that are not 'allOf', 'oneOf',
or 'anyOf' types, using the ConvertTo-PodeOASchemaProperty function for conversion based on a given definition tag.

.PARAMETER Properties
An array of hashtables representing properties to be converted. Each hashtable should contain the property's details.

.PARAMETER DefinitionTag
A string representing the definition tag to be used in the conversion process. This tag is crucial for correctly
formatting the properties according to OpenAPI specifications.

.EXAMPLE
$schemaObject = ConvertTo-PodeOASchemaObjectProperty -Properties $myProperties -DefinitionTag 'myTag'

Converts an array of property hashtables into an OpenAPI schema object using the definition tag 'myTag'.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOASchemaObjectProperty {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Properties,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    # Initialize an empty hashtable for the schema
    $schema = @{}

    # Iterate over each property and convert to OpenAPI schema property if applicable
    foreach ($prop in $Properties) {
        # Exclude properties of type 'allOf', 'oneOf', or 'anyOf'
        if (@('allOf', 'oneOf', 'anyOf') -inotcontains $prop.type) {
            # Convert the property to an OpenAPI schema property and add to the schema hashtable
            $schema[$prop.name] = ($prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
        }
    }

    # Return the constructed schema object
    return $schema
}

<#
.SYNOPSIS
Sets OpenAPI specifications for a given route.

.DESCRIPTION
The Set-PodeOpenApiRouteValue function processes and sets various OpenAPI specifications for a given route based on the provided definition tag.
It handles route attributes such as deprecated status, tags, summary, description, operation ID, parameters, request body, callbacks, authentication,
and responses to build a complete OpenAPI specification for the route.

.PARAMETER Route
A hashtable representing the route for which OpenAPI specifications are being set.

.PARAMETER DefinitionTag
A string representing the definition tag used for specifying OpenAPI documentation details for the route.

.EXAMPLE
$routeValues = Set-PodeOpenApiRouteValue -Route $route -DefinitionTag 'myTag'

Sets OpenAPI specifications for the given route using the definition tag 'myTag'.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOpenApiRouteValue {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Route,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )
    # Initialize an ordered hashtable to store route properties
    $pm = [ordered]@{}

    # Process various OpenAPI attributes for the route
    if ($Route.OpenApi.Deprecated) {
        $pm.deprecated = $Route.OpenApi.Deprecated
    }
    if ($Route.OpenApi.Tags) {
        $pm.tags = $Route.OpenApi.Tags
    }
    if ($Route.OpenApi.Summary) {
        $pm.summary = $Route.OpenApi.Summary
    }
    if ($Route.OpenApi.Description) {
        $pm.description = $Route.OpenApi.Description
    }
    if ($Route.OpenApi.OperationId) {
        $pm.operationId = $Route.OpenApi.OperationId
    }
    if ($Route.OpenApi.Parameters) {
        $pm.parameters = $Route.OpenApi.Parameters
    }
    if ($Route.OpenApi.RequestBody.$DefinitionTag) {
        $pm.requestBody = $Route.OpenApi.RequestBody.$DefinitionTag
    }
    if ($Route.OpenApi.CallBacks.$DefinitionTag) {
        $pm.callbacks = $Route.OpenApi.CallBacks.$DefinitionTag
    }
    if ($Route.OpenApi.Servers) {
        $pm.servers = $Route.OpenApi.Servers
    }
    if ($Route.OpenApi.Authentication.Count -gt 0) {
        $pm.security = @()
        foreach ($sct in (Expand-PodeAuthMerge -Names $Route.OpenApi.Authentication.Keys)) {
            if ($PodeContext.Server.Authentications.Methods.$sct.Scheme.Scheme -ieq 'oauth2') {
                if ($Route.AccessMeta.Scope ) {
                    $sctValue = $Route.AccessMeta.Scope
                }
                else {
                    #if scope is empty means 'any role' => assign an empty array
                    $sctValue = @()
                }
                $pm.security += @{ $sct = $sctValue }
            }
            elseif ($sct -eq '%_allowanon_%') {
                #allow anonymous access
                $pm.security += @{}
            }
            else {
                $pm.security += @{$sct = @() }
            }
        }
    }
    if ($Route.OpenApi.Responses.$DefinitionTag ) {
        $pm.responses = $Route.OpenApi.Responses.$DefinitionTag
    }
    else {
        # Set responses or default to '204 No Content' if not specified
        $pm.responses = @{'204' = @{'description' = (Get-PodeStatusDescription -StatusCode 204) } }
    }
    # Return the processed route properties
    return $pm
}


<#
.SYNOPSIS
Generates an internal OpenAPI definition based on the current Pode server context and specific parameters.

.DESCRIPTION
This function constructs an OpenAPI definition by gathering metadata, route information, and API structure based on the provided parameters.
It supports customization of the API documentation through MetaInfo and directly influences the output by including specific server, authentication, and endpoint details.

.PARAMETER EndpointName
The name of the endpoint for which the OpenAPI definition is generated.

.PARAMETER MetaInfo
A hashtable containing metadata for the OpenAPI definition such as the API title, version, and description.

.PARAMETER DefinitionTag
Mandatory. A tag that identifies the specific OpenAPI definition to be generated or manipulated.

.OUTPUTS
Ordered dictionary representing the OpenAPI definition, which can be further processed into JSON or YAML format.

.EXAMPLE
$metaInfo = @{
Title = "My API";
Version = "v1";
Description = "This is my API description."
}
Get-PodeOpenApiDefinitionInternal -Protocol 'HTTPS' -Address 'myapi.example.com' -EndpointName 'MyAPI' -MetaInfo $metaInfo -DefinitionTag 'MyTag'

.NOTES
This is an internal function and may change in future releases of Pode.
#>

function Get-PodeOpenApiDefinitionInternal {
    param(

        [string]
        $EndpointName,

        [hashtable]
        $MetaInfo,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )


    $Definition = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag]

    if (!$Definition.Version) {
        throw 'OpenApi openapi field is required'
    }
    $localEndpoint = $null
    # set the openapi version
    $def = [ordered]@{
        openapi = $Definition.Version
    }

    if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) {
        $def['jsonSchemaDialect'] = 'https://spec.openapis.org/oas/3.1/dialect/base'
    }

    if ($Definition.info) {
        $def['info'] = $Definition.info
    }

    #overwite default values
    if ($MetaInfo.Title) {
        $def.info.title = $MetaInfo.Title
    }

    if ($MetaInfo.Version) {
        $def.info.version = $MetaInfo.Version
    }

    if ($MetaInfo.Description) {
        $def.info.description = $MetaInfo.Description
    }

    if ($Definition.externalDocs) {
        $def['externalDocs'] = $Definition.externalDocs
    }

    if ($Definition.servers) {
        $def['servers'] = $Definition.servers
        if ($Definition.servers.Count -eq 1 -and $Definition.servers[0].url.StartsWith('/')) {
            $localEndpoint = $Definition.servers[0].url
        }
    }
    elseif (!$MetaInfo.RestrictRoutes -and ($PodeContext.Server.Endpoints.Count -gt 1)) {
        #$def['servers'] = $null
        $def.servers = @(foreach ($endpoint in $PodeContext.Server.Endpoints.Values) {
                @{
                    url         = $endpoint.Url
                    description = (Protect-PodeValue -Value $endpoint.Description -Default $endpoint.Name)
                }
            })
    }
    if ($Definition.tags.Count -gt 0) {
        $def['tags'] = @($Definition.tags.Values)
    }

    # paths
    $def['paths'] = [ordered]@{}
    if ($Definition.webhooks.count -gt 0) {
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
            throw 'Feature webhooks is unsupported in OpenAPI v3.0.x'
        }
        else {
            $keys = [string[]]$Definition.webhooks.Keys
            foreach ($key in $keys) {
                if ($Definition.webhooks[$key].NotPrepared) {
                    $Definition.webhooks[$key] = @{
                        $Definition.webhooks[$key].Method = Set-PodeOpenApiRouteValue -Route $Definition.webhooks[$key] -DefinitionTag $DefinitionTag
                    }
                }
            }
            $def['webhooks'] = $Definition.webhooks
        }
    }
    # components
    $def['components'] = [ordered]@{}#$Definition.components
    $components = $Definition.components

    if ($components.schemas.count -gt 0) {
        $def['components'].schemas = $components.schemas
    }
    if ($components.responses.count -gt 0) {
        $def['components'].responses = $components.responses
    }
    if ($components.parameters.count -gt 0) {
        $def['components'].parameters = $components.parameters
    }
    if ($components.examples.count -gt 0) {
        $def['components'].examples = $components.examples
    }
    if ($components.requestBodies.count -gt 0) {
        $def['components'].requestBodies = $components.requestBodies
    }
    if ($components.headers.count -gt 0) {
        $def['components'].headers = $components.headers
    }
    if ($components.securitySchemes.count -gt 0) {
        $def['components'].securitySchemes = $components.securitySchemes
    }
    if ($components.links.count -gt 0) {
        $def['components'].links = $components.links
    }
    if ($components.callbacks.count -gt 0) {
        $def['components'].callbacks = $components.callbacks
    }
    if ($components.pathItems.count -gt 0) {
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
            throw 'Feature pathItems is unsupported in OpenAPI v3.0.x'
        }
        else {
            $keys = [string[]]$components.pathItems.Keys
            foreach ($key in $keys) {
                if ($components.pathItems[$key].NotPrepared) {
                    $components.pathItems[$key] = @{
                        $components.pathItems[$key].Method = Set-PodeOpenApiRouteValue -Route $components.pathItems[$key] -DefinitionTag $DefinitionTag
                    }
                }
            }
            $def['components'].pathItems = $components.pathItems
        }
    }

    # auth/security components
    if ($PodeContext.Server.Authentications.Methods.Count -gt 0) {
        #if ($null -eq $def.components.securitySchemes) {
        # $def.components.securitySchemes = @{}
        # }
        $authNames = (Expand-PodeAuthMerge -Names $PodeContext.Server.Authentications.Methods.Keys)

        foreach ($authName in $authNames) {
            $authType = (Find-PodeAuth -Name $authName).Scheme
            $_authName = ($authName -replace '\s+', '')

            $_authObj = @{}

            if ($authType.Scheme -ieq 'apikey') {
                $_authObj = [ordered]@{
                    type = $authType.Scheme
                    in   = $authType.Arguments.Location.ToLowerInvariant()
                    name = $authType.Arguments.LocationName
                }
                if ($authType.Arguments.Description) {
                    $_authObj.description = $authType.Arguments.Description
                }
            }
            elseif ($authType.Scheme -ieq 'oauth2') {
                if ($authType.Arguments.Urls.Token -and $authType.Arguments.Urls.Authorise) {
                    $oAuthFlow = 'authorizationCode'
                }
                elseif ($authType.Arguments.Urls.Token ) {
                    if ($null -ne $authType.InnerScheme) {
                        if ($authType.InnerScheme.Name -ieq 'basic' -or $authType.InnerScheme.Name -ieq 'form') {
                            $oAuthFlow = 'password'
                        }
                        else {
                            $oAuthFlow = 'implicit'
                        }
                    }
                }
                $_authObj = [ordered]@{
                    type = $authType.Scheme
                }
                if ($authType.Arguments.Description) {
                    $_authObj.description = $authType.Arguments.Description
                }
                $_authObj.flows = @{
                    $oAuthFlow = [ordered]@{
                    }
                }
                if ($authType.Arguments.Urls.Token) {
                    $_authObj.flows.$oAuthFlow.tokenUrl = $authType.Arguments.Urls.Token
                }

                if ($authType.Arguments.Urls.Authorise) {
                    $_authObj.flows.$oAuthFlow.authorizationUrl = $authType.Arguments.Urls.Authorise
                }
                if ($authType.Arguments.Urls.Refresh) {
                    $_authObj.flows.$oAuthFlow.refreshUrl = $authType.Arguments.Urls.Refresh
                }

                $_authObj.flows.$oAuthFlow.scopes = @{}
                if ($authType.Arguments.Scopes ) {
                    foreach ($scope in $authType.Arguments.Scopes) {
                        if ($PodeContext.Server.Authorisations.Methods.ContainsKey($scope) -and $PodeContext.Server.Authorisations.Methods[$scope].Scheme.Type -ieq 'Scope' -and $PodeContext.Server.Authorisations.Methods[$scope].Description) {
                            $_authObj.flows.$oAuthFlow.scopes[$scope] = $PodeContext.Server.Authorisations.Methods[$scope].Description
                        }
                        else {
                            $_authObj.flows.$oAuthFlow.scopes[$scope] = 'No description.'
                        }
                    }
                }
            }
            else {
                $_authObj = @{
                    type   = $authType.Scheme.ToLowerInvariant()
                    scheme = $authType.Name.ToLowerInvariant()
                }
                if ($authType.Arguments.Description) {
                    $_authObj.description = $authType.Arguments.Description
                }
            }
            if (!$def.components.securitySchemes) {
                $def.components.securitySchemes = [ordered]@{}
            }
            $def.components.securitySchemes[$_authName] = $_authObj
        }

        if ($Definition.Security.Definition -and $Definition.Security.Definition.Length -gt 0) {
            $def['security'] = @($Definition.Security.Definition)
        }
    }

    if ($MetaInfo.RouteFilter) {
        $filter = "^$($MetaInfo.RouteFilter)"
    }
    else {
        $filter = ''
    }

    foreach ($method in $PodeContext.Server.Routes.Keys) {
        foreach ($path in ($PodeContext.Server.Routes[$method].Keys | Sort-Object)) {
            # does it match the route?
            if ($path -inotmatch $filter) {
                continue
            }
            # the current route
            $_routes = @($PodeContext.Server.Routes[$method][$path])
            if ( $MetaInfo -and $MetaInfo.RestrictRoutes) {
                $_routes = @(Get-PodeRouteByUrl -Routes $_routes -EndpointName $EndpointName)
            }

            # continue if no routes
            if (($_routes.Length -eq 0) -or ($null -eq $_routes[0])) {
                continue
            }

            # get the first route for base definition
            $_route = $_routes[0]
            # check if the route has to be published
            if (($_route.OpenApi.Swagger -and $_route.OpenApi.DefinitionTag -contains $DefinitionTag ) -or $Definition.hiddenComponents.enableMinimalDefinitions) {

                #remove the ServerUrl part
                if ( $localEndpoint) {
                    $_route.OpenApi.Path = $_route.OpenApi.Path.replace($localEndpoint, '')
                }
                # do nothing if it has no responses set
                if ($_route.OpenApi.Responses.Count -eq 0) {
                    continue
                }

                # add path to defintion
                if ($null -eq $def.paths[$_route.OpenApi.Path]) {
                    $def.paths[$_route.OpenApi.Path] = @{}
                }
                # add path's http method to defintition

                $pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag
                $def.paths[$_route.OpenApi.Path][$method] = $pm

                # add any custom server endpoints for route
                foreach ($_route in $_routes) {

                    if ($_route.OpenApi.Servers.count -gt 0) {
                        if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
                            $def.paths[$_route.OpenApi.Path][$method].servers = @()
                        }
                        if ($localEndpoint) {
                            $def.paths[$_route.OpenApi.Path][$method].servers += $Definition.servers[0]
                        }
                    }
                    if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Address) -and ($_route.Endpoint.Address -ine '*:*')) {

                        if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
                            $def.paths[$_route.OpenApi.Path][$method].servers = @()
                        }

                        $serverDef = $null
                        if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Name)) {
                            $serverDef = @{
                                url = (Get-PodeEndpointByName -Name $_route.Endpoint.Name).Url
                            }
                        }
                        else {
                            $serverDef = @{
                                url = "$($_route.Endpoint.Protocol)://$($_route.Endpoint.Address)"
                            }
                        }

                        if ($null -ne $serverDef) {
                            $def.paths[$_route.OpenApi.Path][$method].servers += $serverDef
                        }
                    }
                }
            }
        }
    }

    #deal with the external OpenAPI paths
    if ( $Definition.hiddenComponents.externalPath) {
        foreach ($extPath in $Definition.hiddenComponents.externalPath.values) {
            foreach ($method in $extPath.keys) {
                $_route = $extPath[$method]
                if (! ( $def.paths.keys -ccontains $_route.Path)) {
                    $def.paths[$_route.OpenAPI.Path] = @{}
                }
                $pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag
                # add path's http method to defintition
                $def.paths[$_route.OpenAPI.Path][$method.ToLower()] = $pmF
            }
        }
    }
    return $def
}

function ConvertTo-PodeOAPropertyFromCmdletParameter {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Management.Automation.ParameterMetadata]
        $Parameter
    )

    if ($Parameter.SwitchParameter -or ($Parameter.ParameterType.Name -ieq 'boolean')) {
        New-PodeOABoolProperty -Name $Parameter.Name
    }
    else {
        switch ($Parameter.ParameterType.Name) {
            { @('int32', 'int64') -icontains $_ } {
                New-PodeOAIntProperty -Name $Parameter.Name -Format $_
            }

            { @('double', 'float') -icontains $_ } {
                New-PodeOANumberProperty -Name $Parameter.Name -Format $_
            }
        }
    }

    New-PodeOAStringProperty -Name $Parameter.Name
}


<#
.SYNOPSIS
Creates a base OpenAPI object structure.

.DESCRIPTION
The Get-PodeOABaseObject function generates a foundational structure for an OpenAPI object.
This structure includes empty ordered dictionaries for info, paths, webhooks, components, and other OpenAPI elements.
It is used as a base template for building OpenAPI documentation in the Pode framework.

.OUTPUTS
Hashtable
Returns a hashtable representing the base structure of an OpenAPI object.

.EXAMPLE
$baseObject = Get-PodeOABaseObject

This example creates a base OpenAPI object structure.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Get-PodeOABaseObject {
    # Returns a base template for an OpenAPI object
    return @{
        info             = [ordered]@{}
        Path             = $null
        webhooks         = [ordered]@{}
        components       = [ordered]@{
            schemas         = [ordered]@{}
            responses       = [ordered]@{}
            parameters      = [ordered]@{}
            examples        = [ordered]@{}
            requestBodies   = [ordered]@{}
            headers         = [ordered]@{}
            securitySchemes = [ordered]@{}
            links           = [ordered]@{}
            callbacks       = [ordered]@{}
            pathItems       = [ordered]@{}
        }
        Security         = @()
        tags             = [ordered]@{}
        hiddenComponents = @{
            enabled          = $false
            schemaValidation = $false
            version          = 3.0
            depth            = 20
            schemaJson       = @{}
            viewer           = @{}
            postValidation   = @{
                schemas         = @{}
                responses       = @{}
                parameters      = @{}
                examples        = @{}
                requestBodies   = @{}
                headers         = @{}
                securitySchemes = @{}
                links           = @{}
                callbacks       = @{}
                pathItems       = @{}
            }
            externalPath     = [ordered]@{}
            defaultResponses = @{
                '200'     = @{ description = 'OK' }
                'default' = @{ description = 'Internal server error' }
            }
            operationId      = @()
        }
    }
}

<#
.SYNOPSIS
Initializes a table to manage OpenAPI definitions.

.DESCRIPTION
The Initialize-PodeOpenApiTable function creates a table to manage OpenAPI definitions within the Pode framework.
It sets up a default definition tag and initializes a dictionary to hold OpenAPI definitions for each tag.
The function is essential for managing OpenAPI documentation across different parts of the application.

.PARAMETER DefaultDefinitionTag
An optional parameter to set the default OpenAPI definition tag. If not provided, 'default' is used.

.OUTPUTS
Hashtable
Returns a hashtable for managing OpenAPI definitions.

.EXAMPLE
$openApiTable = Initialize-PodeOpenApiTable -DefaultDefinitionTag 'api-v1'

Initializes the OpenAPI table with 'api-v1' as the default definition tag.

.EXAMPLE
$openApiTable = Initialize-PodeOpenApiTable

Initializes the OpenAPI table with 'default' as the default definition tag.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Initialize-PodeOpenApiTable {
    param(
        [string]
        $DefaultDefinitionTag = $null
    )
    # Initialization of the OpenAPI table with default settings
    $OpenAPI = @{
        DefinitionTagSelectionStack = New-Object 'System.Collections.Generic.Stack[System.Object]'
    }
    # Set the default definition tag
    if ([string]::IsNullOrEmpty($DefaultDefinitionTag)) {
        $OpenAPI['DefaultDefinitionTag'] = 'default'
    }
    else {
        $OpenAPI['DefaultDefinitionTag'] = $DefaultDefinitionTag
    }

    # Set the currently selected definition tag
    $OpenAPI['SelectedDefinitionTag'] = $OpenAPI['DefaultDefinitionTag']

    # Initialize the Definitions dictionary with a base OpenAPI object for the selected definition tag
    $OpenAPI['Definitions'] = @{ $OpenAPI['SelectedDefinitionTag'] = Get-PodeOABaseObject }

    # Return the initialized OpenAPI table
    return $OpenAPI
}

<#
.SYNOPSIS
Sets authentication methods for specific routes in OpenAPI documentation.

.DESCRIPTION
The Set-PodeOAAuth function assigns specified authentication methods to given routes for OpenAPI documentation.
It supports setting multiple authentication methods and optionally allows anonymous access.
The function validates the existence of the authentication methods before applying them to the routes.

.PARAMETER Route
An array of hashtables representing the routes to which the authentication methods will be applied.
Each route should contain an OpenApi key for updating OpenAPI documentation.

.PARAMETER Name
An array of names of the authentication methods to be applied to the routes.
These methods should already be defined in the Pode framework.

.PARAMETER AllowAnon
A switch parameter that, if set, allows anonymous access in addition to the specified authentication methods.

.EXAMPLE
Set-PodeOAAuth -Route $myRoute -Name @('BasicAuth', 'ApiKeyAuth') -AllowAnon

Applies 'BasicAuth' and 'ApiKeyAuth' authentication methods to the specified route and allows anonymous access.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOAAuth {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [string[]]
        $Name,

        [switch]
        $AllowAnon
    )
    begin {
        # Validate the existence of specified authentication methods
        foreach ($n in @($Name)) {
            if (!(Test-PodeAuthExists -Name $n)) {
                throw "Authentication method does not exist: $($n)"
            }
        }
    }

    process {
        # Iterate over each route to set authentication
        foreach ($r in @($Route)) {
            #exclude static route
            if ($r.Method -ne 'Static') {
                # Set the authentication methods for the route
                $r.OpenApi.Authentication = @(foreach ($n in @($Name)) {
                        @{
                            "$($n -replace '\s+', '')" = @() # Clean up auth name and initialize empty scopes
                        }
                    })
                # Add anonymous access if allowed
                if ($AllowAnon) {
                    $r.OpenApi.Authentication += @{'%_allowanon_%' = '' }
                }
            }
        }
    }
}


<#
.SYNOPSIS
Sets global authentication methods for specified OpenAPI definitions in the Pode framework.

.DESCRIPTION
The Set-PodeOAGlobalAuth function is used to apply authentication methods globally to specified OpenAPI definitions.
It verifies the existence of the authentication methods and then updates the OpenAPI definitions with these methods,
associating them with specific routes.

.PARAMETER Name
The name of the authentication method to apply. This method should already be defined in the Pode framework.

.PARAMETER Route
The route to which the authentication method is to be applied.

.PARAMETER DefinitionTag
An array of definition tags specifying the OpenAPI definitions to which the authentication method should be applied.

.EXAMPLE
Set-PodeOAGlobalAuth -Name 'BasicAuth' -Route '/api/*' -DefinitionTag @('tag1', 'tag2')

Applies 'BasicAuth' authentication method to all routes under '/api/*' in the OpenAPI definitions tagged with 'tag1' and 'tag2'.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOAGlobalAuth {
    param(
        [string]
        $Name,

        [string]
        $Route,

        [Parameter(Mandatory = $true)]
        [string[]]
        $DefinitionTag
    )

    # Check if the specified authentication method exists
    if (!(Test-PodeAuthExists -Name $Name)) {
        throw "Authentication method does not exist: $($Name)"
    }

    # Iterate over each definition tag to apply the authentication method
    foreach ($tag in $DefinitionTag) {
        # Initialize security array if it's empty
        if (Test-PodeIsEmpty $PodeContext.Server.OpenAPI.Definitions[$tag].Security) {
            $PodeContext.Server.OpenAPI.Definitions[$tag].Security = @()
        }

        # Apply authentication to each expanded auth name
        foreach ($authName in (Expand-PodeAuthMerge -Names $Name)) {
            $authType = Get-PodeAuth $authName

            # Determine the scopes of the authentication
            if ($authType.Scheme.Arguments.Scopes) {
                $Scopes = @($authType.Scheme.Arguments.Scopes)
            }
            else {
                $Scopes = @()
            }

            # Update the OpenAPI definition with the authentication information
            $PodeContext.Server.OpenAPI.Definitions[$tag].Security += @{
                Definition = @{ "$($authName -replace '\s+', '')" = $Scopes }
                Route      = (ConvertTo-PodeRouteRegex -Path $Route)
            }
        }
    }
}

function Resolve-PodeOAReference {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $ComponentSchema,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    begin {
        $Schemas = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson
        $Keys = @()
    }

    process {
        if ($ComponentSchema.properties) {
            foreach ($item in $ComponentSchema.properties.Keys) {
                $Keys += $item
            }
        }
        foreach ($item in $ComponentSchema.Keys) {
            if ( @('allof', 'oneof', 'anyof') -icontains $item ) {
                $Keys += $item
            }
        }

        foreach ($key in $Keys) {
            if ( @('allof', 'oneof', 'anyof') -icontains $key ) {
                if ($key -ieq 'allof') {
                    $tmpProp = @()
                    foreach ( $comp in $ComponentSchema[$key] ) {
                        if ($comp.'$ref') {
                            if (($comp.'$ref').StartsWith('#/components/schemas/')) {
                                $refName = ($comp.'$ref') -replace '#/components/schemas/', ''
                                if ($Schemas.ContainsKey($refName)) {
                                    $tmpProp += $Schemas[$refName].schema
                                }
                            }
                        }
                        elseif ( $comp.properties) {
                            if ($comp.type -eq 'object') {
                                $tmpProp += Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema$comp
                            }
                            else {
                                throw 'Unsupported object'
                            }
                        }
                    }

                    $ComponentSchema.type = 'object'
                    $ComponentSchema.remove('allOf')
                    if ($tmpProp.count -gt 0) {
                        foreach ($t in $tmpProp) {
                            $ComponentSchema.properties += $t.properties
                        }
                    }

                }
                elseif ($key -ieq 'oneof') {
                    throw 'Validation of schema with oneof is not supported'
                }
                elseif ($key -ieq 'anyof') {
                    throw 'Validation of schema with anyof is not supported'
                }
            }
            elseif ($ComponentSchema.properties[$key].type -eq 'object') {
                $ComponentSchema.properties[$key].properties = Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema $ComponentSchema.properties[$key].properties
            }
            elseif ($ComponentSchema.properties[$key].'$ref') {
                if (($ComponentSchema.properties[$key].'$ref').StartsWith('#/components/schemas/')) {
                    $refName = ($ComponentSchema.properties[$key].'$ref') -replace '#/components/schemas/', ''
                    if ($Schemas.ContainsKey($refName)) {
                        $ComponentSchema.properties[$key] = $Schemas[$refName].schema
                    }
                }
            }
            elseif ($ComponentSchema.properties[$key].items -and $ComponentSchema.properties[$key].items.'$ref' ) {
                if (($ComponentSchema.properties[$key].items.'$ref').StartsWith('#/components/schemas/')) {
                    $refName = ($ComponentSchema.properties[$key].items.'$ref') -replace '#/components/schemas/', ''
                    if ($Schemas.ContainsKey($refName)) {
                        $ComponentSchema.properties[$key].items = $schemas[$refName].schema
                    }
                }
            }
        }
    }

    end {
        return $ComponentSchema
    }
}


<#
.SYNOPSIS
Creates a new OpenAPI property object based on provided parameters.

.DESCRIPTION
The New-PodeOAPropertyInternal function constructs an OpenAPI property object using parameters like type, name,
description, and various other attributes. It is used internally for building OpenAPI documentation elements in the Pode framework.

.PARAMETER Type
The type of the property. This parameter is optional if the type is specified in the Params hashtable.

.PARAMETER Params
A hashtable containing various attributes of the property such as name, description, format, and constraints like
required, readOnly, writeOnly, etc.

.OUTPUTS
System.Collections.Specialized.OrderedDictionary
An ordered dictionary representing the constructed OpenAPI property object.

.EXAMPLE
$property = New-PodeOAPropertyInternal -Type 'string' -Params $myParams

Demonstrates how to create an OpenAPI property object of type 'string' using the specified parameters.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAPropertyInternal {
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [String]
        $Type,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $Params

    )

    # Initialize an ordered dictionary for the property
    $param = [ordered]@{}

    # Set the type of the property
    if ($type) {
        $param.type = $type
    }
    else {
        if ( $Params.type) {
            $param.type = $Params.type
        }
        else {
            throw 'Cannot create the property no type is defined'
        }
    }

    # Set name if provided
    if ($Params.Name) {
        $param.name = $Params.Name
    }

    # Set description if provided
    if ($Params.Description) {
        $param.description = $Params.Description
    }

    # Additional property settings based on provided parameters
    if ($Params.Array.IsPresent) { $param.array = $Params.Array.IsPresent }

    if ($Params.Object.IsPresent) { $param.object = $Params.Object.IsPresent }

    if ($Params.Required.IsPresent) { $param.required = $Params.Required.IsPresent }

    if ($Params.Default) { $param.default = $Params.Default }

    if ($Params.Format) { $param.format = $Params.Format.ToLowerInvariant() }

    if ($Params.Deprecated.IsPresent) { $param.deprecated = $Params.Deprecated.IsPresent }

    if ($Params.Nullable.IsPresent) { $param.nullable = $Params.Nullable.IsPresent }

    if ($Params.WriteOnly.IsPresent) { $param.writeOnly = $Params.WriteOnly.IsPresent }

    if ($Params.ReadOnly.IsPresent) { $param.readOnly = $Params.ReadOnly.IsPresent }

    if ($Params.Example) { $param.example = $Params.Example }

    if ($Params.UniqueItems.IsPresent) { $param.uniqueItems = $Params.UniqueItems.IsPresent }

    if ($Params.MaxItems) { $param.maxItems = $Params.MaxItems }

    if ($Params.MinItems) { $param.minItems = $Params.MinItems }

    if ($Params.Enum) { $param.enum = $Params.Enum }

    if ($Params.Minimum) { $param.minimum = $Params.Minimum }

    if ($Params.Maximum) { $param.maximum = $Params.Maximum }

    if ($Params.ExclusiveMaximum.IsPresent) { $param.exclusiveMaximum = $Params.ExclusiveMaximum.IsPresent }

    if ($Params.ExclusiveMinimum.IsPresent) { $param.exclusiveMinimum = $Params.ExclusiveMinimum.IsPresent }
    if ($Params.MultiplesOf) { $param.multipleOf = $Params.MultiplesOf }

    if ($Params.Pattern) { $param.pattern = $Params.Pattern }

    if ($Params.MinLength) { $param.minLength = $Params.MinLength }

    if ($Params.MaxLength) { $param.maxLength = $Params.MaxLength }

    if ($Params.MinProperties) { $param.minProperties = $Params.MinProperties }

    if ($Params.MaxProperties) { $param.maxProperties = $Params.MaxProperties }

    if ($Params.XmlName -or $Params.XmlNamespace -or $Params.XmlPrefix -or $Params.XmlAttribute.IsPresent -or $Params.XmlWrapped.IsPresent) {

        $param.xml = @{}

        if ($Params.XmlName) { $param.xml.name = $Params.XmlName }

        if ($Params.XmlNamespace) { $param.xml.namespace = $Params.XmlNamespace }

        if ($Params.XmlPrefix) { $param.xml.prefix = $Params.XmlPrefix }

        if ($Params.XmlAttribute.IsPresent) { $param.xml.attribute = $Params.XmlAttribute.IsPresent }

        if ($Params.XmlWrapped.IsPresent) { $param.xml.wrapped = $Params.XmlWrapped.IsPresent }
    }

    if ($Params.XmlItemName) { $param.xmlItemName = $Params.XmlItemName }

    if ($Params.ExternalDocs) { $param.externalDocs = $Params.ExternalDocs }

    if ($Params.NoAdditionalProperties.IsPresent -and $Params.AdditionalProperties) {
        throw 'Params -NoAdditionalProperties and -AdditionalProperties are mutually exclusive'
    }
    else {
        if ($Params.NoAdditionalProperties.IsPresent) { $param.additionalProperties = $false }

        if ($Params.AdditionalProperties) { $param.additionalProperties = $Params.AdditionalProperties }
    }

    return $param
}


<#
.SYNOPSIS
Converts header properties to a format compliant with OpenAPI specifications.

.DESCRIPTION
The ConvertTo-PodeOAHeaderProperty function is designed to take an array of hashtables representing header properties and
convert them into a structure suitable for OpenAPI documentation. It ensures that each header property includes a name and
schema definition and can handle additional attributes like description.

.PARAMETER Headers
An array of hashtables, where each hashtable represents a header property with attributes like name, type, description, etc.

.EXAMPLE
$headerProperties = ConvertTo-PodeOAHeaderProperty -Headers $myHeaders

This example demonstrates how to convert an array of header properties into a format suitable for OpenAPI documentation.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAHeaderProperty {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable[]]
        $Headers
    )

    $elems = @{}

    foreach ($e in $Headers) {
        # Ensure each header has a name
        if ($e.name) {
            $elems.$($e.name) = @{}
            # Add description if present
            if ($e.description) {
                $elems.$($e.name).description = $e.description
            }
            # Define the schema, including the type and any additional properties
            $elems.$($e.name).schema = @{
                type = $($e.type)
            }
            foreach ($k in $e.keys) {
                if (@('name', 'description') -notcontains $k) {
                    $elems.$($e.name).schema.$k = $e.$k
                }
            }
        }
        else {
            throw 'Header requires a name when used in an encoding context'
        }
    }

    return $elems
}


<#
.SYNOPSIS
Creates a new OpenAPI callback component for a given definition tag.

.DESCRIPTION
The New-PodeOAComponentCallBackInternal function constructs an OpenAPI callback component based on provided parameters.
This function is designed for internal use within the Pode framework to define callbacks in OpenAPI documentation.
It handles the creation of callback structures including the path, HTTP method, request bodies, and responses
based on the given definition tag.

.PARAMETER Params
A hashtable containing parameters for the callback component, such as Method, Path, RequestBody, and Responses.

.PARAMETER DefinitionTag
A mandatory string parameter that specifies the definition tag in OpenAPI documentation.

.EXAMPLE
$callback = New-PodeOAComponentCallBackInternal -Params $myParams -DefinitionTag 'myTag'

This example demonstrates how to create an OpenAPI callback component for 'myTag' using the provided parameters.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAComponentCallBackInternal {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Params,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    # Convert HTTP method to lower case
    $_method = $Params.Method.ToLower()

    # Construct the base structure for the callback with the given path and method
    $callBack = [ordered]@{
        "'$($Params.Path)'" = [ordered]@{
            $_method = [ordered]@{}
        }
    }

    # Add request body to the callback if it is specified for the given definition tag
    if ($Params.RequestBody.ContainsKey($DefinitionTag)) {
        $callBack."'$($Params.Path)'".$_method.requestBody = $Params.RequestBody[$DefinitionTag]
    }

    # Add responses to the callback if they are specified for the given definition tag
    if ($Params.Responses.ContainsKey($DefinitionTag)) {
        $callBack."'$($Params.Path)'".$_method.responses = $Params.Responses[$DefinitionTag]
    }

    # Return the constructed callback object
    return $callBack

}




<#
.SYNOPSIS
Creates a new OpenAPI response object based on provided parameters and a definition tag.

.DESCRIPTION
The New-PodeOResponseInternal function constructs an OpenAPI response object using provided parameters.
It sets a description for the status code, references existing components if specified,
and builds content-type and header schemas. This function is intended for internal use within the
Pode framework for API documentation purposes.

.PARAMETER Params
A hashtable containing parameters for building the OpenAPI response object, including description,
status code, content, headers, links, and reference to existing components.

.PARAMETER DefinitionTag
A mandatory string parameter that specifies the definition tag in OpenAPI documentation.

.EXAMPLE
$response = New-PodeOResponseInternal -Params $myParams -DefinitionTag 'myTag'

This example demonstrates how to create an OpenAPI response object for 'myTag' using the provided parameters.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOResponseInternal {
    param(
        [hashtable]
        $Params,

        [Parameter(Mandatory = $true)]
        [string]
        $DefinitionTag
    )

    # Set a general description for the status code
    if ([string]::IsNullOrWhiteSpace($Params.Description)) {
        if ($Params.Default) {
            $Description = 'Default Response.'
        }
        elseif ($Params.StatusCode) {
            $Description = Get-PodeStatusDescription -StatusCode $Params.StatusCode
        }
        else {
            throw 'A Description is required'
        }
    }
    else {
        $Description = $Params.Description
    }

    # Handle response referencing an existing component
    if ($Params.Reference) {
        Test-PodeOAComponentInternal -Field responses -DefinitionTag $DefinitionTag -Name $Params.Reference -PostValidation
        $response = @{
            '$ref' = "#/components/responses/$($Params.Reference)"
        }
    }
    else {
        # Build content-type schemas if provided
        $_content = $null
        if ($null -ne $Params.Content) {
            $_content = ConvertTo-PodeOAObjectSchema -DefinitionTag $DefinitionTag -Content $Params.Content
        }

        # Build header schemas based on the type of the Headers parameter
        $_headers = $null
        if ($null -ne $Params.Headers) {
            if ($Params.Headers -is [System.Object[]] -or $Params.Headers -is [string] -or $Params.Headers -is [string[]]) {
                if ($Params.Headers -is [System.Object[]] -and $Params.Headers.Count -gt 0 -and ($Params.Headers[0] -is [hashtable] -or $Params.Headers[0] -is [ordered])) {
                    $_headers = ConvertTo-PodeOAHeaderProperty -Headers $Params.Headers
                }
                else {
                    $_headers = @{}
                    foreach ($h in $Params.Headers) {
                        Test-PodeOAComponentInternal -Field headers -DefinitionTag $DefinitionTag -Name $h -PostValidation
                        $_headers[$h] = @{
                            '$ref' = "#/components/headers/$h"
                        }
                    }
                }
            }
            elseif ($Params.Headers -is [hashtable]) {
                $_headers = ConvertTo-PodeOAObjectSchema -DefinitionTag $DefinitionTag -Content $Params.Headers
            }
        }

        # Construct the response object
        $response = [ordered]@{
            description = $Description
        }

        if ($_headers) { $response.headers = $_headers }

        if ($_content) { $response.content = $_content }

        if ($Params.Links) { $response.links = $Params.Links }

    }

    return $response
}




<#
.SYNOPSIS
Creates a new OpenAPI response link object.

.DESCRIPTION
The New-PodeOAResponseLinkInternal function generates an OpenAPI response link object from provided parameters.
This includes setting up descriptions, operation IDs, references, parameters, and request bodies for the link.
This function is designed for internal use within the Pode framework to facilitate the creation of response
link objects in OpenAPI documentation.

.PARAMETER Params
A hashtable of parameters for the OpenAPI response link.

.EXAMPLE
$link = New-PodeOAResponseLinkInternal -Params $myParams

Generates a new OpenAPI response link object using the provided parameters in $myParams.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAResponseLinkInternal {
    param(
        [hashtable]
        $Params
    )

    # Initialize an ordered dictionary for the link
    $link = [ordered]@{}

    # Add properties to the link based on the provided parameters
    if ($Params.Description) { $link.description = $Params.Description }
    if ($Params.OperationId) { $link.operationId = $Params.OperationId }
    if ($Params.OperationRef) { $link.operationRef = $Params.OperationRef }
    if ($Params.Parameters) { $link.parameters = $Params.Parameters }
    if ($Params.RequestBody) { $link.requestBody = $Params.RequestBody }

    return $link
}


<#
.SYNOPSIS
Tests the internal OpenAPI definitions for compliance and validity.

.DESCRIPTION
The Test-PodeOADefinitionInternal function validates OpenAPI definitions within the Pode framework.
It checks for various issues like undefined references, mandatory fields (like title and version),
and missing components. If any issues are found, they are displayed with detailed messages, and
the function throws an error indicating non-compliance with OpenAPI document standards.

.EXAMPLE
Test-PodeOADefinitionInternal

This example demonstrates how to call the function to validate OpenAPI definitions.

.NOTES
This is an internal function and may change in future releases of Pode.
#>

function Test-PodeOADefinitionInternal {

    # Validate OpenAPI definitions and store any issues found
    $definitionIssues = Test-PodeOADefinition

    # Check if the validation result indicates issues
    if (! $definitionIssues.valid) {
        # Print a header for undefined OpenAPI references
        Write-PodeHost 'Undefined OpenAPI References :' -ForegroundColor Red

        # Iterate over each issue found in the definitions
        foreach ($tag in $definitionIssues.issues.keys) {
            Write-PodeHost "Definition $tag :" -ForegroundColor Red

            # Check and display issues related to OpenAPI document generation error
            if ($definitionIssues.issues[$tag].definition ) {
                Write-PodeHost ' OpenAPI generation document error: ' -ForegroundColor Red
                Write-PodeHost " $($definitionIssues.issues[$tag].definition)" -ForegroundColor Red
            }

            # Check for missing mandatory 'title' field
            if ($definitionIssues.issues[$tag].title ) {
                Write-PodeHost ' info.title is mandatory' -ForegroundColor Red
            }

            # Check for missing mandatory 'version' field
            if ($definitionIssues.issues[$tag].version ) {
                Write-PodeHost ' info.version is mandatory' -ForegroundColor Red
            }

            # Check for missing components and list them
            if ($definitionIssues.issues[$tag].components ) {
                Write-PodeHost ' Missing component(s)' -ForegroundColor Red
                foreach ($key in $definitionIssues.issues[$tag].components.keys) {
                    $occurences = $definitionIssues.issues[$tag].components[$key]
                    # Adjust occurrence count based on schema validation setting
                    if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) {
                        $occurences = $occurences / 2
                    }
                    Write-PodeHost "`$refs : $key ($occurences)" -ForegroundColor Red
                }
            }

            # Add a blank line for readability
            Write-PodeHost
        }

        # Throw an error indicating non-compliance with OpenAPI standards
        throw 'OpenAPI document compliance issues'
    }
}

<#
.SYNOPSIS
Check the OpenAPI component exist (Internal Function)

.DESCRIPTION
Check the OpenAPI component exist (Internal Function)

.PARAMETER Field
The component type

.PARAMETER Name
The component Name

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.PARAMETER ThrowException
Generate an exception if the component doesn't exist

.PARAMETER PostValidation
Postpone the check before the server start

.EXAMPLE
Test-PodeOAComponentInternal -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOAComponentInternal {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems')]
        [string]
        $Field,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [string[]]
        $DefinitionTag,

        [switch]
        $ThrowException,

        [switch]
        $PostValidation
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    if ($PostValidation.IsPresent) {
        foreach ($tag in $DefinitionTag) {
            if (! ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field].keys -ccontains $Name)) {
                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name] = 1
            }
            else {
                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name] += 1
            }
        }
    }
    else {
        foreach ($tag in $DefinitionTag) {
            if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field].keys -ccontains $Name)) {
                # If $Name is not found in the current $tag, return $false or throw an exception
                if ($ThrowException.IsPresent ) {
                    throw "No components of type $field named $Name are available in the $tag definition."
                }
                else {
                    return $false
                }
            }
        }
        if (!$ThrowException.IsPresent) {
            return $true
        }
    }
}
src\Private\PodeServer.ps1
using namespace Pode

function Start-PodeWebServer {
    param(
        [switch]
        $Browse
    )

    # setup any inbuilt middleware
    $inbuilt_middleware = @(
        (Get-PodeSecurityMiddleware),
        (Get-PodeAccessMiddleware),
        (Get-PodeLimitMiddleware),
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeQueryMiddleware),
        (Get-PodeCookieMiddleware)
    )

    $PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)

    # work out which endpoints to listen on
    $endpoints = @()
    $endpointsMap = @{}

    @(Get-PodeEndpoints -Type Http, Ws) | ForEach-Object {
        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
        $_ip = Get-PodeIPAddress -IP $_ip -DualMode:($_.DualMode)

        # dual mode?
        $addrs = $_ip
        if ($_.DualMode) {
            $addrs = Resolve-PodeIPDualMode -IP $_ip
        }

        # the endpoint
        $_endpoint = @{
            Name                   = $_.Name
            Key                    = "$($_ip):$($_.Port)"
            Address                = $addrs
            Hostname               = $_.HostName
            IsIPAddress            = $_.IsIPAddress
            Port                   = $_.Port
            Certificate            = $_.Certificate.Raw
            AllowClientCertificate = $_.Certificate.AllowClientCertificate
            Url                    = $_.Url
            Protocol               = $_.Protocol
            Type                   = $_.Type
            Pool                   = $_.Runspace.PoolName
            SslProtocols           = $_.Ssl.Protocols
            DualMode               = $_.DualMode
        }

        # add endpoint to list
        $endpoints += $_endpoint

        # add to map
        if (!$endpointsMap.ContainsKey($_endpoint.Key)) {
            $endpointsMap[$_endpoint.Key] = @{ Type = $_.Type }
        }
        else {
            if ($endpointsMap[$_endpoint.Key].Type -ine $_.Type) {
                $endpointsMap[$_endpoint.Key].Type = 'HttpAndWs'
            }
        }
    }

    # create the listener
    $listener = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)Listener -CancellationToken `$PodeContext.Tokens.Cancellation.Token")))
    $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
    $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
    $listener.RequestTimeout = $PodeContext.Server.Request.Timeout
    $listener.RequestBodySize = $PodeContext.Server.Request.BodySize
    $listener.ShowServerDetails = [bool]$PodeContext.Server.Security.ServerDetails

    try {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            $socket = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket -Name `$_.Name -Address `$_.Address -Port `$_.Port -SslProtocols `$_.SslProtocols -Type `$endpointsMap[`$_.Key].Type -Certificate `$_.Certificate -AllowClientCertificate `$_.AllowClientCertificate -DualMode:`$_.DualMode")))
            $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout

            if (!$_.IsIPAddress) {
                $socket.Hostnames.Add($_.HostName)
            }

            $listener.Add($socket)
        }

        $listener.Start()
        $PodeContext.Listeners += $listener
        $PodeContext.Server.Signals.Enabled = $true
        $PodeContext.Server.Signals.Listener = $listener
        $PodeContext.Server.Http.Listener = $listener
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $listener
        throw $_.Exception
    }

    # only if HTTP endpoint
    if (Test-PodeEndpoints -Type Http) {
        # script for listening out for incoming requests
        $listenScript = {
            param(
                [Parameter(Mandatory = $true)]
                $Listener,

                [Parameter(Mandatory = $true)]
                [int]
                $ThreadId
            )

            try {
                while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                    # get request and response
                    $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))

                    try {
                        try {
                            $Request = $context.Request
                            $Response = $context.Response

                            # reset with basic event data
                            $WebEvent = @{
                                OnEnd            = @()
                                Auth             = @{}
                                Response         = $Response
                                Request          = $Request
                                Lockable         = $PodeContext.Threading.Lockables.Global
                                Path             = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath)
                                Method           = $Request.HttpMethod.ToLowerInvariant()
                                Query            = $null
                                Endpoint         = @{
                                    Protocol = $Request.Url.Scheme
                                    Address  = $Request.Host
                                    Name     = $context.EndpointName
                                }
                                ContentType      = $Request.ContentType
                                ErrorType        = $null
                                Cookies          = @{}
                                PendingCookies   = @{}
                                Parameters       = $null
                                Data             = $null
                                Files            = $null
                                Streamed         = $true
                                Route            = $null
                                StaticContent    = $null
                                Timestamp        = [datetime]::UtcNow
                                TransferEncoding = $null
                                AcceptEncoding   = $null
                                Ranges           = $null
                                Sse              = $null
                                Metadata         = @{}
                            }

                            # if iis, and we have an app path, alter it
                            if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Path.IsNonRoot) {
                                $WebEvent.Path = ($WebEvent.Path -ireplace $PodeContext.Server.IIS.Path.Pattern, '')
                                if ([string]::IsNullOrEmpty($WebEvent.Path)) {
                                    $WebEvent.Path = '/'
                                }
                            }

                            # accept/transfer encoding
                            $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding (Get-PodeHeader -Name 'Transfer-Encoding') -ThrowError)
                            $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError)
                            $WebEvent.Ranges = (Get-PodeRanges -Range (Get-PodeHeader -Name 'Range') -ThrowError)

                            # add logging endware for post-request
                            Add-PodeRequestLogEndware -WebEvent $WebEvent

                            # stop now if the request has an error
                            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 [System.Net.Http.HttpRequestException]::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 [System.Net.Http.HttpRequestException]::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)")
                                }

                                $WebEvent.Sse = @{
                                    Name        = $WebEvent.Request.SseClientName
                                    Group       = $WebEvent.Request.SseClientGroup
                                    ClientId    = $WebEvent.Request.SseClientId
                                    LastEventId = $null
                                    IsLocal     = $false
                                }
                            }

                            # 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
                                }

                                if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
                                    # 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
                                        }
                                        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
                                        }
                                    }
                                    elseif ($null -ne $WebEvent.Route.Logic) {
                                        $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
                                            -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
                                    }
                                }
                            }
                        }
                        catch [System.OperationCanceledException] {}
                        catch [System.Net.Http.HttpRequestException] {
                            if ($Response.StatusCode -ge 500) {
                                $_.Exception | Write-PodeErrorLog -CheckInnerException
                            }

                            $code = [int]($_.Exception.Data['PodeStatusCode'])
                            if ($code -le 0) {
                                $code = 400
                            }

                            Set-PodeResponseStatus -Code $code -Exception $_
                        }
                        catch {
                            $_ | Write-PodeErrorLog
                            $_.Exception | Write-PodeErrorLog -CheckInnerException
                            Set-PodeResponseStatus -Code 500 -Exception $_
                        }
                        finally {
                            Update-PodeServerRequestMetrics -WebEvent $WebEvent
                        }

                        # invoke endware specifc to the current web event
                        $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
                        Invoke-PodeEndware -Endware $_endware
                    }
                    finally {
                        $WebEvent = $null
                        Close-PodeDisposable -Disposable $context
                    }
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $_ | Write-PodeErrorLog
                $_.Exception | Write-PodeErrorLog -CheckInnerException
                throw $_.Exception
            }
        }

        # start the runspace for listening on x-number of threads
        1..$PodeContext.Threads.General | ForEach-Object {
            Add-PodeRunspace -Type Web -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
        }
    }

    # only if WS endpoint
    if (Test-PodeEndpoints -Type Ws) {
        # script to write messages back to the client(s)
        $signalScript = {
            param(
                [Parameter(Mandatory = $true)]
                $Listener
            )

            try {
                while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                    $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token))

                    try {
                        # get the sockets for the message
                        $sockets = @()

                        # by clientId
                        if (![string]::IsNullOrWhiteSpace($message.ClientId)) {
                            $sockets = @($Listener.Signals[$message.ClientId])
                        }
                        else {
                            $sockets = @($Listener.Signals.Values)

                            # by path
                            if (![string]::IsNullOrWhiteSpace($message.Path)) {
                                $sockets = @(foreach ($socket in $sockets) {
                                        if ($socket.Path -ieq $message.Path) {
                                            $socket
                                        }
                                    })
                            }
                        }

                        # do nothing if no socket found
                        if (($null -eq $sockets) -or ($sockets.Length -eq 0)) {
                            continue
                        }

                        # send the message to all found sockets
                        foreach ($socket in $sockets) {
                            try {
                                $socket.Context.Response.SendSignal($message)
                            }
                            catch {
                                $null = $Listener.Signals.Remove($socket.ClientId)
                            }
                        }
                    }
                    catch [System.OperationCanceledException] {}
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                    finally {
                        Close-PodeDisposable -Disposable $message
                    }
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $_ | Write-PodeErrorLog
                $_.Exception | Write-PodeErrorLog -CheckInnerException
                throw $_.Exception
            }
        }

        Add-PodeRunspace -Type Signals -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener }
    }

    # only if WS endpoint
    if (Test-PodeEndpoints -Type Ws) {
        # script to queue messages from clients to send back to other clients from the server
        $clientScript = {
            param(
                [Parameter(Mandatory = $true)]
                $Listener,

                [Parameter(Mandatory = $true)]
                [int]
                $ThreadId
            )

            try {
                while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                    $context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token))

                    try {
                        $payload = ($context.Message | ConvertFrom-Json)
                        $Request = $context.Signal.Context.Request
                        $Response = $context.Signal.Context.Response

                        $SignalEvent = @{
                            Response  = $Response
                            Request   = $Request
                            Lockable  = $PodeContext.Threading.Lockables.Global
                            Path      = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath)
                            Data      = @{
                                Path     = [System.Web.HttpUtility]::UrlDecode($payload.path)
                                Message  = $payload.message
                                ClientId = $payload.clientId
                                Direct   = [bool]$payload.direct
                            }
                            Endpoint  = @{
                                Protocol = $Request.Url.Scheme
                                Address  = $Request.Host
                                Name     = $context.Signal.Context.EndpointName
                            }
                            Route     = $null
                            ClientId  = $context.Signal.ClientId
                            Timestamp = $context.Timestamp
                            Streamed  = $true
                            Metadata  = @{}
                        }

                        # see if we have a route and invoke it, otherwise auto-send
                        $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name

                        if ($null -ne $SignalEvent.Route) {
                            $null = Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat
                        }
                        else {
                            Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId
                        }
                    }
                    catch [System.OperationCanceledException] {}
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                    finally {
                        Update-PodeServerSignalMetrics -SignalEvent $SignalEvent
                        Close-PodeDisposable -Disposable $context
                    }
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $_ | Write-PodeErrorLog
                $_.Exception | Write-PodeErrorLog -CheckInnerException
                throw $_.Exception
            }
        }

        # start the runspace for listening on x-number of threads
        1..$PodeContext.Threads.General | ForEach-Object {
            Add-PodeRunspace -Type Signals -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
        }
    }

    # script to keep web server listening until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Listener
        }
    }

    $waitType = 'Web'
    if (!(Test-PodeEndpoints -Type Http)) {
        $waitType = 'Signals'
    }

    Add-PodeRunspace -Type $waitType -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile

    # browse to the first endpoint, if flagged
    if ($Browse) {
        Start-Process $endpoints[0].Url
    }

    return @(foreach ($endpoint in $endpoints) {
            @{
                Url      = $endpoint.Url
                Pool     = $endpoint.Pool
                DualMode = $endpoint.DualMode
            }
        })
}

function New-PodeListener {
    [CmdletBinding()]
    [OutputType([Pode.PodeListener])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Threading.CancellationToken]
        $CancellationToken
    )

    return [PodeListener]::new($CancellationToken)
}

function New-PodeListenerSocket {
    [CmdletBinding()]
    [OutputType([Pode.PodeSocket])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [ipaddress[]]
        $Address,

        [Parameter(Mandatory = $true)]
        [int]
        $Port,

        [Parameter()]
        [System.Security.Authentication.SslProtocols]
        $SslProtocols,

        [Parameter(Mandatory = $true)]
        [PodeProtocolType]
        $Type,

        [Parameter()]
        [X509Certificate]
        $Certificate,

        [Parameter()]
        [bool]
        $AllowClientCertificate,

        [switch]
        $DualMode
    )

    return [PodeSocket]::new($Name, $Address, $Port, $SslProtocols, $Type, $Certificate, $AllowClientCertificate, 'Implicit', $DualMode.IsPresent)
}
src\Private\Responses.ps1
<#
.SYNOPSIS
Displays a customized error page based on the provided error code and additional error details.

.DESCRIPTION
This function is responsible for displaying a custom error page when an error occurs within a Pode web application. It takes an error code, a description, an exception object, and a content type as input. The function then attempts to find a corresponding error page based on the error code and content type. If a custom error page is found, and if exception details are to be shown (as per server settings), it builds a detailed exception message. Finally, it writes the error page to the response stream, displaying the custom error page to the user.

.PARAMETER Code
The HTTP status code of the error. This code is used to find a matching custom error page.

.PARAMETER Description
A descriptive message about the error. This is displayed on the error page if available.

.PARAMETER Exception
The exception object that caused the error. If exception tracing is enabled, details from this object are displayed on the error page.

.PARAMETER ContentType
The content type of the error page to be displayed. This is used to select an appropriate error page format (e.g., HTML, JSON).

.EXAMPLE
Show-PodeErrorPage -Code 404 -Description "Not Found" -ContentType "text/html"

This example shows how to display a custom 404 Not Found error page in HTML format.

.OUTPUTS
None. This function writes the error page directly to the response stream.

.NOTES
- The function uses `Find-PodeErrorPage` to locate a custom error page based on the HTTP status code and content type.
- It checks for server configuration to determine whether to show detailed exception information on the error page.
- The function relies on the global `$PodeContext` variable for server settings and to encode exception and URL details safely.
- `Write-PodeFileResponse` is used to send the custom error page as the response, along with any dynamic data (e.g., exception details, URL).
- This is an internal function and may change in future releases of Pode.
#>
function Show-PodeErrorPage {
    param(
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        $Exception,

        [Parameter()]
        [string]
        $ContentType
    )

    # error page info
    $errorPage = Find-PodeErrorPage -Code $Code -ContentType $ContentType

    # if no page found, return
    if (Test-PodeIsEmpty $errorPage) {
        return
    }

    # if exception trace showing enabled then build the exception details object
    $ex = $null
    if (!(Test-PodeIsEmpty $Exception) -and $PodeContext.Server.Web.ErrorPages.ShowExceptions) {
        $ex = @{
            Message    = [System.Web.HttpUtility]::HtmlEncode($Exception.Exception.Message)
            StackTrace = [System.Web.HttpUtility]::HtmlEncode($Exception.ScriptStackTrace)
            Line       = [System.Web.HttpUtility]::HtmlEncode($Exception.InvocationInfo.PositionMessage)
            Category   = [System.Web.HttpUtility]::HtmlEncode($Exception.CategoryInfo.ToString())
        }
    }

    # setup the data object for dynamic pages
    $data = @{
        Url         = [System.Web.HttpUtility]::HtmlEncode((Get-PodeUrl))
        Status      = @{
            Code        = $Code
            Description = $Description
        }
        Exception   = $ex
        ContentType = $errorPage.ContentType
    }

    # write the error page to the stream
    Write-PodeFileResponse -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType
}



<#
.SYNOPSIS
Serves files as HTTP responses in a Pode web server, handling both dynamic and static content.

.DESCRIPTION
This function serves files from the server to the client, supporting both static files and files that are dynamically processed by a view engine.
For dynamic content, it uses the server's configured view engine to process the file and returns the rendered content.
For static content, it simply returns the file's content. The function allows for specifying content type, cache control, and HTTP status code.

.PARAMETER Path
The relative path to the file to be served. This path is resolved against the server's root directory.

.PARAMETER Data
A hashtable of data that can be passed to the view engine for dynamic files.

.PARAMETER ContentType
The MIME type of the response. If not provided, it is inferred from the file extension.

.PARAMETER MaxAge
The maximum age (in seconds) for which the response can be cached by the client. Applies only to static content.

.PARAMETER StatusCode
The HTTP status code to accompany the response. Defaults to 200 (OK).

.PARAMETER Cache
A switch to indicate whether the response should include HTTP caching headers. Applies only to static content.

.EXAMPLE
Write-PodeFileResponseInternal -Path 'index.pode' -Data @{ Title = 'Home Page' } -ContentType 'text/html'

Serves the 'index.pode' file as an HTTP response, processing it with the view engine and passing in a title for dynamic content rendering.

.EXAMPLE
Write-PodeFileResponseInternal -Path 'logo.png' -ContentType 'image/png' -Cache

Serves the 'logo.png' file as a static file with the specified content type and caching enabled.

.OUTPUTS
None. The function writes directly to the HTTP response stream.

.NOTES
This is an internal function and may change in future releases of Pode.
#>

function Write-PodeFileResponseInternal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $ContentType = $null,

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache,

        [switch]
        $FileBrowser
    )

    # Attempt to retrieve information about the path
    $pathInfo = Test-PodePath -Path $Path -Force -ReturnItem -FailOnDirectory:(!$FileBrowser)

    if (!$pathinfo) {
        return
    }

    # Check if the path is a directory
    if ( $pathInfo.PSIsContainer) {
        # If directory browsing is enabled, use the directory response function
        Write-PodeDirectoryResponseInternal -Path $Path
    }
    else {
        # are we dealing with a dynamic file for the view engine? (ignore html)
        # Determine if the file is dynamic and should be processed by the view engine
        $mainExt = $pathInfo.Extension.TrimStart('.')

        # generate dynamic content
        if (![string]::IsNullOrWhiteSpace($mainExt) -and (
        ($mainExt -ieq 'pode') -or
        ($mainExt -ieq $PodeContext.Server.ViewEngine.Extension -and $PodeContext.Server.ViewEngine.IsDynamic)
            )
        ) {
            # Process dynamic content with the view engine
            $content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data

            # Determine the correct content type for the response
            # get the sub-file extension, if empty, use original
            $subExt = [System.IO.Path]::GetExtension($pathInfo.BaseName).TrimStart('.')

            $subExt = (Protect-PodeValue -Value $subExt -Default $mainExt)

            $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt))

            # Write the processed content as the HTTP response
            Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode
        }
        # this is a static file
        else {
            try {
                if (Test-PodeIsPSCore) {
                    $content = (Get-Content -Path $Path -Raw -AsByteStream)
                }
                else {
                    $content = (Get-Content -Path $Path -Raw -Encoding byte)
                }
                # Determine and set the content type for static files
                $ContentType = Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt)
                # Write the file content as the HTTP response
                Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache
                return
            }
            catch [System.UnauthorizedAccessException] {
                $statusCode = 401
            }
            catch {
                $statusCode = 400
            }
            # If the file does not exist, set the HTTP response status code appropriately
            Set-PodeResponseStatus -Code $StatusCode

        }
    }
}

<#
.SYNOPSIS
Serves a directory listing as a web page.

.DESCRIPTION
The Write-PodeDirectoryResponseInternal function generates an HTML response that lists the contents of a specified directory,
allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the
display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it
serves the file directly.

.PARAMETER Path
The relative path to the directory that should be displayed. This path is resolved and used to generate a list of contents.


.EXAMPLE
# resolve for relative path
$RelativePath = Get-PodeRelativePath -Path './static' -JoinRoot
Write-PodeDirectoryResponseInternal -Path './static'

Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Write-PodeDirectoryResponseInternal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [string]
        $Path
    )

    if ($WebEvent.Path -eq '/') {
        $leaf = '/'
        $rootPath = '/'
    }
    else {
        # get leaf of current physical path, and set root path
        $leaf = ($Path.Split(':')[1] -split '[\\/]+') -join '/'
        $rootPath = $WebEvent.Path -ireplace "$($leaf)$", ''
    }

    # Determine if the server is running in Windows mode or is running a varsion that support Linux
    # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7.4#example-10-output-for-non-windows-operating-systems
    $windowsMode = ((Test-PodeIsWindows) -or ($PSVersionTable.PSVersion -lt [version]'7.1.0') )

    # Construct the HTML content for the file browser view
    $htmlContent = [System.Text.StringBuilder]::new()

    $atoms = $WebEvent.Path -split '/'
    $atoms = @(foreach ($atom in $atoms) {
            if (![string]::IsNullOrEmpty($atom)) {
                [uri]::EscapeDataString($atom)
            }
        })
    if ([string]::IsNullOrWhiteSpace($atoms)) {
        $baseLink = ''
    }
    else {
        $baseLink = "/$($atoms -join '/')"
    }

    # Handle navigation to the parent directory (..)
    if ($leaf -ne '/') {
        $LastSlash = $baseLink.LastIndexOf('/')
        if ($LastSlash -eq -1) {
            Set-PodeResponseStatus -Code 404
            return
        }
        $ParentLink = $baseLink.Substring(0, $LastSlash)
        if ([string]::IsNullOrWhiteSpace($ParentLink)) {
            $ParentLink = '/'
        }
        $item = Get-Item '..'
        if ($windowsMode) {
            $htmlContent.Append("<tr> <td class='mode'>")
            $htmlContent.Append($item.Mode)
        }
        else {
            $htmlContent.Append("<tr> <td class='unixMode'>")
            $htmlContent.Append($item.UnixMode)
            $htmlContent.Append("</td> <td class='user'>")
            $htmlContent.Append($item.User)
            $htmlContent.Append("</td> <td class='group'>")
            $htmlContent.Append($item.Group)
        }
        $htmlContent.Append("</td> <td class='dateTime'>")
        $htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss'))
        $htmlContent.Append("</td> <td class='dateTime'>")
        $htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))
        $htmlContent.Append( "</td> <td class='size'></td> <td class='icon'><i class='bi bi-folder2-open'></td> <td class='name'><a href='")
        $htmlContent.Append($ParentLink)
        $htmlContent.AppendLine("'>..</a></td> </tr>")
    }
    # Retrieve the child items of the specified directory
    $child = Get-ChildItem -Path $Path -Force
    foreach ($item in $child) {
        $link = "$baseLink/$([uri]::EscapeDataString($item.Name))"
        if ($item.PSIsContainer) {
            $size = ''
            $icon = '📁'
        }
        else {
            $size = '{0:N2}KB' -f ($item.Length / 1KB)
            $icon = '📄'
        }

        # Format each item as an HTML row
        if ($windowsMode) {
            $htmlContent.Append("<tr> <td class='mode'>")
            $htmlContent.Append($item.Mode)
        }
        else {
            $htmlContent.Append("<tr> <td class='unixMode'>")
            $htmlContent.Append($item.UnixMode)
            $htmlContent.Append("</td> <td class='user'>")
            $htmlContent.Append($item.User)
            $htmlContent.Append("</td> <td class='group'>")
            $htmlContent.Append($item.Group)
        }
        $htmlContent.Append("</td> <td class='dateTime'>")
        $htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss'))
        $htmlContent.Append("</td> <td class='dateTime'>")
        $htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))
        $htmlContent.Append("</td> <td class='size'>")
        $htmlContent.Append( $size)
        $htmlContent.Append( "</td> <td class='icon'>")
        $htmlContent.Append( $icon)
        $htmlContent.Append( "</td> <td class='name'><a href='")
        $htmlContent.Append( $link)
        $htmlContent.Append( "'>")
        $htmlContent.Append($item.Name )
        $htmlContent.AppendLine('</a></td> </tr>' )
    }

    $Data = @{
        RootPath    = $RootPath
        Path        = $leaf.Replace('\', '/')
        WindowsMode = $windowsMode.ToString().ToLower()
        FileContent = $htmlContent.ToString()   # Convert the StringBuilder content to a string
    }

    $podeRoot = Get-PodeModuleMiscPath
    # Write the response
    Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-file-browsing.html.pode')) -Data $Data

}



<#
.SYNOPSIS
Sends a file as an attachment in the response, supporting both file streaming and directory browsing options.

.DESCRIPTION
The Write-PodeAttachmentResponseInternal function is designed to handle HTTP responses for file downloads or directory browsing within a Pode web server. It resolves the given file or directory path, sets the appropriate content type, and configures the response to either download the file as an attachment or list the directory contents if browsing is enabled. The function supports both PowerShell Core and Windows PowerShell environments for file content retrieval.

.PARAMETER Path
The path to the file or directory. This parameter is mandatory and accepts pipeline input. The function resolves relative paths based on the server's root directory.

.PARAMETER ContentType
The MIME type of the file being served. This is validated against a pattern to ensure it's in the format 'type/subtype'. If not specified, the function attempts to determine the content type based on the file extension.

.PARAMETER FileBrowser
A switch parameter that, when present, enables directory browsing. If the path points to a directory and this parameter is enabled, the function will list the directory's contents instead of returning a 404 error.

.EXAMPLE
Write-PodeAttachmentResponseInternal -Path './files/document.pdf' -ContentType 'application/pdf'

Serves the 'document.pdf' file with the 'application/pdf' MIME type as a downloadable attachment.

.EXAMPLE
Write-PodeAttachmentResponseInternal -Path './files' -FileBrowser

Lists the contents of the './files' directory if the FileBrowser switch is enabled; otherwise, returns a 404 error.

.NOTES
- This function integrates with Pode's internal handling of HTTP responses, leveraging other Pode-specific functions like Get-PodeContentType and Set-PodeResponseStatus. It differentiates between streamed and serverless environments to optimize file delivery.
- This is an internal function and may change in future releases of Pode.
#>
function Write-PodeAttachmentResponseInternal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [switch]
        $FileBrowser

    )

    # Attempt to retrieve information about the path
    $pathInfo = Test-PodePath -Path $Path -Force -ReturnItem -FailOnDirectory:(!$FileBrowser)

    if (!$pathinfo) {
        return
    }

    # Check if the path exists
    if ($null -eq $pathInfo) {
        return
    }

    if ( $pathInfo.PSIsContainer) {
        # filebrowsing is enabled, use the directory response function
        Write-PodeDirectoryResponseInternal -Path $Path
        return
    }
    try {
        # setup the content type and disposition
        if (!$ContentType) {
            $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $pathInfo.Extension)
        }
        else {
            $WebEvent.Response.ContentType = $ContentType
        }

        Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($pathInfo.Name)"

        # if serverless, get the content raw and return
        if (!$WebEvent.Streamed) {
            if (Test-PodeIsPSCore) {
                $content = (Get-Content -Path $Path -Raw -AsByteStream)
            }
            else {
                $content = (Get-Content -Path $Path -Raw -Encoding byte)
            }

            $WebEvent.Response.Body = $content
        }

        # else if normal, stream the content back
        else {
            # setup the response details and headers
            $WebEvent.Response.SendChunked = $false

            # set file as an attachment on the response
            $buffer = [byte[]]::new(64 * 1024)
            $read = 0

            # open up the file as a stream
            $fs = (Get-Item $Path).OpenRead()
            $WebEvent.Response.ContentLength64 = $fs.Length

            while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
                $WebEvent.Response.OutputStream.Write($buffer, 0, $read)
            }
        }
    }
    finally {
        Close-PodeDisposable -Disposable $fs
    }

}
src\Private\Routes.ps1
function Test-PodeRouteFromRequest {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $CheckWildMethod
    )

    $route = Find-PodeRoute -Method $Method -Path $Path -EndpointName $EndpointName -CheckWildMethod:$CheckWildMethod
    return ($null -ne $route)
}

function Find-PodeRoute {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $CheckWildMethod
    )

    # first, if supplied, check the wildcard method
    if ($CheckWildMethod -and ($PodeContext.Server.Routes['*'].Count -ne 0)) {
        $found = Find-PodeRoute -Method '*' -Path $Path -EndpointName $EndpointName
        if ($null -ne $found) {
            return $found
        }
    }

    # first ensure we have the method
    $_method = $PodeContext.Server.Routes[$Method]
    if ($null -eq $_method) {
        return $null
    }

    # is this a static route?
    $isStatic = ($Method -ieq 'static')

    # if we have a perfect match for the route, return it if the protocol is right
    if (!$isStatic) {
        $found = Get-PodeRouteByUrl -Routes $_method[$Path] -EndpointName $EndpointName
        if ($null -ne $found) {
            return $found
        }
    }

    # otherwise, match the path to routes on regex (first match only)
    $paths = @($_method.Keys)
    if ($isStatic) {
        [array]::Sort($paths)
        [array]::Reverse($paths)
    }

    $valid = @(foreach ($key in $paths) {
            if ($Path -imatch "^$($key)$") {
                $key
                break
            }
        })[0]

    if ($null -eq $valid) {
        return $null
    }

    # is the route valid for any protocols/endpoints?
    $found = Get-PodeRouteByUrl -Routes $_method[$valid] -EndpointName $EndpointName
    if ($null -eq $found) {
        return $null
    }

    return $found
}

function Find-PodePublicRoute {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    $source = $null
    $publicPath = $PodeContext.Server.InbuiltDrives['public']

    # reutrn null if there is no public directory
    if ([string]::IsNullOrWhiteSpace($publicPath)) {
        return $source
    }

    # use the public static directory (but only if path is a file, and a public dir is present)
    if (Test-PodePathIsFile $Path) {
        $source = [System.IO.Path]::Combine($publicPath, $Path.TrimStart('/', '\'))
        if (!(Test-PodePath -Path $source -NoStatus)) {
            $source = $null
        }
    }

    # return the route details
    return $source
}


<#
.SYNOPSIS
Finds a static route for a given path in a Pode web server application, with optional checks for public routes.

.DESCRIPTION
This function searches for a static route matching the specified path within a Pode web server application. It attempts to resolve the route to a physical file or directory and supports additional checks for public routes as a fallback option. The function returns a hashtable with route details, including whether the route is for a downloadable file, if it's cacheable, and whether it redirects to a default document.

.PARAMETER Path
The URL path for which to find a static route. This parameter is mandatory.

.PARAMETER EndpointName
Optional. Specifies the name of the endpoint to which the route may belong. If not provided, the function searches across all endpoints.

.PARAMETER CheckPublic
A switch parameter. If specified, the function also checks for the route in public routes as a fallback option.

.EXAMPLE
$staticRoute = Find-PodeStaticRoute -Path '/images/logo.png' -CheckPublic

Searches for a static route for '/images/logo.png'. If not found, checks if a public route exists for the same path.

.EXAMPLE
$staticRoute = Find-PodeStaticRoute -Path '/css/style.css' -EndpointName 'WebUI'

Searches for a static route for '/css/style.css' specifically within the 'WebUI' endpoint, without checking public routes.

.OUTPUTS
Hashtable. Returns a hashtable containing the route details, such as the source path, download flag, cacheability, and redirect status.

.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Find-PodeStaticRoute {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $CheckPublic
    )

    # attempt to get a static route for the path
    $found = Find-PodeRoute -Method 'static' -Path $Path -EndpointName $EndpointName
    $download = ([bool]$found.Download)
    $source = $null
    $isDefault = $false
    $redirectToDefault = ([bool]$found.RedirectToDefault)

    # if we have a defined static route, use that
    if ($null -ne $found) {
        # see if we have a file
        $file = [string]::Empty

        if ($found.KleeneStar) {
            $matchingPath = "$($found.Path -ireplace '.\*', '.+?')$"
        }
        else {
            $matchingPath = "$($found.Path)$"
        }
        if ($Path -imatch $matchingPath) {
            $file = (Protect-PodeValue -Value $Matches['file'] -Default ([string]::Empty))
        }

        $fileInfo = Get-Item -Path ([System.IO.Path]::Combine($found.Source, $file)) -Force -ErrorAction Ignore
        #if $file doesn't exist return $null
        if ($null -eq $fileInfo) {
            return $null
        }

        # if there's no file, we need to check defaults
        if (!$found.Download -and $fileInfo.PSIsContainer -and (Get-PodeCount @($found.Defaults)) -gt 0) {
            foreach ($def in $found.Defaults) {
                $fileInfoDefaultFile = Get-Item -Path ([System.IO.Path]::Combine($fileInfo.FullName, $def)) -Force -ErrorAction Ignore
                if ($fileInfoDefaultFile) {
                    $file = $fileInfoDefaultFile.FullName
                    $isDefault = $true
                    break
                }
            }
        }
        $source = [System.IO.Path]::Combine($found.Source, $file)

    }

    # check public, if flagged
    if ($CheckPublic -and !(Test-PodePath -Path $source -NoStatus)) {
        $source = Find-PodePublicRoute -Path $Path
        $download = $false
        $found = $null
        $isDefault = $false
        $redirectToDefault = $false
    }

    # return nothing if no source
    if ([string]::IsNullOrWhiteSpace($source)) {
        return $null
    }

    # return the route details
    if ($redirectToDefault -and $isDefault) {
        $redirectToDefault = $true
    }
    else {
        $redirectToDefault = $false
    }

    return @{
        Content = @{
            Source            = $source
            IsDownload        = $download
            IsCachable        = (Test-PodeRouteValidForCaching -Path $Path)
            RedirectToDefault = $redirectToDefault
        }
        Route   = $found
    }
}


function Find-PodeSignalRoute {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    # attempt to get a signal route for the path
    return (Find-PodeRoute -Method 'signal' -Path $Path -EndpointName $EndpointName)
}

function Test-PodeRouteValidForCaching {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    # check current state of caching
    $config = $PodeContext.Server.Web.Static.Cache
    $caching = $config.Enabled

    # if caching, check include/exclude
    if ($caching) {
        if (($null -ne $config.Exclude) -and ($Path -imatch $config.Exclude)) {
            $caching = $false
        }

        if (($null -ne $config.Include) -and ($Path -inotmatch $config.Include)) {
            $caching = $false
        }
    }

    return $caching
}

<#
.SYNOPSIS
Finds and returns a route from an array of routes based on an endpoint name and/or path.

.DESCRIPTION
This function iterates over an array of route definitions to locate a specific route that matches the provided endpoint name and path.
It supports scenarios where only one of the parameters is provided or both. If no matching route is found, or if the routes array is empty or null,
the function returns $null.

.PARAMETER Routes
An array of hashtable objects, each representing a route with potentially defined properties like Root and Endpoint.Name.

.PARAMETER EndpointName
The name of the endpoint to search for within the route definitions. This parameter is optional.

.EXAMPLE
$routes = @(
    @{ Root = '/api'; Endpoint = @{ Name = 'GetData' } },
    @{ Root = '/home'; Endpoint = @{ Name = 'Index' } }
)
Get-PodeRouteByUrl -Routes $routes -EndpointName 'GetData'

Returns the route for the '/api' endpoint named 'GetData'.

.EXAMPLE
$routes = @(
    @{ Root = '/api'; Endpoint = @{ Name = 'GetData' } },
    @{ Root = '/home'; Endpoint = @{ Name = 'Index' } }
)
Get-PodeRouteByUrl -Routes $routes -Path '/api'

Returns the route for the '/api' path, regardless of the endpoint name.

.NOTES
The function prioritizes matching both the endpoint name and path but can return a route based on either criterion if the other is unspecified.
#>
function Get-PodeRouteByUrl {
    param(
        [Parameter()]
        [hashtable[]]
        $Routes,

        [Parameter()]
        [string]
        $EndpointName
    )

    # Return null immediately if routes are not defined or empty
    if (($null -eq $Routes) -or ($Routes.Length -eq 0)) {
        return $null
    }

    # Handle case when no specific endpoint name is provided
    if ([string]::IsNullOrWhiteSpace($EndpointName)) {
        foreach ($route in $Routes) {
            # Return the first route as a default if no path is specified
            return $route
        }
    }
    else {
        # Handle case when an endpoint name is provided
        foreach ($route in $Routes) {
            if (  $route.Endpoint.Name -ieq $EndpointName) {
                # Return the first route that matches the endpoint name as a default
                return $route
            }
        }
    }

    # Last resort check only route with no endpoint name
    foreach ($route in $Routes) {
        if ([string]::IsNullOrWhiteSpace($route.Endpoint.Name)) {
            # Return the first route that matches the endpoint name as a default
            return $route
        }
    }

    # Return null if no matching route is found
    return $null
}


function ConvertTo-PodeOpenApiRoutePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    return (Resolve-PodePlaceholders -Path $Path -Pattern '\:(?<tag>[\w]+)' -Prepend '{' -Append '}')
}

function Update-PodeRouteSlashes {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [switch]
        $Static,

        [switch]
        $NoLeadingSlash
    )

    # ensure route starts with a '/'
    if (!$NoLeadingSlash -and !$Path.StartsWith('/')) {
        $Path = "/$($Path)"
    }

    if ($Static) {
        # ensure the static route ends with '/{0,1}.*'
        $Path = $Path.TrimEnd('/*')
        $Path = "$($Path)[/]{0,1}(?<file>*)"
    }

    # replace * with .*
    $Path = ($Path -ireplace '\*', '.*')
    return $Path
}

function Split-PodeRouteQuery {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    return ($Path -isplit '\?')[0]
}

function ConvertTo-PodeRouteRegex {
    param(
        [Parameter()]
        [string]
        $Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return [string]::Empty
    }

    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = Split-PodeRouteQuery -Path $Path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Resolve-PodePlaceholders -Path $Path

    return $Path
}

function Get-PodeStaticRouteDefault {
    if (!(Test-PodeIsEmpty $PodeContext.Server.Web.Static.Defaults)) {
        return @($PodeContext.Server.Web.Static.Defaults)
    }

    return @(
        'index.html',
        'index.htm',
        'default.html',
        'default.htm'
    )
}

function Test-PodeRouteInternal {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Address,

        [switch]
        $ThrowError
    )

    # check the routes
    $found = $false
    $routes = @($PodeContext.Server.Routes[$Method][$Path])

    foreach ($route in $routes) {
        if (($route.Endpoint.Protocol -ieq $Protocol) -and ($route.Endpoint.Address -ieq $Address)) {
            $found = $true
            break
        }
    }

    # skip if not found
    if (!$found) {
        return $false
    }

    # do we want to throw an error if found, or skip?
    if (!$ThrowError) {
        return $true
    }

    # throw error
    $_url = $Protocol
    if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Address)) {
        $_url = "$($_url)://$($Address)"
    }
    elseif (![string]::IsNullOrWhiteSpace($Address)) {
        $_url = $Address
    }

    if ([string]::IsNullOrEmpty($_url)) {
        throw "[$($Method)] $($Path): Already defined"
    }

    throw "[$($Method)] $($Path): Already defined for $($_url)"
}

function Convert-PodeFunctionVerbToHttpMethod {
    param(
        [Parameter()]
        [string]
        $Verb
    )

    # if empty, just return default
    switch ($Verb) {
        { $_ -iin @('Find', 'Format', 'Get', 'Join', 'Search', 'Select', 'Split', 'Measure', 'Ping', 'Test', 'Trace') } { 'GET' }
        { $_ -iin @('Set') } { 'PUT' }
        { $_ -iin @('Rename', 'Edit', 'Update') } { 'PATCH' }
        { $_ -iin @('Clear', 'Close', 'Exit', 'Hide', 'Remove', 'Undo', 'Dismount', 'Unpublish', 'Disable', 'Uninstall', 'Unregister') } { 'DELETE' }
        Default { 'POST' }
    }
}


<#
.SYNOPSIS
Finds and returns the appropriate transfer encoding for a given route path in a Pode server context.

.DESCRIPTION
This function determines the correct transfer encoding for a specified route path within a Pode web server. It checks if a transfer encoding is already specified and returns it; otherwise, it defaults to the server's default transfer encoding. The function searches the server's transfer encoding route settings for a pattern that matches the given path. If a match is found, the corresponding transfer encoding is returned. This is useful for dynamically setting response encodings based on specific route patterns.

.PARAMETER Path
The route path for which the transfer encoding is being determined. This parameter is mandatory.

.PARAMETER TransferEncoding
The current transfer encoding, if already determined. This is an optional parameter. If specified and not null or whitespace, this function returns the given value without further processing.

.EXAMPLE
$encoding = Find-PodeRouteTransferEncoding -Path "/api/data" -TransferEncoding "chunked"

This example determines the transfer encoding for the route "/api/data", with an initial encoding of "chunked". If "/api/data" matches a specific pattern in the server's transfer encoding settings, the corresponding encoding is returned; otherwise, "chunked" is returned.

.OUTPUTS
String. Returns the determined transfer encoding for the given route path. This will be either the input TransferEncoding (if provided and valid), a matched encoding from the server's settings, or the server's default transfer encoding.

.NOTES
- The function uses a case-insensitive match (`-imatch`) to find the first route key pattern that matches the specified path.
- This is an internal function and may change in future releases of Pode.
#>
function Find-PodeRouteTransferEncoding {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $TransferEncoding
    )

    # if we already have one, return it
    if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
        return $TransferEncoding
    }

    # set the default
    $TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Default

    # find type by pattern from settings
    $matched = $null
    foreach ($key in $PodeContext.Server.Web.TransferEncoding.Routes.Keys) {
        if ($Path -imatch $key) {
            $matched = $key
            break
        }
    }

    # if we get a match, set it
    if (!(Test-PodeIsEmpty $matched)) {
        $TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Routes[$matched]
    }

    return $TransferEncoding
}

function Find-PodeRouteContentType {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $ContentType
    )

    # if we already have one, return it
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        return $ContentType
    }

    # set the default
    $ContentType = $PodeContext.Server.Web.ContentType.Default

    # find type by pattern from settings
    $matched = $null
    foreach ($key in $PodeContext.Server.Web.ContentType.Routes.Keys) {
        if ($Path -imatch $key) {
            $matched = $key
            break
        }
    }

    # if we get a match, set it
    if (!(Test-PodeIsEmpty $matched)) {
        $ContentType = $PodeContext.Server.Web.ContentType.Routes[$matched]
    }

    return $ContentType
}

function ConvertTo-PodeMiddleware {
    [OutputType([hashtable[]])]
    param(
        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    # return if no middleware
    if (Test-PodeIsEmpty $Middleware) {
        return $null
    }

    $Middleware = @($Middleware)

    # ensure supplied middlewares are either a scriptblock, or a valid hashtable
    foreach ($mid in $Middleware) {
        if ($null -eq $mid) {
            continue
        }

        # check middleware is a type valid
        if (($mid -isnot [scriptblock]) -and ($mid -isnot [hashtable])) {
            throw "One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: $($mid.GetType().Name)"
        }

        # if middleware is hashtable, ensure the keys are valid (logic is a scriptblock)
        if ($mid -is [hashtable]) {
            if ($null -eq $mid.Logic) {
                throw 'A Hashtable Middleware supplied has no Logic defined'
            }

            if ($mid.Logic -isnot [scriptblock]) {
                throw "A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: $($mid.Logic.GetType().Name)"
            }
        }
    }

    # if we have middleware, convert scriptblocks to hashtables
    $converted = @(for ($i = 0; $i -lt $Middleware.Length; $i++) {
            if ($null -eq $Middleware[$i]) {
                continue
            }

            if ($Middleware[$i] -is [scriptblock]) {
                $_script, $_usingVars = Convert-PodeScopedVariables -ScriptBlock $Middleware[$i] -PSSession $PSSession

                $Middleware[$i] = @{
                    Logic          = $_script
                    UsingVariables = $_usingVars
                }
            }

            $Middleware[$i]
        })

    return $converted
}

function Get-PodeRouteIfExistsPreference {
    # from route groups
    $groupPref = $RouteGroup.IfExists
    if (![string]::IsNullOrWhiteSpace($groupPref) -and ($groupPref -ine 'default')) {
        return $groupPref
    }

    # from Use-PodeRoute
    if (![string]::IsNullOrWhiteSpace($script:RouteIfExists) -and ($script:RouteIfExists -ine 'default')) {
        return $script:RouteIfExists
    }

    # global preference
    $globalPref = $PodeContext.Server.Preferences.Routes.IfExists
    if (![string]::IsNullOrWhiteSpace($globalPref) -and ($globalPref -ine 'default')) {
        return $globalPref
    }

    # final global default
    return 'Error'
}
src\Private\Schedules.ps1
function Find-PodeSchedule {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeContext.Schedules.Items[$Name]
}

function Test-PodeSchedulesExist {
    return (($null -ne $PodeContext.Schedules) -and (($PodeContext.Schedules.Enabled) -or ($PodeContext.Schedules.Items.Count -gt 0)))
}

function Start-PodeScheduleRunspace {
    if (!(Test-PodeSchedulesExist)) {
        return
    }

    Add-PodeSchedule -Name '__pode_schedule_housekeeper__' -Cron '@minutely' -ScriptBlock {
        if ($PodeContext.Schedules.Processes.Count -eq 0) {
            return
        }

        foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) {
            $process = $PodeContext.Schedules.Processes[$key]

            # is it completed?
            if (!$process.Runspace.Handler.IsCompleted) {
                continue
            }

            # dispose and remove the schedule process
            Close-PodeScheduleInternal -Process $process
        }

        $process = $null
    }

    $script = {
        # select the schedules that trigger on-start
        $_now = [DateTime]::Now

        $PodeContext.Schedules.Items.Values |
            Where-Object {
                $_.OnStart
            } | ForEach-Object {
                Invoke-PodeInternalSchedule -Schedule $_
            }

        # complete any schedules
        Complete-PodeInternalSchedules -Now $_now

        # first, sleep for a period of time to get to 00 seconds (start of minute)
        Start-Sleep -Seconds (60 - [DateTime]::Now.Second)

        while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
            $_now = [DateTime]::Now

            # select the schedules that need triggering
            $PodeContext.Schedules.Items.Values |
                Where-Object {
                    !$_.Completed -and
                    (($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and
                    (($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and
                    (Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now)
                } | ForEach-Object {
                    Invoke-PodeInternalSchedule -Schedule $_
                }

            # complete any schedules
            Complete-PodeInternalSchedules -Now $_now

            # cron expression only goes down to the minute, so sleep for 1min
            Start-Sleep -Seconds (60 - [DateTime]::Now.Second)
        }
    }

    Add-PodeRunspace -Type Main -ScriptBlock $script -NoProfile
}

function Close-PodeScheduleInternal {
    param(
        [Parameter()]
        [hashtable]
        $Process
    )

    if ($null -eq $Process) {
        return
    }

    Close-PodeDisposable -Disposable $Process.Runspace.Pipeline
    $null = $PodeContext.Schedules.Processes.Remove($Process.ID)
}

function Complete-PodeInternalSchedules {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]
        $Now
    )

    # add any schedules to remove that have exceeded their end time
    $Schedules = @($PodeContext.Schedules.Items.Values |
            Where-Object { (($null -ne $_.EndTime) -and ($_.EndTime -lt $Now)) })

    if (($null -eq $Schedules) -or ($Schedules.Length -eq 0)) {
        return
    }

    # set any expired schedules as being completed
    $Schedules | ForEach-Object {
        $_.Completed = $true
    }
}

function Invoke-PodeInternalSchedule {
    param(
        [Parameter(Mandatory = $true)]
        $Schedule
    )

    $Schedule.OnStart = $false

    # increment total number of triggers for the schedule
    $Schedule.Count++

    # set last trigger to current next trigger
    if ($null -ne $Schedule.NextTriggerTime) {
        $Schedule.LastTriggerTime = $Schedule.NextTriggerTime
    }
    else {
        $Schedule.LastTriggerTime = [datetime]::Now
    }

    # check if we have hit the limit, and remove
    if (($Schedule.Limit -gt 0) -and ($Schedule.Count -ge $Schedule.Limit)) {
        $Schedule.Completed = $true
    }

    # reset the cron and next trigger
    if (!$Schedule.Completed) {
        $Schedule.Crons = Reset-PodeRandomCronExpressions -Expressions $Schedule.Crons
        $Schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $Schedule.Crons -EndTime $Schedule.EndTime
    }
    else {
        $Schedule.NextTriggerTime = $null
    }

    # trigger the schedules logic
    Invoke-PodeInternalScheduleLogic -Schedule $Schedule
}

function Invoke-PodeInternalScheduleLogic {
    param(
        [Parameter(Mandatory = $true)]
        $Schedule,

        [Parameter()]
        [hashtable]
        $ArgumentList = $null
    )

    try {
        # setup event param
        $parameters = @{
            Event = @{
                Lockable = $PodeContext.Threading.Lockables.Global
                Sender   = $Schedule
                Metadata = @{}
            }
        }

        # add any schedule args
        foreach ($key in $Schedule.Arguments.Keys) {
            $parameters[$key] = $Schedule.Arguments[$key]
        }

        # add adhoc schedule invoke args
        if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) {
            foreach ($key in $ArgumentList.Keys) {
                $parameters[$key] = $ArgumentList[$key]
            }
        }

        # add any using variables
        if ($null -ne $Schedule.UsingVariables) {
            foreach ($usingVar in $Schedule.UsingVariables) {
                $parameters[$usingVar.NewName] = $usingVar.Value
            }
        }

        $name = New-PodeGuid
        $runspace = Add-PodeRunspace -Type Schedules -ScriptBlock (($Schedule.Script).GetNewClosure()) -Parameters $parameters -PassThru

        $PodeContext.Schedules.Processes[$name] = @{
            ID       = $name
            Schedule = $Schedule.Name
            Runspace = $runspace
        }
    }
    catch {
        $_ | Write-PodeErrorLog
    }
}
src\Private\ScopedVariables.ps1
function Add-PodeScopedVariablesInbuilt {
    Add-PodeScopedVariableInbuiltUsing
    Add-PodeScopedVariableInbuiltCache
    Add-PodeScopedVariableInbuiltSecret
    Add-PodeScopedVariableInbuiltSession
    Add-PodeScopedVariableInbuiltState
}

function Add-PodeScopedVariableInbuiltCache {
    Add-PodeScopedVariable -Name 'cache' `
        -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " `
        -GetReplace "Get-PodeCache -Key '{{name}}'"
}

function Add-PodeScopedVariableInbuiltSecret {
    Add-PodeScopedVariable -Name 'secret' `
        -SetReplace "Update-PodeSecret -Name '{{name}}' -InputObject " `
        -GetReplace "Get-PodeSecret -Name '{{name}}'"
}

function Add-PodeScopedVariableInbuiltSession {
    Add-PodeScopedVariable -Name 'session' `
        -SetReplace "`$WebEvent.Session.Data.'{{name}}'" `
        -GetReplace "`$WebEvent.Session.Data.'{{name}}'"
}

function Add-PodeScopedVariableInbuiltState {
    Add-PodeScopedVariable -Name 'state' `
        -SetReplace "Set-PodeState -Name '{{name}}' -Value " `
        -GetReplace "`$PodeContext.Server.State.'{{name}}'.Value"
}

function Add-PodeScopedVariableInbuiltUsing {
    Add-PodeScopedVariable -Name 'using' -ScriptBlock {
        param($ScriptBlock, $PSSession)

        # do nothing if no script or session
        if (($null -eq $ScriptBlock) -or ($null -eq $PSSession)) {
            return $ScriptBlock, $null
        }

        # rename any __using_ vars for inner timers, etcs
        $strScriptBlock = "$($ScriptBlock)"
        $foundInnerUsing = $false

        while ($strScriptBlock -imatch '(?<full>\$__using_(?<name>[a-z0-9_\?]+))') {
            $foundInnerUsing = $true
            $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "`$using:$($Matches['name'])")
        }

        # just return if there are no $using:
        if ($strScriptBlock -inotmatch '\$using:') {
            return $ScriptBlock, $null
        }

        # if we found any inner usings, recreate the scriptblock
        if ($foundInnerUsing) {
            $ScriptBlock = [scriptblock]::Create($strScriptBlock)
        }

        # get any using variables
        $usingVars = Get-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock
        if (($null -eq $usingVars) -or ($usingVars.Count -eq 0)) {
            return $ScriptBlock, $null
        }

        # convert any using vars to use new names
        $usingVars = Find-PodeScopedVariableUsingVariableValues -UsingVariables $usingVars -PSSession $PSSession

        # now convert the script
        $newScriptBlock = Convert-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock -UsingVariables $usingVars

        # return converted script
        return $newScriptBlock, $usingVars
    }
}

function Get-PodeScopedVariableUsingVariables {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )

    return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true)
}

function Find-PodeScopedVariableUsingVariableValues {
    param(
        [Parameter(Mandatory = $true)]
        $UsingVariables,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    $mapped = @{}

    foreach ($usingVar in $UsingVariables) {
        # var name
        $varName = $usingVar.SubExpression.VariablePath.UserPath

        # only retrieve value if new var
        if (!$mapped.ContainsKey($varName)) {
            # get value, or get __using_ value for child scripts
            $value = $PSSession.PSVariable.Get($varName)
            if ([string]::IsNullOrEmpty($value)) {
                $value = $PSSession.PSVariable.Get("__using_$($varName)")
            }

            if ([string]::IsNullOrEmpty($value)) {
                throw "Value for `$using:$($varName) could not be found"
            }

            # add to mapped
            $mapped[$varName] = @{
                OldName           = $usingVar.SubExpression.Extent.Text
                NewName           = "__using_$($varName)"
                NewNameWithDollar = "`$__using_$($varName)"
                SubExpressions    = @()
                Value             = $value.Value
            }
        }

        # add the vars sub-expression for replacing later
        $mapped[$varName].SubExpressions += $usingVar.SubExpression
    }

    return @($mapped.Values)
}

function Convert-PodeScopedVariableUsingVariables {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $UsingVariables
    )

    $varsList = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]'
    $newParams = New-Object System.Collections.ArrayList

    foreach ($usingVar in $UsingVariables) {
        foreach ($subExp in $usingVar.SubExpressions) {
            $null = $varsList.Add($subExp)
        }
    }

    $null = $newParams.AddRange(@($UsingVariables.NewNameWithDollar))
    $newParams = ($newParams -join ', ')
    $tupleParams = [tuple]::Create($varsList, $newParams)

    $bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance'
    $_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags)
    $convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams))

    if (!$ScriptBlock.Ast.ParamBlock) {
        $convertedScriptBlockStr = "param($($newParams))`n$($convertedScriptBlockStr)"
    }

    $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)

    if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) {
        $convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|')
        $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)
    }

    return $convertedScriptBlock
}
src\Private\Secrets.ps1
function Initialize-PodeSecretVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )

    $null = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters)
}

function Register-PodeSecretManagementVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig,

        [Parameter()]
        [string]
        $VaultName,

        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName
    )

    # use the Name for VaultName if not passed
    if ([string]::IsNullOrWhiteSpace($VaultName)) {
        $VaultName = $VaultConfig.Name
    }

    # import the modules
    $null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false
    $null = Import-Module -Name $ModuleName -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false

    # attempt to register the vault
    $null = Register-SecretVault -Name $VaultName -ModuleName $ModuleName -VaultParameters $VaultConfig.Parameters -Confirm:$false -AllowClobber -ErrorAction Stop

    # all is good, so set the config
    $VaultConfig['SecretManagement'] = @{
        VaultName  = $VaultName
        ModuleName = $ModuleName
    }
}

function Register-PodeSecretCustomVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [scriptblock]
        $UnlockScriptBlock,

        [Parameter()]
        [scriptblock]
        $RemoveScriptBlock,

        [Parameter()]
        [scriptblock]
        $SetScriptBlock,

        [Parameter()]
        [scriptblock]
        $UnregisterScriptBlock
    )

    # unlock secret with no script?
    if ($VaultConfig.Unlock.Enabled -and (Test-PodeIsEmpty $UnlockScriptBlock)) {
        throw 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied'
    }

    # all is good, so set the config
    $VaultConfig['Custom'] = @{
        Read       = $ScriptBlock
        Unlock     = $UnlockScriptBlock
        Remove     = $RemoveScriptBlock
        Set        = $SetScriptBlock
        Unregister = $UnregisterScriptBlock
    }
}

function Unlock-PodeSecretManagementVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unlock the vault?
    if (!$VaultConfig.Unlock.Enabled) {
        return $null
    }

    # unlock the vault
    $null = Unlock-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Password $VaultConfig.Unlock.Secret -ErrorAction Stop

    # interval?
    if ($VaultConfig.Unlock.Interval -gt 0) {
        return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
    }

    return $null
}

function Unlock-PodeSecretCustomVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unlock the vault?
    if (!$VaultConfig.Unlock.Enabled) {
        return
    }

    # do we have an unlock scriptblock
    if ($null -eq $VaultConfig.Custom.Unlock) {
        throw "No Unlock ScriptBlock supplied for unlocking the vault '$($VaultConfig.Name)'"
    }

    # unlock the vault, and get back an expiry
    $expiry = (Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unlock -Splat -Return -Arguments @(
            $VaultConfig.Parameters,
        (ConvertFrom-SecureString -SecureString $VaultConfig.Unlock.Secret -AsPlainText)
        ))

    # return expiry if given, otherwise check interval
    if ($null -ne $expiry) {
        return $expiry
    }

    if ($VaultConfig.Unlock.Interval -gt 0) {
        return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
    }

    return $null
}

function Unregister-PodeSecretManagementVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unregister the vault?
    if ($VaultConfig.AutoImported) {
        return
    }

    # unregister the vault
    $null = Unregister-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}

function Unregister-PodeSecretCustomVault {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unregister the vault?
    if ($VaultConfig.AutoImported) {
        return
    }

    # do we have an unregister scriptblock? if not, just do nothing
    if ($null -eq $VaultConfig.Custom.Unregister) {
        return
    }

    # unregister the vault
    $null = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @(
        $VaultConfig.Parameters
    )
}

function Get-PodeSecretManagementKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # fetch the secret
    return (Get-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -AsPlainText -ErrorAction Stop)
}

function Get-PodeSecretCustomKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # fetch the secret
    return (Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Read -Splat -Return -Arguments (@(
                $_vault.Parameters,
                $Key
            ) + $ArgumentList))
}

function Set-PodeSecretManagementKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [object]
        $Value,

        [Parameter()]
        [hashtable]
        $Metadata
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # set the secret
    $null = Set-Secret -Name $Key -Secret $Value -Vault $_vault.SecretManagement.VaultName -Metadata $Metadata -Confirm:$false -ErrorAction Stop
}

function Set-PodeSecretCustomKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [object]
        $Value,

        [Parameter()]
        [hashtable]
        $Metadata,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # do we have a set scriptblock?
    if ($null -eq $_vault.Custom.Set) {
        throw "No Set ScriptBlock supplied for updating/creating secrets in the vault '$($_vault.Name)'"
    }

    # set the secret
    $null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Set -Splat -Arguments (@(
            $_vault.Parameters,
            $Key,
            $Value,
            $Metadata
        ) + $ArgumentList)
}

function Remove-PodeSecretManagementKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # remove the secret
    $null = Remove-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}

function Remove-PodeSecretCustomKey {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # do we have a remove scriptblock?
    if ($null -eq $_vault.Custom.Remove) {
        throw "No Remove ScriptBlock supplied for removing secrets from the vault '$($_vault.Name)'"
    }

    # remove the secret
    $null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Remove -Splat -Arguments (@(
            $_vault.Parameters,
            $Key
        ) + $ArgumentList)
}

function Start-PodeSecretCacheHousekeeper {
    if (Test-PodeTimer -Name '__pode_secrets_cache_expiry__') {
        return
    }

    Add-PodeTimer -Name '__pode_secrets_cache_expiry__' -Interval 60 -ScriptBlock {
        $now = [datetime]::UtcNow

        foreach ($key in $PodeContext.Server.Secrets.Keys.Values) {
            if (!$key.Cache.Enabled -or ($null -eq $key.Cache.Expiry) -or ($key.Cache.Expiry -gt $now)) {
                continue
            }

            $key.Cache.Expiry = $null
            $key.Cache.Value = $null
        }
    }
}

function Start-PodeSecretVaultUnlocker {
    if (Test-PodeTimer -Name '__pode_secrets_vault_unlock__') {
        return
    }

    Add-PodeTimer -Name '__pode_secrets_vault_unlock__' -Interval 60 -ScriptBlock {
        $now = [datetime]::UtcNow

        foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values) {
            if (!$vault.Unlock.Enabled -or ($null -eq $vault.Unlock.Expiry) -or ($vault.Unlock.Expiry -gt $now)) {
                continue
            }

            Unlock-PodeSecretVault -Name $vault.Name
        }
    }
}

function Unregister-PodeSecretVaults {
    param(
        [switch]
        $ThrowError
    )

    if (Test-PodeIsEmpty $PodeContext.Server.Secrets.Vaults) {
        return
    }

    foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values.Name) {
        if ([string]::IsNullOrEmpty($vault)) {
            continue
        }

        try {
            Unregister-PodeSecretVault -Name $vault
        }
        catch {
            if ($ThrowError) {
                throw
            }
            else {
                $_ | Write-PodeErrorLog
            }
        }
    }
}

function Protect-PodeSecretValueType {
    param(
        [Parameter(Mandatory = $true)]
        [object]
        $Value
    )

    if ($Value -is [System.ValueType]) {
        $Value = $Value.ToString()
    }

    if ([string]::IsNullOrEmpty($Value)) {
        $Value = [string]::Empty
    }

    if ($Value -is [ordered]) {
        $Value = [hashtable]$Value
    }

    if (!(
         ($Value -is [string]) -or
         ($Value -is [securestring]) -or
         ($Value -is [hashtable]) -or
         ($Value -is [byte[]]) -or
         ($Value -is [pscredential]) -or
         ($Value -is [System.Management.Automation.OrderedHashtable])
        )) {
        throw "Value to set secret to is of an invalid type. Expected either String, SecureString, HashTable, Byte[], or PSCredential. But got: $($Value.GetType().Name)"
    }

    return $Value
}
src\Private\Security.ps1
using namespace System.Security.Cryptography

function Test-PodeIPLimit {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $IP
    )

    $type = 'IP'

    # get the limit rules and active list
    $rules = $PodeContext.Server.Limits.Rules[$type]
    $active = $PodeContext.Server.Limits.Active[$type]

    # if there are no rules, it's valid
    if (($null -eq $rules) -or ($rules.Count -eq 0)) {
        return $true
    }

    # get the ip address in bytes
    $IP = @{
        String = $IP.IPAddressToString
        Family = $IP.AddressFamily
        Bytes  = $IP.GetAddressBytes()
    }

    # now
    $now = [DateTime]::UtcNow

    # is the ip active? (get a direct match, then try grouped subnets)
    $_active_ip = $active[$IP.String]
    if ($null -eq $_active_ip) {
        $_groups = @(foreach ($key in $active.Keys) {
                if ($active[$key].Rule.Grouped) {
                    $active[$key]
                }
            })

        $_active_ip = @(foreach ($_group in $_groups) {
                if (Test-PodeIPAddressInRange -IP $IP -LowerIP $_group.Rule.Lower -UpperIP $_group.Rule.Upper) {
                    $_group
                    break
                }
            })[0]
    }

    # the ip is active, or part of a grouped subnet
    if ($null -ne $_active_ip) {
        # if limit is -1, always allowed
        if ($_active_ip.Rule.Limit -eq -1) {
            return $true
        }

        # check expire time, a reset if needed
        if ($now -ge $_active_ip.Expire) {
            $_active_ip.Rate = 0
            $_active_ip.Expire = $now.AddSeconds($_active_ip.Rule.Seconds)
        }

        # are we over the limit?
        if ($_active_ip.Rate -ge $_active_ip.Rule.Limit) {
            return $false
        }

        # increment the rate
        $_active_ip.Rate++
        return $true
    }

    # the ip isn't active
    else {
        # get the ip's rule
        $_rule_ip = @(foreach ($rule in $rules.Values) {
                if (Test-PodeIPAddressInRange -IP $IP -LowerIP $rule.Lower -UpperIP $rule.Upper) {
                    $rule
                    break
                }
            })[0]

        # if ip not in rules, it's valid
        # (add to active list as always allowed - saves running where search everytime)
        if ($null -eq $_rule_ip) {
            $active[$IP.String] = @{
                Rule = @{
                    Limit = -1
                }
            }

            return $true
        }

        # add ip to active list (ip if not grouped, else the subnet if it's grouped)
        $_ip = (Resolve-PodeValue -Check $_rule_ip.Grouped -TrueValue $_rule_ip.IP -FalseValue $IP.String)

        $active[$_ip] = @{
            Rule   = $_rule_ip
            Rate   = 1
            Expire = $now.AddSeconds($_rule_ip.Seconds)
        }

        # if limit is 0, it's never allowed
        return ($_rule_ip -ne 0)
    }
}

function Test-PodeRouteLimit {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [string]
        $Path
    )

    $type = 'Route'

    # get the limit rules and active list
    $rules = $PodeContext.Server.Limits.Rules[$type]
    $active = $PodeContext.Server.Limits.Active[$type]

    # if there are no rules, it's valid
    if (($null -eq $rules) -or ($rules.Count -eq 0)) {
        return $true
    }

    # now
    $now = [DateTime]::UtcNow

    # is the route active?
    $_active_route = $active[$Path]

    # the ip is active, or part of a grouped subnet
    if ($null -ne $_active_route) {
        # if limit is -1, always allowed
        if ($_active_route.Rule.Limit -eq -1) {
            return $true
        }

        # check expire time, a reset if needed
        if ($now -ge $_active_route.Expire) {
            $_active_route.Rate = 0
            $_active_route.Expire = $now.AddSeconds($_active_route.Rule.Seconds)
        }

        # are we over the limit?
        if ($_active_route.Rate -ge $_active_route.Rule.Limit) {
            return $false
        }

        # increment the rate
        $_active_route.Rate++
        return $true
    }

    # the route isn't active
    else {
        # get the route's rule
        $_rule_route = $rules[$Path]

        # if route not in rules, it's valid (add to active list as always allowed)
        if ($null -eq $_rule_route) {
            $active[$Path] = @{
                Rule = @{
                    Limit = -1
                }
            }

            return $true
        }

        # add route to active list
        $active[$Path] = @{
            Rule   = $_rule_route
            Rate   = 1
            Expire = $now.AddSeconds($_rule_route.Seconds)
        }

        # if limit is 0, it's never allowed
        return ($_rule_route -ne 0)
    }
}

function Test-PodeEndpointLimit {
    param(
        [Parameter()]
        [string]
        $EndpointName
    )

    $type = 'Endpoint'

    if ([string]::IsNullOrWhiteSpace($EndpointName)) {
        return $true
    }

    # get the limit rules and active list
    $rules = $PodeContext.Server.Limits.Rules[$type]
    $active = $PodeContext.Server.Limits.Active[$type]

    # if there are no rules, it's valid
    if (($null -eq $rules) -or ($rules.Count -eq 0)) {
        return $true
    }

    # now
    $now = [DateTime]::UtcNow

    # is the endpoint active?
    $_active_endpoint = $active[$EndpointName]

    # the endpoint is active
    if ($null -ne $_active_endpoint) {
        # if limit is -1, always allowed
        if ($_active_endpoint.Rule.Limit -eq -1) {
            return $true
        }

        # check expire time, a reset if needed
        if ($now -ge $_active_endpoint.Expire) {
            $_active_endpoint.Rate = 0
            $_active_endpoint.Expire = $now.AddSeconds($_active_endpoint.Rule.Seconds)
        }

        # are we over the limit?
        if ($_active_endpoint.Rate -ge $_active_endpoint.Rule.Limit) {
            return $false
        }

        # increment the rate
        $_active_endpoint.Rate++
        return $true
    }

    # the endpoint isn't active
    else {
        # get the endpoint's rule
        $_rule_endpoint = $rules[$EndpointName]

        # if endpoint not in rules, it's valid (add to active list as always allowed)
        if ($null -eq $_rule_endpoint) {
            $active[$EndpointName] = @{
                Rule = @{
                    Limit = -1
                }
            }

            return $true
        }

        # add endpoint to active list
        $active[$EndpointName] = @{
            Rule   = $_rule_endpoint
            Rate   = 1
            Expire = $now.AddSeconds($_rule_endpoint.Seconds)
        }

        # if limit is 0, it's never allowed
        return ($_rule_endpoint -ne 0)
    }
}

function Test-PodeIPAccess {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $IP
    )

    $type = 'IP'

    # get permission lists for ip
    $allow = $PodeContext.Server.Access.Allow[$type]
    $deny = $PodeContext.Server.Access.Deny[$type]

    # are they empty?
    $alEmpty = (($null -eq $allow) -or ($allow.Count -eq 0))
    $dnEmpty = (($null -eq $deny) -or ($deny.Count -eq 0))

    # if both are empty, value is valid
    if ($alEmpty -and $dnEmpty) {
        return $true
    }

    # get the ip address in bytes
    $IP = @{
        Family = $IP.AddressFamily
        Bytes  = $IP.GetAddressBytes()
    }

    # if value in allow, it's allowed
    if (!$alEmpty) {
        $match = @(foreach ($value in $allow.Values) {
                if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
                    $value
                    break
                }
            })[0]

        if ($null -ne $match) {
            return $true
        }
    }

    # if value in deny, it's disallowed
    if (!$dnEmpty) {
        $match = @(foreach ($value in $deny.Values) {
                if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
                    $value
                    break
                }
            })[0]

        if ($null -ne $match) {
            return $false
        }
    }

    # if we have an allow, it's disallowed (because it's not in there)
    if (!$alEmpty) {
        return $false
    }

    # otherwise it's allowed (because it's not in the deny)
    return $true
}

function Add-PodeIPLimit {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [string]
        $IP,

        [Parameter(Mandatory = $true)]
        [int]
        $Limit,

        [Parameter(Mandatory = $true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # current limit type
    $type = 'IP'

    # ensure limit and seconds are non-zero and negative
    if ($Limit -le 0) {
        throw "Limit value cannot be 0 or less for $($IP)"
    }

    if ($Seconds -le 0) {
        throw "Seconds value cannot be 0 or less for $($IP)"
    }

    # get current rules
    $rules = $PodeContext.Server.Limits.Rules[$type]

    # setup up perm type
    if ($null -eq $rules) {
        $PodeContext.Server.Limits.Rules[$type] = @{}
        $PodeContext.Server.Limits.Active[$type] = @{}
        $rules = $PodeContext.Server.Limits.Rules[$type]
    }

    # have we already added the ip?
    elseif ($rules.ContainsKey($IP)) {
        return
    }

    # calculate the lower/upper ip bounds
    if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
        $_tmp = Get-PodeSubnetRange -SubnetMask $IP
        $_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
        $_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
    }
    elseif (Test-PodeIPAddressAny -IP $IP) {
        $_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
        $_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
    }
    else {
        $_tmpLo = Get-PodeIPAddress -IP $IP
        $_tmpHi = $_tmpLo
    }

    # add limit rule for ip
    $rules.Add($IP, @{
            Limit   = $Limit
            Seconds = $Seconds
            Grouped = [bool]$Group
            IP      = $IP
            Lower   = @{
                Family = $_tmpLo.AddressFamily
                Bytes  = $_tmpLo.GetAddressBytes()
            }
            Upper   = @{
                Family = $_tmpHi.AddressFamily
                Bytes  = $_tmpHi.GetAddressBytes()
            }
        })
}

function Add-PodeRouteLimit {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [int]
        $Limit,

        [Parameter(Mandatory = $true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # current limit type
    $type = 'Route'

    # ensure limit and seconds are non-zero and negative
    if ($Limit -le 0) {
        throw "Limit value cannot be 0 or less for $($IP)"
    }

    if ($Seconds -le 0) {
        throw "Seconds value cannot be 0 or less for $($IP)"
    }

    # get current rules
    $rules = $PodeContext.Server.Limits.Rules[$type]

    # setup up perm type
    if ($null -eq $rules) {
        $PodeContext.Server.Limits.Rules[$type] = @{}
        $PodeContext.Server.Limits.Active[$type] = @{}
        $rules = $PodeContext.Server.Limits.Rules[$type]
    }

    # have we already added the route?
    elseif ($rules.ContainsKey($Path)) {
        return
    }

    # add limit rule for the route
    $rules.Add($Path, @{
            Limit   = $Limit
            Seconds = $Seconds
            Grouped = [bool]$Group
            Path    = $Path
        })
}

function Add-PodeEndpointLimit {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [string]
        $EndpointName,

        [Parameter(Mandatory = $true)]
        [int]
        $Limit,

        [Parameter(Mandatory = $true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # current limit type
    $type = 'Endpoint'

    # does the endpoint exist?
    $endpoint = Get-PodeEndpointByName -Name $EndpointName
    if ($null -eq $endpoint) {
        throw "Endpoint not found: $($EndpointName)"
    }

    # ensure limit and seconds are non-zero and negative
    if ($Limit -le 0) {
        throw "Limit value cannot be 0 or less for $($IP)"
    }

    if ($Seconds -le 0) {
        throw "Seconds value cannot be 0 or less for $($IP)"
    }

    # get current rules
    $rules = $PodeContext.Server.Limits.Rules[$type]

    # setup up perm type
    if ($null -eq $rules) {
        $PodeContext.Server.Limits.Rules[$type] = @{}
        $PodeContext.Server.Limits.Active[$type] = @{}
        $rules = $PodeContext.Server.Limits.Rules[$type]
    }

    # have we already added the endpoint?
    elseif ($rules.ContainsKey($EndpointName)) {
        return
    }

    # add limit rule for the endpoint
    $rules.Add($EndpointName, @{
            Limit        = $Limit
            Seconds      = $Seconds
            Grouped      = [bool]$Group
            EndpointName = $EndpointName
        })
}

function Add-PodeIPAccess {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Access,

        [Parameter(Mandatory = $true)]
        [string]
        $IP
    )

    # current access type
    $type = 'IP'

    # get opposite permission
    $opp = "$(if ($Access -ieq 'allow') { 'Deny' } else { 'Allow' })"

    # get permission lists for type
    $permType = $PodeContext.Server.Access[$Access][$type]
    $oppType = $PodeContext.Server.Access[$opp][$type]

    # setup up perm type
    if ($null -eq $permType) {
        $PodeContext.Server.Access[$Access][$type] = @{}
        $permType = $PodeContext.Server.Access[$Access][$type]
    }

    # have we already added the ip?
    elseif ($permType.ContainsKey($IP)) {
        return
    }

    # remove from opp type
    if ($null -ne $oppType -and $oppType.ContainsKey($IP)) {
        $oppType.Remove($IP)
    }

    # calculate the lower/upper ip bounds
    if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
        $_tmp = Get-PodeSubnetRange -SubnetMask $IP
        $_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
        $_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
    }
    elseif (Test-PodeIPAddressAny -IP $IP) {
        $_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
        $_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
    }
    else {
        $_tmpLo = Get-PodeIPAddress -IP $IP
        $_tmpHi = $_tmpLo
    }

    # add access rule for ip
    $permType.Add($IP, @{
            Lower = @{
                Family = $_tmpLo.AddressFamily
                Bytes  = $_tmpLo.GetAddressBytes()
            }
            Upper = @{
                Family = $_tmpHi.AddressFamily
                Bytes  = $_tmpHi.GetAddressBytes()
            }
        })
}

function Get-PodeCsrfToken {
    # key name to search
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # check the payload
    if (!(Test-PodeIsEmpty $WebEvent.Data[$key])) {
        return $WebEvent.Data[$key]
    }

    # check the query string
    if (!(Test-PodeIsEmpty $WebEvent.Query[$key])) {
        return $WebEvent.Query[$key]
    }

    # check the headers
    $value = (Get-PodeHeader -Name $key)
    if (!(Test-PodeIsEmpty $value)) {
        return $value
    }

    return $null
}

function Test-PodeCsrfToken {
    param(
        [Parameter()]
        [string]
        $Secret,

        [Parameter()]
        [string]
        $Token
    )

    # if there's no token/secret, fail
    if ((Test-PodeIsEmpty $Secret) -or (Test-PodeIsEmpty $Token)) {
        return $false
    }

    # the token must start with "t:"
    if (!$Token.StartsWith('t:')) {
        return $false
    }

    # get the salt from the token
    $_token = $Token.Substring(2)
    $periodIndex = $_token.LastIndexOf('.')
    if ($periodIndex -eq -1) {
        return $false
    }

    $salt = $_token.Substring(0, $periodIndex)

    # ensure the token is valid
    if ((Restore-PodeCsrfToken -Secret $Secret -Salt $salt) -ne $Token) {
        return $false
    }

    return $true
}

function New-PodeCsrfSecret {
    # see if there's already a secret in session/cookie
    $secret = (Get-PodeCsrfSecret)
    if (!(Test-PodeIsEmpty $secret)) {
        return $secret
    }

    # otherwise, make a new secret and cache it
    $secret = (New-PodeGuid -Secure -Length 16)
    Set-PodeCsrfSecret -Secret $secret
    return $secret
}

function Get-PodeCsrfSecret {
    # key name to get secret
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # are we getting it from a cookie, or session?
    if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
        $cookie = Get-PodeCookie `
            -Name $PodeContext.Server.Cookies.Csrf.Name `
            -Secret $PodeContext.Server.Cookies.Csrf.Secret
        return $cookie.Value
    }

    # on session
    else {
        return $WebEvent.Session.Data[$key]
    }
}

function Set-PodeCsrfSecret {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Secret
    )

    # key name to set secret under
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # are we setting this on a cookie, or session?
    if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
        $null = Set-PodeCookie `
            -Name $PodeContext.Server.Cookies.Csrf.Name `
            -Value $Secret `
            -Secret $PodeContext.Server.Cookies.Csrf.Secret
    }

    # on session
    else {
        $WebEvent.Session.Data[$key] = $Secret
    }
}

function Restore-PodeCsrfToken {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Secret,

        [Parameter(Mandatory = $true)]
        [string]
        $Salt
    )

    return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}

function Test-PodeCsrfConfigured {
    return (!(Test-PodeIsEmpty $PodeContext.Server.Cookies.Csrf))
}

function Get-PodeCertificateByFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Certificate,

        [Parameter()]
        [string]
        $Password = $null,

        [Parameter()]
        [string]
        $Key = $null
    )

    # cert + key
    if (![string]::IsNullOrWhiteSpace($Key)) {
        return (Get-PodeCertificateByPemFile -Certificate $Certificate -Password $Password -Key $Key)
    }

    $path = Get-PodeRelativePath -Path $Certificate -JoinRoot -Resolve

    # cert + password
    if (![string]::IsNullOrWhiteSpace($Password)) {
        return [X509Certificates.X509Certificate2]::new($path, $Password)
    }

    # plain cert
    return [X509Certificates.X509Certificate2]::new($path)
}

function Get-PodeCertificateByPemFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Certificate,

        [Parameter()]
        [string]
        $Password = $null,

        [Parameter()]
        [string]
        $Key = $null
    )

    $cert = $null

    $certPath = Get-PodeRelativePath -Path $Certificate -JoinRoot -Resolve
    $keyPath = Get-PodeRelativePath -Path $Key -JoinRoot -Resolve

    # pem's kinda work in .NET3/.NET5
    if ([version]$PSVersionTable.PSVersion -ge [version]'7.0.0') {
        $cert = [X509Certificates.X509Certificate2]::new($certPath)
        $keyText = [System.IO.File]::ReadAllText($keyPath)
        $rsa = [RSA]::Create()

        # .NET5
        if ([version]$PSVersionTable.PSVersion -ge [version]'7.1.0') {
            if ([string]::IsNullOrWhiteSpace($Password)) {
                $rsa.ImportFromPem($keyText)
            }
            else {
                $rsa.ImportFromEncryptedPem($keyText, $Password)
            }
        }

        # .NET3
        else {
            $keyBlocks = $keyText.Split('-', [System.StringSplitOptions]::RemoveEmptyEntries)
            $keyBytes = [System.Convert]::FromBase64String($keyBlocks[1])

            if ($keyBlocks[0] -ieq 'BEGIN PRIVATE KEY') {
                $rsa.ImportPkcs8PrivateKey($keyBytes, [ref]$null)
            }
            elseif ($keyBlocks[0] -ieq 'BEGIN RSA PRIVATE KEY') {
                $rsa.ImportRSAPrivateKey($keyBytes, [ref]$null)
            }
            elseif ($keyBlocks[0] -ieq 'BEGIN ENCRYPTED PRIVATE KEY') {
                $rsa.ImportEncryptedPkcs8PrivateKey($Password, $keyBytes, [ref]$null)
            }
        }

        $cert = [X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($cert, $rsa)
        $cert = [X509Certificates.X509Certificate2]::new($cert.Export([X509Certificates.X509ContentType]::Pkcs12))
    }

    # for everything else, there's the openssl way
    else {
        $tempFile = Join-Path (Split-Path -Parent -Path $certPath) 'temp.pfx'

        try {
            if ([string]::IsNullOrWhiteSpace($Password)) {
                $Password = [string]::Empty
            }

            $result = openssl pkcs12 -inkey $keyPath -in $certPath -export -passin pass:$Password -password pass:$Password -out $tempFile
            if (!$?) {
                throw "Failed to create openssl cert: $($result)"
            }

            $cert = [X509Certificates.X509Certificate2]::new($tempFile, $Password)
        }
        finally {
            $null = Remove-Item $tempFile -Force
        }
    }

    return $cert
}

function Find-PodeCertificateInCertStore {
    param(
        [Parameter(Mandatory = $true)]
        [X509Certificates.X509FindType]
        $FindType,

        [Parameter(Mandatory = $true)]
        [string]
        $Query,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreName]
        $StoreName,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreLocation]
        $StoreLocation
    )

    # fail if not windows
    if (!(Test-PodeIsWindows)) {
        throw 'Certificate Thumbprints/Name are only supported on Windows'
    }

    # open the currentuser\my store
    $x509store = [X509Certificates.X509Store]::new($StoreName, $StoreLocation)

    try {
        # attempt to find the cert
        $x509store.Open([X509Certificates.OpenFlags]::ReadOnly)
        $x509certs = $x509store.Certificates.Find($FindType, $Query, $false)
    }
    finally {
        # close the store!
        if ($null -ne $x509store) {
            Close-PodeDisposable -Disposable $x509store -Close
        }
    }

    # fail if no cert found for query
    if (($null -eq $x509certs) -or ($x509certs.Count -eq 0)) {
        throw "No certificate could be found in $($StoreLocation)\$($StoreName) for '$($Query)'"
    }

    return ([X509Certificates.X509Certificate2]($x509certs[0]))
}

function Get-PodeCertificateByThumbprint {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Thumbprint,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreName]
        $StoreName,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreLocation]
        $StoreLocation
    )

    return Find-PodeCertificateInCertStore `
        -FindType ([X509Certificates.X509FindType]::FindByThumbprint) `
        -Query $Thumbprint `
        -StoreName $StoreName `
        -StoreLocation $StoreLocation
}

function Get-PodeCertificateByName {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreName]
        $StoreName,

        [Parameter(Mandatory = $true)]
        [X509Certificates.StoreLocation]
        $StoreLocation
    )

    return Find-PodeCertificateInCertStore `
        -FindType ([X509Certificates.X509FindType]::FindBySubjectName) `
        -Query $Name `
        -StoreName $StoreName `
        -StoreLocation $StoreLocation
}

function New-PodeSelfSignedCertificate {
    $sanBuilder = [X509Certificates.SubjectAlternativeNameBuilder]::new()
    $null = $sanBuilder.AddIpAddress([ipaddress]::Loopback)
    $null = $sanBuilder.AddIpAddress([ipaddress]::IPv6Loopback)
    $null = $sanBuilder.AddDnsName('localhost')

    if (![string]::IsNullOrWhiteSpace($PodeContext.Server.ComputerName)) {
        $null = $sanBuilder.AddDnsName($PodeContext.Server.ComputerName)
    }

    $rsa = [RSA]::Create(2048)
    $distinguishedName = [X500DistinguishedName]::new('CN=localhost')

    $req = [X509Certificates.CertificateRequest]::new(
        $distinguishedName,
        $rsa,
        [HashAlgorithmName]::SHA256,
        [RSASignaturePadding]::Pkcs1
    )

    $flags = (
        [X509Certificates.X509KeyUsageFlags]::DataEncipherment -bor
        [X509Certificates.X509KeyUsageFlags]::KeyEncipherment -bor
        [X509Certificates.X509KeyUsageFlags]::DigitalSignature
    )

    $null = $req.CertificateExtensions.Add(
        [X509Certificates.X509KeyUsageExtension]::new(
            $flags,
            $false
        )
    )

    $oid = [OidCollection]::new()
    $null = $oid.Add([Oid]::new('1.3.6.1.5.5.7.3.1'))

    $req.CertificateExtensions.Add(
        [X509Certificates.X509EnhancedKeyUsageExtension]::new(
            $oid,
            $false
        )
    )

    $null = $req.CertificateExtensions.Add($sanBuilder.Build())

    $cert = $req.CreateSelfSigned(
        [System.DateTimeOffset]::UtcNow.AddDays(-1),
        [System.DateTimeOffset]::UtcNow.AddYears(10)
    )

    if (Test-PodeIsWindows) {
        $cert.FriendlyName = 'localhost'
    }

    $cert = [X509Certificates.X509Certificate2]::new(
        $cert.Export([X509Certificates.X509ContentType]::Pfx, 'self-signed'),
        'self-signed'
    )

    return $cert
}

function Protect-PodeContentSecurityKeyword {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Value,

        [switch]
        $Append
    )

    # cache it
    if ($Append -and !(Test-PodeIsEmpty $PodeContext.Server.Security.Cache.ContentSecurity[$Name])) {
        $Value += @($PodeContext.Server.Security.Cache.ContentSecurity[$Name])
    }

    $PodeContext.Server.Security.Cache.ContentSecurity[$Name] = $Value

    # do nothing if no value
    if (($null -eq $Value) -or ($Value.Length -eq 0)) {
        return $null
    }

    # keywords
    $Name = $Name.ToLowerInvariant()

    $keywords = @(
        'none',
        'self',
        'unsafe-inline',
        'unsafe-eval'
    )

    $schemes = @(
        'http',
        'https',
        'ws',
        'wss',
        'data',
        'file'
    )

    # build the value
    $values = @(foreach ($v in $Value) {
            if ($keywords -icontains $v) {
                "'$($v.ToLowerInvariant())'"
                continue
            }

            if ($schemes -icontains $v) {
                "$($v.ToLowerInvariant()):"
                continue
            }

            $v
        })

    return "$($Name) $($values -join ' ')"
}

function Protect-PodePermissionsPolicyKeyword {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Value,

        [switch]
        $Append
    )

    # cache it
    if ($Append -and !(Test-PodeIsEmpty $PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])) {
        if (($Value.Length -eq 0) -or (@($PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])[0] -ine 'none')) {
            $Value += @($PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])
        }
    }

    $PodeContext.Server.Security.Cache.PermissionsPolicy[$Name] = $Value

    # do nothing if no value
    if (($null -eq $Value) -or ($Value.Length -eq 0)) {
        return $null
    }

    # build value
    $Name = $Name.ToLowerInvariant()

    if ($Value -icontains 'none') {
        return "$($Name)=()"
    }

    $keywords = @(
        'self'
    )

    $values = @(foreach ($v in $Value) {
            if ($keywords -icontains $v) {
                $v
                continue
            }

            "`"$($v)`""
        })

    return "$($Name)=($($values -join ' '))"
}
src\Private\Server.ps1
function Start-PodeInternalServer {
    param(
        [Parameter()]
        $Request,

        [switch]
        $Browse
    )

    try {
        # Check if the running version of Powershell is EOL
        Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID))" -ForegroundColor Cyan
        $null = Test-PodeVersionPwshEOL -ReportUntested

        # setup temp drives for internal dirs
        Add-PodePSInbuiltDrive

        # setup inbuilt scoped vars
        Add-PodeScopedVariablesInbuilt

        # create the shared runspace state
        New-PodeRunspaceState

        # if iis, setup global middleware to validate token
        Initialize-PodeIISMiddleware

        # load any secret vaults
        Import-PodeSecretVaultsIntoRegistry

        # get the server's script and invoke it - to set up routes, timers, middleware, etc
        $_script = $PodeContext.Server.Logic
        if (Test-PodePath -Path $PodeContext.Server.LogicPath -NoStatus) {
            $_script = Convert-PodeFileToScriptBlock -FilePath $PodeContext.Server.LogicPath
        }

        $_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using
        $null = Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat

        #Validate OpenAPI definitions
        Test-PodeOADefinitionInternal

        # load any modules/snapins
        Import-PodeSnapinsIntoRunspaceState
        Import-PodeModulesIntoRunspaceState

        # load any functions
        Import-PodeFunctionsIntoRunspaceState -ScriptBlock $_script

        # run start event hooks
        Invoke-PodeEvent -Type Start

        # start timer for task housekeeping
        Start-PodeTaskHousekeeper

        # start the cache housekeeper
        Start-PodeCacheHousekeeper

        # create timer/schedules for auto-restarting
        New-PodeAutoRestartServer

        # start the runspace pools for web, schedules, etc
        New-PodeRunspacePools
        Open-PodeRunspacePools

        if (!$PodeContext.Server.IsServerless) {
            # start runspace for loggers
            Start-PodeLoggingRunspace

            # start runspace for timers
            Start-PodeTimerRunspace

            # start runspace for schedules
            Start-PodeScheduleRunspace

            # start runspace for gui
            Start-PodeGuiRunspace

            # start runspace for websockets
            Start-PodeWebSocketRunspace

            # start runspace for file watchers
            Start-PodeFileWatcherRunspace
        }

        # start the appropriate server
        $endpoints = @()

        # - service
        if ($PodeContext.Server.IsService) {
            Start-PodeServiceServer
        }

        # - serverless
        elseif ($PodeContext.Server.IsServerless) {
            switch ($PodeContext.Server.ServerlessType.ToUpperInvariant()) {
                'AZUREFUNCTIONS' {
                    Start-PodeAzFuncServer -Data $Request
                }

                'AWSLAMBDA' {
                    Start-PodeAwsLambdaServer -Data $Request
                }
            }
        }

        # - normal
        else {
            # start each server type
            foreach ($_type in $PodeContext.Server.Types) {
                switch ($_type.ToUpperInvariant()) {
                    'SMTP' {
                        $endpoints += (Start-PodeSmtpServer)
                    }

                    'TCP' {
                        $endpoints += (Start-PodeTcpServer)
                    }

                    'HTTP' {
                        $endpoints += (Start-PodeWebServer -Browse:$Browse)
                    }
                }
            }

            # now go back through, and wait for each server type's runspace pool to be ready
            foreach ($pool in ($endpoints.Pool | Sort-Object -Unique)) {
                $start = [datetime]::Now
                Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready"

                # wait
                while ($PodeContext.RunspacePools[$pool].State -ieq 'Waiting') {
                    Start-Sleep -Milliseconds 100
                }

                Write-Verbose "$($pool) RunspacePool $($PodeContext.RunspacePools[$pool].State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"

                # errored?
                if ($PodeContext.RunspacePools[$pool].State -ieq 'error') {
                    throw "$($pool) RunspacePool failed to load"
                }
            }
        }

        # set the start time of the server (start and after restart)
        $PodeContext.Metrics.Server.StartTime = [datetime]::UtcNow

        # run running event hooks
        Invoke-PodeEvent -Type Running

        # state what endpoints are being listened on
        if ($endpoints.Length -gt 0) {
            Write-PodeHost "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads.General) thread(s)]:" -ForegroundColor Yellow
            $endpoints | ForEach-Object {
                $flags = @()
                if ($_.DualMode) {
                    $flags += 'DualMode'
                }

                if ($flags.Length -eq 0) {
                    $flags = [string]::Empty
                }
                else {
                    $flags = "[$($flags -join ',')]"
                }

                Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow
            }
            # state the OpenAPI endpoints for each definition
            foreach ($key in  $PodeContext.Server.OpenAPI.Definitions.keys) {
                $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks
                if ( $bookmarks) {
                    Write-PodeHost
                    if (!$OpenAPIHeader) {
                        Write-PodeHost 'OpenAPI Info:' -ForegroundColor Yellow
                        $OpenAPIHeader = $true
                    }
                    Write-PodeHost " '$key':" -ForegroundColor Yellow

                    if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) {
                        Write-PodeHost '   - Specification:' -ForegroundColor Yellow
                        foreach ($endpoint in   $bookmarks.route.Endpoint) {
                            Write-PodeHost "     . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor Yellow
                        }
                        Write-PodeHost '   - Documentation:' -ForegroundColor Yellow
                        foreach ($endpoint in   $bookmarks.route.Endpoint) {
                            Write-PodeHost "     . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor Yellow
                        }
                    }
                    else {
                        Write-PodeHost '   - Specification:' -ForegroundColor Yellow
                        $endpoints | ForEach-Object {
                            $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl)
                            Write-PodeHost "     . $url" -ForegroundColor Yellow
                        }
                        Write-PodeHost '   - Documentation:' -ForegroundColor Yellow
                        $endpoints | ForEach-Object {
                            $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path)
                            Write-PodeHost "     . $url" -ForegroundColor Yellow
                        }
                    }
                }
            }

        }
    }
    catch {
        throw $_.Exception
    }
}

function Restart-PodeInternalServer {
    try {
        # inform restart
        Write-PodeHost 'Restarting server...' -NoNewline -ForegroundColor Cyan

        # run restart event hooks
        Invoke-PodeEvent -Type Restart

        # cancel the session token
        $PodeContext.Tokens.Cancellation.Cancel()

        # close all current runspaces
        Close-PodeRunspaces -ClosePool

        # remove all of the pode temp drives
        Remove-PodePSDrives

        # clear-up modules
        $PodeContext.Server.Modules.Clear()

        # clear up timers, schedules and loggers
        $PodeContext.Server.Routes | Clear-PodeHashtableInnerKeys
        $PodeContext.Server.Handlers | Clear-PodeHashtableInnerKeys
        $PodeContext.Server.Events | Clear-PodeHashtableInnerKeys

        if ($null -ne $PodeContext.Server.Verbs) {
            $PodeContext.Server.Verbs.Clear()
        }

        $PodeContext.Server.Views.Clear()
        $PodeContext.Timers.Items.Clear()
        $PodeContext.Server.Logging.Types.Clear()

        # clear schedules
        $PodeContext.Schedules.Items.Clear()
        $PodeContext.Schedules.Processes.Clear()

        # clear tasks
        $PodeContext.Tasks.Items.Clear()
        $PodeContext.Tasks.Results.Clear()

        # clear file watchers
        $PodeContext.Fim.Items.Clear()

        # auto-importers
        Reset-PodeAutoImportConfiguration

        # clear middle/endware
        $PodeContext.Server.Middleware = @()
        $PodeContext.Server.Endware = @()

        # clear body parsers
        $PodeContext.Server.BodyParsers.Clear()

        # clear security headers
        $PodeContext.Server.Security.Headers.Clear()
        $PodeContext.Server.Security.Cache | Clear-PodeHashtableInnerKeys

        # clear endpoints
        $PodeContext.Server.Endpoints.Clear()
        $PodeContext.Server.EndpointsMap.Clear()

        # clear openapi
        $PodeContext.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $PodeContext.Server.Configuration.Web.OpenApi.DefaultDefinitionTag
        # clear the sockets
        $PodeContext.Server.Signals.Enabled = $false
        $PodeContext.Server.Signals.Listener = $null
        $PodeContext.Server.Http.Listener = $null
        $PodeContext.Listeners = @()
        $PodeContext.Receivers = @()
        $PodeContext.Watchers = @()

        # set view engine back to default
        $PodeContext.Server.ViewEngine = @{
            Type           = 'html'
            Extension      = 'html'
            ScriptBlock    = $null
            UsingVariables = $null
            IsDynamic      = $false
        }

        # clear up cookie sessions
        $PodeContext.Server.Sessions.Clear()

        # clear up authentication methods
        $PodeContext.Server.Authentications.Methods.Clear()
        $PodeContext.Server.Authorisations.Methods.Clear()

        # clear up shared state
        $PodeContext.Server.State.Clear()

        # clear scoped variables
        $PodeContext.Server.ScopedVariables.Clear()

        # clear cache
        $PodeContext.Server.Cache.Items.Clear()
        $PodeContext.Server.Cache.Storage.Clear()

        # clear up secret vaults/cache
        Unregister-PodeSecretVaults -ThrowError
        $PodeContext.Server.Secrets.Vaults.Clear()
        $PodeContext.Server.Secrets.Keys.Clear()

        # dispose mutex/semaphores
        Clear-PodeLockables
        Clear-PodeMutexes
        Clear-PodeSemaphores

        # clear up output
        $PodeContext.Server.Output.Variables.Clear()

        # reset type if smtp/tcp
        $PodeContext.Server.Types = @()

        # recreate the session tokens
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
        $PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource

        Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart
        $PodeContext.Tokens.Restart = New-Object System.Threading.CancellationTokenSource

        # reload the configuration
        $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext

        # done message
        Write-PodeHost ' Done' -ForegroundColor Green

        # restart the server
        $PodeContext.Metrics.Server.RestartCount++
        Start-PodeInternalServer
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Test-PodeServerKeepOpen {
    # if we have any timers/schedules/fim - keep open
    if ((Test-PodeTimersExist) -or (Test-PodeSchedulesExist) -or (Test-PodeFileWatchersExist)) {
        return $true
    }

    # if not a service, and not any type/serverless - close server
    if (!$PodeContext.Server.IsService -and (($PodeContext.Server.Types.Length -eq 0) -or $PodeContext.Server.IsServerless)) {
        return $false
    }

    # keep server open
    return $true
}
src\Private\Serverless.ps1
function Start-PodeAzFuncServer {
    param(
        [Parameter(Mandatory = $true)]
        $Data
    )

    # setup any inbuilt middleware that works for azure functions
    $inbuilt_middleware = @(
        (Get-PodeSecurityMiddleware),
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeCookieMiddleware)
    )

    $PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)

    try {
        try {
            # get the request
            $request = $Data.Request

            # setup the response
            $response = New-Object -TypeName HttpResponseContext
            $response.StatusCode = 200
            $response.Headers = @{}

            # reset event data
            $global:WebEvent = @{
                OnEnd            = @()
                Auth             = @{}
                Response         = $response
                Request          = $request
                Lockable         = $PodeContext.Threading.Lockables.Global
                Path             = [string]::Empty
                Method           = $request.Method.ToLowerInvariant()
                Query            = $request.Query
                Endpoint         = @{
                    Protocol = ($request.Url -split '://')[0]
                    Address  = $null
                    Name     = $null
                }
                ContentType      = $null
                ErrorType        = $null
                Cookies          = @{}
                PendingCookies   = @{}
                Parameters       = $null
                Data             = $null
                Files            = $null
                Streamed         = $false
                Route            = $null
                StaticContent    = $null
                Timestamp        = [datetime]::UtcNow
                TransferEncoding = $null
                AcceptEncoding   = $null
                Ranges           = $null
                Metadata         = @{}
            }

            $WebEvent.Endpoint.Address = ((Get-PodeHeader -Name 'host') -split ':')[0]
            $WebEvent.ContentType = (Get-PodeHeader -Name 'content-type')

            # set the path, using static content query parameter if passed
            if (![string]::IsNullOrWhiteSpace($request.Query['static-file'])) {
                $WebEvent.Path = $request.Query['static-file']
            }
            else {
                $funcName = $Data.sys.MethodName
                if ([string]::IsNullOrWhiteSpace($funcName)) {
                    $funcName = $Data.FunctionName
                }

                $WebEvent.Path = "/api/$($funcName)"
            }

            $WebEvent.Path = [System.Web.HttpUtility]::UrlDecode($WebEvent.Path)

            # set pode in server response header
            Set-PodeServerHeader -Type 'Kestrel'

            # invoke global and route middleware
            if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
                if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
                    # 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 {
                        $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
                    }
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            Set-PodeResponseStatus -Code 500 -Exception $_
        }
        finally {
            Update-PodeServerRequestMetrics -WebEvent $WebEvent
        }

        # invoke endware specifc to the current web event
        $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
        Invoke-PodeEndware -Endware $_endware

        # close and send the response
        Push-OutputBinding -Name Response -Value $response
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Start-PodeAwsLambdaServer {
    param(
        [Parameter(Mandatory = $true)]
        $Data
    )

    # setup any inbuilt middleware that works for aws lambda
    $inbuilt_middleware = @(
        (Get-PodeSecurityMiddleware),
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeCookieMiddleware)
    )

    $PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)

    try {
        try {
            # get the request
            $request = $Data

            # setup the response
            $response = @{
                StatusCode = 200
                Headers    = @{}
                Body       = [string]::Empty
            }

            # reset event data
            $global:WebEvent = @{
                OnEnd            = @()
                Auth             = @{}
                Response         = $response
                Request          = $request
                Lockable         = $PodeContext.Threading.Lockables.Global
                Path             = [System.Web.HttpUtility]::UrlDecode($request.path)
                Method           = $request.httpMethod.ToLowerInvariant()
                Query            = $request.queryStringParameters
                Endpoint         = @{
                    Protocol = $null
                    Address  = $null
                    Name     = $null
                }
                ContentType      = $null
                ErrorType        = $null
                Cookies          = @{}
                PendingCookies   = @{}
                Parameters       = $null
                Data             = $null
                Files            = $null
                Streamed         = $false
                Route            = $null
                StaticContent    = $null
                Timestamp        = [datetime]::UtcNow
                TransferEncoding = $null
                AcceptEncoding   = $null
                Ranges           = $null
                Metadata         = @{}
            }

            $WebEvent.Endpoint.Protocol = (Get-PodeHeader -Name 'X-Forwarded-Proto')
            $WebEvent.Endpoint.Address = ((Get-PodeHeader -Name 'Host') -split ':')[0]
            $WebEvent.ContentType = (Get-PodeHeader -Name 'Content-Type')

            # set pode in server response header
            Set-PodeServerHeader -Type 'Lambda'

            # invoke global and route middleware
            if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
                if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
                    # 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 {
                        $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
                    }
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            Set-PodeResponseStatus -Code 500 -Exception $_
        }
        finally {
            Update-PodeServerRequestMetrics -WebEvent $WebEvent
        }

        # invoke endware specifc to the current web event
        $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
        Invoke-PodeEndware -Endware $_endware

        # close and send the response
        if (![string]::IsNullOrWhiteSpace($response.ContentType)) {
            Set-PodeHeader -Name 'Content-Type' -Value $response.ContentType
        }

        return (@{
                'statusCode' = $response.StatusCode
                'headers'    = $response.Headers
                'body'       = $response.Body
            } | ConvertTo-Json -Depth 10 -Compress)
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}
src\Private\ServiceServer.ps1
function Start-PodeServiceServer {
    # ensure we have service handlers
    if (Test-PodeIsEmpty (Get-PodeHandler -Type Service)) {
        throw 'No Service handlers have been defined'
    }

    # state we're running
    Write-PodeHost "Server looping every $($PodeContext.Server.Interval)secs" -ForegroundColor Yellow

    # script for the looping server
    $serverScript = {
        try {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                # the event object
                $ServiceEvent = @{
                    Lockable = $PodeContext.Threading.Lockables.Global
                    Metadata = @{}
                }

                # invoke the service handlers
                $handlers = Get-PodeHandler -Type Service
                foreach ($name in $handlers.Keys) {
                    $handler = $handlers[$name]
                    $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat
                }

                # sleep before next run
                Start-Sleep -Seconds $PodeContext.Server.Interval
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
    }

    # start the runspace for the server
    Add-PodeRunspace -Type Main -ScriptBlock $serverScript
}
src\Private\Sessions.ps1
function New-PodeSession {
    # sessionId
    $sessionId = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.GenerateId -Return

    # tabId
    $tabId = $null
    if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) {
        $tabId = Get-PodeSessionTabId
    }

    # return new session data
    return @{
        Name      = $PodeContext.Server.Sessions.Name
        Id        = $sessionId
        TabId     = $tabId
        FullId    = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId)
        Extend    = $PodeContext.Server.Sessions.Info.Extend
        TimeStamp = [datetime]::UtcNow
        Data      = @{}
    }
}

function Get-PodeSessionFullId {
    param(
        [Parameter()]
        [string]
        $SessionId,

        [Parameter()]
        [string]
        $TabId
    )

    if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) {
        return "$($SessionId)-$($TabId)"
    }

    return $SessionId
}

function Set-PodeSession {
    if ($null -eq $WebEvent.Session) {
        throw 'there is no session available to set on the response'
    }

    # convert secret to strict mode
    $strict = $PodeContext.Server.Sessions.Info.Strict
    $secret = $PodeContext.Server.Sessions.Secret

    # set session on header
    if ($PodeContext.Server.Sessions.Info.UseHeaders) {
        Set-PodeHeader -Name $WebEvent.Session.Name -Value $WebEvent.Session.Id -Secret $secret -Strict:$strict
    }

    # set session as cookie
    else {
        $null = Set-PodeCookie `
            -Name $WebEvent.Session.Name `
            -Value $WebEvent.Session.Id `
            -Secret $secret `
            -Strict:$strict `
            -ExpiryDate (Get-PodeSessionExpiry) `
            -HttpOnly:$PodeContext.Server.Sessions.Info.HttpOnly `
            -Secure:$PodeContext.Server.Sessions.Info.Secure
    }
}

function Get-PodeSession {
    $secret = $PodeContext.Server.Sessions.Secret
    $sessionId = $null
    $tabId = Get-PodeSessionTabId
    $name = $PodeContext.Server.Sessions.Name

    # convert secret to strict mode
    if ($PodeContext.Server.Sessions.Info.Strict) {
        $secret = ConvertTo-PodeStrictSecret -Secret $secret
    }

    # session from header
    if ($PodeContext.Server.Sessions.Info.UseHeaders) {
        # check that the header is validly signed
        if (!(Test-PodeHeaderSigned -Name $PodeContext.Server.Sessions.Name -Secret $secret)) {
            return $null
        }

        # get the header from the request
        $sessionId = Get-PodeHeader -Name $PodeContext.Server.Sessions.Name -Secret $secret
        if ([string]::IsNullOrEmpty($sessionId)) {
            return $null
        }
    }

    # session from cookie
    else {
        # check that the cookie is validly signed
        if (!(Test-PodeCookieSigned -Name $PodeContext.Server.Sessions.Name -Secret $secret)) {
            return $null
        }

        # get the cookie from the request
        $cookie = Get-PodeCookie -Name $PodeContext.Server.Sessions.Name -Secret $secret
        if ([string]::IsNullOrEmpty($cookie)) {
            return $null
        }

        # get details from cookie
        $name = $cookie.Name
        $sessionId = $cookie.Value
    }

    # generate the session data
    return @{
        Name      = $name
        Id        = $sessionId
        TabId     = $tabId
        FullId    = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId)
        Extend    = $PodeContext.Server.Sessions.Info.Extend
        TimeStamp = $null
        Data      = @{}
    }
}

function Revoke-PodeSession {
    # do nothing if no current session
    if ($null -eq $WebEvent.Session) {
        return
    }

    # remove from cookie if being used
    if (!$PodeContext.Server.Sessions.Info.UseHeaders) {
        Remove-PodeCookie -Name $WebEvent.Session.Name
    }

    # remove session from store
    Remove-PodeSessionInternal
}

function Set-PodeSessionDataHash {
    if ($null -eq $WebEvent.Session) {
        throw 'No session available to calculate data hash'
    }

    if (($null -eq $WebEvent.Session.Data) -or ($WebEvent.Session.Data.Count -eq 0)) {
        $WebEvent.Session.Data = @{}
    }

    $WebEvent.Session.DataHash = (Invoke-PodeSHA256Hash -Value (ConvertTo-Json -InputObject $WebEvent.Session.Data.Clone() -Depth 10 -Compress))
}

function Test-PodeSessionDataHash {
    if ($null -eq $WebEvent.Session) {
        return $false
    }

    if ([string]::IsNullOrWhiteSpace($WebEvent.Session.DataHash)) {
        return $false
    }

    if (($null -eq $WebEvent.Session.Data) -or ($WebEvent.Session.Data.Count -eq 0)) {
        $WebEvent.Session.Data = @{}
    }

    $hash = (Invoke-PodeSHA256Hash -Value (ConvertTo-Json -InputObject $WebEvent.Session.Data -Depth 10 -Compress))
    return ($WebEvent.Session.DataHash -eq $hash)
}

function Save-PodeSessionInternal {
    param(
        [switch]
        $Force
    )

    # do nothing if session has no ID
    if ([string]::IsNullOrEmpty($WebEvent.Session.FullId)) {
        return
    }

    # only save if check and hashes different, but not if extending expiry or updated
    if (!$WebEvent.Session.Extend -and $Force -and (Test-PodeSessionDataHash)) {
        return
    }

    # generate the expiry
    $expiry = Get-PodeSessionExpiry

    # the data to save - which will be the data, and some extra metadata like timestamp
    $data = @{
        Version  = 3
        Metadata = @{
            TimeStamp = $WebEvent.Session.TimeStamp
        }
        Data     = $WebEvent.Session.Data
    }

    # save base session data to store
    if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and $WebEvent.Session.TabId) {
        $authData = @{
            Version  = 3
            Metadata = @{
                TimeStamp = $WebEvent.Session.TimeStamp
                Tabbed    = $true
            }
            Data     = @{
                Auth = $WebEvent.Session.Data.Auth
            }
        }

        $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.Id, $authData, $expiry) -Splat
        $data.Metadata['Parent'] = $WebEvent.Session.Id
    }

    # save session data to store
    $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.FullId, $data, $expiry) -Splat

    # update session's data hash
    Set-PodeSessionDataHash
}

function Remove-PodeSessionInternal {
    if ($null -eq $WebEvent.Session) {
        return
    }

    # remove data from store
    $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $WebEvent.Session.Id

    # clear session
    $WebEvent.Session.Clear()
    $WebEvent.Session = $null
}

function Get-PodeSessionInMemStore {
    $store = New-Object -TypeName psobject

    # add in-mem storage
    $store | Add-Member -MemberType NoteProperty -Name Memory -Value @{}

    # delete a sessionId and data
    $store | Add-Member -MemberType NoteProperty -Name Delete -Value {
        param($sessionId)
        $null = $PodeContext.Server.Sessions.Store.Memory.Remove($sessionId)
        if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) {
            Invoke-PodeSchedule -Name '__pode_session_inmem_cleanup__'
        }
    }

    # get a sessionId's data
    $store | Add-Member -MemberType NoteProperty -Name Get -Value {
        param($sessionId)

        $s = $PodeContext.Server.Sessions.Store.Memory[$sessionId]

        # if expire, remove
        if (($null -ne $s) -and ($s.Expiry -lt [DateTime]::UtcNow)) {
            $null = $PodeContext.Server.Sessions.Store.Memory.Remove($sessionId)
            return $null
        }

        return $s.Data
    }

    # update/insert a sessionId and data
    $store | Add-Member -MemberType NoteProperty -Name Set -Value {
        param($sessionId, $data, $expiry)

        $PodeContext.Server.Sessions.Store.Memory[$sessionId] = @{
            Data   = $data
            Expiry = $expiry
        }
    }

    return $store
}

function Set-PodeSessionInMemClearDown {
    # don't setup if serverless - as memory is short lived anyway
    if ($PodeContext.Server.IsServerless) {
        return
    }

    # cleardown expired inmem session every 10 minutes
    Add-PodeSchedule -Name '__pode_session_inmem_cleanup__' -Cron '0/10 * * * *' -ScriptBlock {
        # do nothing if no sessions
        $store = $PodeContext.Server.Sessions.Store
        if (($null -eq $store.Memory) -or ($store.Memory.Count -eq 0)) {
            return
        }

        # remove sessions that have expired, or where the parent is gone
        $now = [DateTime]::UtcNow
        foreach ($key in $store.Memory.Keys) {
            # expired
            if ($store.Memory[$key].Expiry -lt $now) {
                $null = $store.Memory.Remove($key)
                continue
            }

            # parent check - gone/expired
            $parentKey = $store.Memory[$key].Data.Metadata.Parent
            if ($parentKey -and (!$store.Memory.ContainsKey($parentKey) -or ($store.Memory[$parentKey].Expiry -lt $now))) {
                $null = $store.Memory.Remove($key)
            }
        }
    }
}

function Test-PodeSessionsInUse {
    return (($null -ne $WebEvent.Session) -and ($WebEvent.Session.Count -gt 0))
}

function Get-PodeSessionData {
    param(
        [Parameter()]
        [string]
        $SessionId,

        [Parameter()]
        [string]
        $TabId = $null
    )

    $data = $null

    # try and get Tab session
    if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) {
        $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments "$($SessionId)-$($TabId)" -Return

        # now get the parent - but fail if it doesn't exist
        if ($data.Metadata.Parent) {
            $parent = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $data.Metadata.Parent -Return
            if (!$parent) {
                return $null
            }

            if (!$data.Data.Auth) {
                $data.Data.Auth = $parent.Data.Auth
            }
        }
    }

    # try and get normal session
    if (($null -eq $data) -and ![string]::IsNullOrEmpty($SessionId)) {
        $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $SessionId -Return
    }

    return $data
}

function Get-PodeSessionMiddleware {
    return {
        # if session already set, return
        if ($WebEvent.Session) {
            return $true
        }

        try {
            # retrieve the current session from cookie/header
            $WebEvent.Session = Get-PodeSession

            # if no session found, create a new one on the current web event
            if (!$WebEvent.Session) {
                $WebEvent.Session = New-PodeSession
                $new = $true
            }

            # get the session's data from store
            elseif ($null -ne ($data = (Get-PodeSessionData -SessionId $WebEvent.Session.Id -TabId $WebEvent.Session.TabId))) {
                if ($data.Version -lt 3) {
                    $WebEvent.Session.Data = $data
                    $WebEvent.Session.TimeStamp = [datetime]::UtcNow
                }
                else {
                    $WebEvent.Session.Data = $data.Data
                    if ($data.Metadata.Tabbed) {
                        $WebEvent.Session.TimeStamp = [datetime]::UtcNow
                    }
                    else {
                        $WebEvent.Session.TimeStamp = $data.Metadata.TimeStamp
                    }
                }
            }

            # session not in store, create a new one
            else {
                $WebEvent.Session = New-PodeSession
                $new = $true
            }

            # set data hash
            Set-PodeSessionDataHash

            # add session to response if it's new or extendible
            if ($new -or $WebEvent.Session.Extend) {
                Set-PodeSession
            }

            # assign endware for session to set cookie/header
            $WebEvent.OnEnd += @{
                Logic = {
                    if ($null -ne $WebEvent.Session) {
                        Save-PodeSession -Force
                    }
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            return $false
        }

        # move along
        return $true
    }
}
src\Private\Setup.ps1
function Invoke-PodePackageScript {
    param(
        [Parameter()]
        [string]
        $ActionScript
    )

    if ([string]::IsNullOrWhiteSpace($ActionScript)) {
        return
    }

    Invoke-Expression -Command $ActionScript
}

function Install-PodeLocalModules {
    param(
        [Parameter()]
        $Modules = $null
    )

    if ($null -eq $Modules) {
        return
    }

    $psModules = './ps_modules'

    # download modules to ps_modules
    $Modules.psobject.properties.name | ForEach-Object {
        $_name = $_

        # get the module version
        $_version = $Modules.$_name.version
        if ([string]::IsNullOrWhiteSpace($_version)) {
            $_version = $Modules.$_name
        }

        # get the module repository
        $_repository = Protect-PodeValue -Value $Modules.$_name.repository -Default 'PSGallery'

        try {
            # if version is latest, retrieve current
            if ($_version -ieq 'latest') {
                $_version = [string]((Find-Module $_name -Repository $_repository -ErrorAction Ignore).Version)
            }

            Write-Host "=> Downloading $($_name)@$($_version) from $($_repository)... " -NoNewline -ForegroundColor Cyan

            # if the current version exists, do nothing
            if (!(Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)/$($_version)")))) {
                # remove other versions
                if (Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)"))) {
                    $null = Remove-Item -Path ([System.IO.Path]::Combine($psModules, "$($_name)")) -Force -Recurse
                }

                # download the module
                $null = Save-Module -Name $_name -RequiredVersion $_version -Repository $_repository -Path $psModules -Force -ErrorAction Stop
            }

            Write-Host 'Success' -ForegroundColor Green
        }
        catch {
            Write-Host 'Failed' -ForegroundColor Red
            throw "Module or version not found on $($_repository): $($_name)@$($_version)"
        }
    }
}
src\Private\SmtpServer.ps1
using namespace Pode

function Start-PodeSmtpServer {
    # ensure we have smtp handlers
    if (Test-PodeIsEmpty (Get-PodeHandler -Type Smtp)) {
        throw 'No SMTP handlers have been defined'
    }

    # work out which endpoints to listen on
    $endpoints = @()

    @(Get-PodeEndpoints -Type Smtp) | ForEach-Object {
        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
        $_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode)

        # dual mode?
        $addrs = $_ip
        if ($_.DualMode) {
            $addrs = Resolve-PodeIPDualMode -IP $_ip
        }

        # the endpoint
        $_endpoint = @{
            Name                   = $_.Name
            Key                    = "$($_ip):$($_.Port)"
            Address                = $addrs
            Hostname               = $_.HostName
            IsIPAddress            = $_.IsIPAddress
            Port                   = $_.Port
            Certificate            = $_.Certificate.Raw
            AllowClientCertificate = $_.Certificate.AllowClientCertificate
            TlsMode                = $_.Certificate.TlsMode
            Url                    = $_.Url
            Protocol               = $_.Protocol
            Type                   = $_.Type
            Pool                   = $_.Runspace.PoolName
            Acknowledge            = $_.Tcp.Acknowledge
            SslProtocols           = $_.Ssl.Protocols
            DualMode               = $_.DualMode
        }

        # add endpoint to list
        $endpoints += $_endpoint
    }

    # create the listener
    $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token)
    $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
    $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
    $listener.RequestTimeout = $PodeContext.Server.Request.Timeout
    $listener.RequestBodySize = $PodeContext.Server.Request.BodySize

    try {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            $socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Smtp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode)
            $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
            $socket.AcknowledgeMessage = $_.Acknowledge

            if (!$_.IsIPAddress) {
                $socket.Hostnames.Add($_.HostName)
            }

            $listener.Add($socket)
        }

        $listener.Start()
        $PodeContext.Listeners += $listener
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $listener
        throw $_.Exception
    }

    # script for listening out of for incoming requests
    $listenScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Listener,

            [Parameter(Mandatory = $true)]
            [int]
            $ThreadId
        )

        try {
            while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                # get email
                $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))

                try {
                    try {
                        $Request = $context.Request
                        $Response = $context.Response

                        $SmtpEvent = @{
                            Response  = $Response
                            Request   = $Request
                            Lockable  = $PodeContext.Threading.Lockables.Global
                            Email     = @{
                                From            = $Request.From
                                To              = $Request.To
                                Data            = $Request.RawBody
                                Headers         = $Request.Headers
                                Subject         = $Request.Subject
                                IsUrgent        = $Request.IsUrgent
                                ContentType     = $Request.ContentType
                                ContentEncoding = $Request.ContentEncoding
                                Attachments     = $Request.Attachments
                                Body            = $Request.Body
                            }
                            Endpoint  = @{
                                Protocol = $Request.Scheme
                                Address  = $Request.Address
                                Name     = $context.EndpointName
                            }
                            Timestamp = [datetime]::UtcNow
                            Metadata  = @{}
                        }

                        # stop now if the request has an error
                        if ($Request.IsAborted) {
                            throw $Request.Error
                        }

                        # convert the ip
                        $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint)

                        # ensure the request ip is allowed
                        if (!(Test-PodeIPAccess -IP $ip)) {
                            $Response.WriteLine('554 Your IP address was rejected', $true)
                        }

                        # has the ip hit the rate limit?
                        elseif (!(Test-PodeIPLimit -IP $ip)) {
                            $Response.WriteLine('554 Your IP address has hit the rate limit', $true)
                        }

                        # deal with smtp call
                        else {
                            $handlers = Get-PodeHandler -Type Smtp
                            foreach ($name in $handlers.Keys) {
                                $handler = $handlers[$name]
                                $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat
                            }
                        }
                    }
                    catch [System.OperationCanceledException] {}
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                }
                finally {
                    $SmtpEvent = $null
                    Close-PodeDisposable -Disposable $context
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    # start the runspace for listening on x-number of threads
    1..$PodeContext.Threads.General | ForEach-Object {
        Add-PodeRunspace -Type Smtp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
    }

    # script to keep smtp server listening until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Listener
        }
    }

    Add-PodeRunspace -Type Smtp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile

    # state where we're running
    return @(foreach ($endpoint in $endpoints) {
            @{
                Url      = $endpoint.Url
                Pool     = $endpoint.Pool
                DualMode = $endpoint.DualMode
            }
        })
}
src\Private\Streams.ps1
function Read-PodeStreamToEnd {
    param(
        [Parameter()]
        $Stream,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    if ($null -eq $Stream) {
        return [string]::Empty
    }

    return (Use-PodeStream -Stream ([System.IO.StreamReader]::new($Stream, $Encoding)) {
            return $args[0].ReadToEnd()
        })
}

function Read-PodeByteLineFromByteArray {
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [Parameter()]
        [int]
        $StartIndex = 0,

        [switch]
        $IncludeNewLine
    )

    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding

    # attempt to find \n
    $index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex)
    $fIndex = $index

    # if not including new line, remove any trailing \r and \n
    if (!$IncludeNewLine) {
        $fIndex--

        if ($Bytes[$fIndex] -eq $nlBytes.Return) {
            $fIndex--
        }
    }

    # grab the portion of the bytes array - which is our line
    return @{
        Bytes      = $Bytes[$StartIndex..$fIndex]
        StartIndex = $StartIndex
        EndIndex   = $index
    }
}

function Get-PodeByteLinesFromByteArray {
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [switch]
        $IncludeNewLine
    )

    # lines
    $lines = @()
    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding

    # attempt to find \n
    $index = 0
    while (($nextIndex = [array]::IndexOf($Bytes, $nlBytes.NewLine, $index)) -gt 0) {
        $fIndex = $nextIndex

        # if not including new line, remove any trailing \r and \n
        if (!$IncludeNewLine) {
            $fIndex--
            if ($Bytes[$fIndex] -eq $nlBytes.Return) {
                $fIndex--
            }
        }

        # add the line, and get the next one
        $lines += , $Bytes[$index..$fIndex]
        $index = $nextIndex + 1
    }

    return $lines
}

function ConvertFrom-PodeStreamToBytes {
    param(
        [Parameter(Mandatory = $true)]
        $Stream
    )

    $buffer = [byte[]]::new(64 * 1024)
    $ms = New-Object -TypeName System.IO.MemoryStream
    $read = 0

    while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
        $ms.Write($buffer, 0, $read)
    }

    $ms.Close()
    return $ms.ToArray()
}

function ConvertFrom-PodeValueToBytes {
    param(
        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    return $Encoding.GetBytes($Value)
}

function ConvertFrom-PodeBytesToString {
    param(
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [switch]
        $RemoveNewLine
    )

    if (($null -eq $Bytes) -or ($Bytes.Length -eq 0)) {
        return $Bytes
    }

    $value = $Encoding.GetString($Bytes)
    if ($RemoveNewLine) {
        $value = $value.Trim("`r`n")
    }

    return $value
}

function Get-PodeNewLineBytes {
    param(
        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    return @{
        NewLine = @($Encoding.GetBytes("`n"))[0]
        Return  = @($Encoding.GetBytes("`r"))[0]
    }
}

function Test-PodeByteArrayIsBoundary {
    param(
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter()]
        [string]
        $Boundary,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    # if no bytes, return
    if ($Bytes.Length -eq 0) {
        return $false
    }

    # if length difference >3, return (ie, 2 offset for `r`n)
    if (($Bytes.Length - $Boundary.Length) -gt 3) {
        return $false
    }

    # check if bytes starts with the boundary
    return (ConvertFrom-PodeBytesToString $Bytes $Encoding).StartsWith($Boundary)
}

function Remove-PodeNewLineBytesFromArray {
    param(
        [Parameter()]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding
    $length = $Bytes.Length - 1

    if ($Bytes[$length] -eq $nlBytes.NewLine) {
        $length--
    }

    if ($Bytes[$length] -eq $nlBytes.Return) {
        $length--
    }

    return $Bytes[0..$length]
}
src\Private\Tasks.ps1
function Test-PodeTasksExist {
    return (($null -ne $PodeContext.Tasks) -and (($PodeContext.Tasks.Enabled) -or ($PodeContext.Tasks.Items.Count -gt 0)))
}

function Start-PodeTaskHousekeeper {
    if (!(Test-PodeTasksExist)) {
        return
    }

    Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock {
        if ($PodeContext.Tasks.Results.Count -eq 0) {
            return
        }

        $now = [datetime]::UtcNow

        foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) {
            $result = $PodeContext.Tasks.Results[$key]

            # has it force expired?
            if ($result.ExpireTime -lt $now) {
                Close-PodeTaskInternal -Result $result
                continue
            }

            # is it completed?
            if (!$result.Runspace.Handler.IsCompleted) {
                continue
            }

            # is a completed time set?
            if ($null -eq $result.CompletedTime) {
                $result.CompletedTime = [datetime]::UtcNow
                continue
            }

            # is it expired by completion? if so, dispose and remove
            if ($result.CompletedTime.AddMinutes(1) -lt $now) {
                Close-PodeTaskInternal -Result $result
            }
        }

        $result = $null
    }
}

function Close-PodeTaskInternal {
    param(
        [Parameter()]
        [hashtable]
        $Result
    )

    if ($null -eq $Result) {
        return
    }

    Close-PodeDisposable -Disposable $Result.Runspace.Pipeline
    Close-PodeDisposable -Disposable $Result.Result
    $null = $PodeContext.Tasks.Results.Remove($Result.ID)
}

function Invoke-PodeInternalTask {
    param(
        [Parameter(Mandatory = $true)]
        $Task,

        [Parameter()]
        [hashtable]
        $ArgumentList = $null,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    try {
        # setup event param
        $parameters = @{
            Event = @{
                Lockable = $PodeContext.Threading.Lockables.Global
                Sender   = $Task
                Metadata = @{}
            }
        }

        # add any task args
        foreach ($key in $Task.Arguments.Keys) {
            $parameters[$key] = $Task.Arguments[$key]
        }

        # add adhoc task invoke args
        if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) {
            foreach ($key in $ArgumentList.Keys) {
                $parameters[$key] = $ArgumentList[$key]
            }
        }

        # add any using variables
        if ($null -ne $Task.UsingVariables) {
            foreach ($usingVar in $Task.UsingVariables) {
                $parameters[$usingVar.NewName] = $usingVar.Value
            }
        }

        $name = New-PodeGuid
        $result = [System.Management.Automation.PSDataCollection[psobject]]::new()
        $runspace = Add-PodeRunspace -Type Tasks -ScriptBlock (($Task.Script).GetNewClosure()) -Parameters $parameters -OutputStream $result -PassThru

        if ($Timeout -ge 0) {
            $expireTime = [datetime]::UtcNow.AddSeconds($Timeout)
        }
        else {
            $expireTime = [datetime]::MaxValue
        }

        $PodeContext.Tasks.Results[$name] = @{
            ID            = $name
            Task          = $Task.Name
            Runspace      = $runspace
            Result        = $result
            CompletedTime = $null
            ExpireTime    = $expireTime
            Timeout       = $Timeout
        }

        return $PodeContext.Tasks.Results[$name]
    }
    catch {
        $_ | Write-PodeErrorLog
    }
}

function Wait-PodeNetTaskInternal {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Threading.Tasks.Task]
        $Task,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    # do we need a timeout?
    $timeoutTask = $null
    if ($Timeout -gt 0) {
        $timeoutTask = [System.Threading.Tasks.Task]::Delay($Timeout)
    }

    # set the check task
    if ($null -eq $timeoutTask) {
        $checkTask = $Task
    }
    else {
        $checkTask = [System.Threading.Tasks.Task]::WhenAny($Task, $timeoutTask)
    }

    # is there a cancel token to supply?
    if (($null -eq $PodeContext) -or ($null -eq $PodeContext.Tokens.Cancellation.Token)) {
        $checkTask.Wait()
    }
    else {
        $checkTask.Wait($PodeContext.Tokens.Cancellation.Token)
    }

    # if the main task isnt complete, it timed out
    if (($null -ne $timeoutTask) -and (!$Task.IsCompleted)) {
        throw [System.TimeoutException]::new("Task has timed out after $($Timeout)ms")
    }

    # only return a value if the result has one
    if ($null -ne $Task.Result) {
        return $Task.Result
    }
}

function Wait-PodeTaskInternal {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Task,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    # timeout needs to be in milliseconds
    if ($Timeout -gt 0) {
        $Timeout *= 1000
    }

    # wait for the pipeline to finish processing
    $null = $Task.Runspace.Handler.AsyncWaitHandle.WaitOne($Timeout)

    # get the current result
    $result = $Task.Result.ReadAll()

    # close the task
    Close-PodeTask -Task $Task

    # only return a value if the result has one
    if (($null -ne $result) -and ($result.Count -gt 0)) {
        return $result
    }
}
src\Private\TcpServer.ps1
using namespace Pode

function Start-PodeTcpServer {
    # work out which endpoints to listen on
    $endpoints = @()

    @(Get-PodeEndpoints -Type Tcp) | ForEach-Object {
        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
        $_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode)

        # dual mode?
        $addrs = $_ip
        if ($_.DualMode) {
            $addrs = Resolve-PodeIPDualMode -IP $_ip
        }

        # the endpoint
        $_endpoint = @{
            Name                   = $_.Name
            Key                    = "$($_ip):$($_.Port)"
            Address                = $addrs
            Hostname               = $_.HostName
            IsIPAddress            = $_.IsIPAddress
            Port                   = $_.Port
            Certificate            = $_.Certificate.Raw
            AllowClientCertificate = $_.Certificate.AllowClientCertificate
            TlsMode                = $_.Certificate.TlsMode
            Url                    = $_.Url
            Protocol               = $_.Protocol
            Type                   = $_.Type
            Pool                   = $_.Runspace.PoolName
            Acknowledge            = $_.Tcp.Acknowledge
            CRLFMessageEnd         = $_.Tcp.CRLFMessageEnd
            SslProtocols           = $_.Ssl.Protocols
            DualMode               = $_.DualMode
        }

        # add endpoint to list
        $endpoints += $_endpoint
    }

    # create the listener
    $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token)
    $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
    $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
    $listener.RequestTimeout = $PodeContext.Server.Request.Timeout
    $listener.RequestBodySize = $PodeContext.Server.Request.BodySize

    try {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            $socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Tcp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode)
            $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
            $socket.AcknowledgeMessage = $_.Acknowledge
            $socket.CRLFMessageEnd = $_.CRLFMessageEnd

            if (!$_.IsIPAddress) {
                $socket.Hostnames.Add($_.HostName)
            }

            $listener.Add($socket)
        }

        $listener.Start()
        $PodeContext.Listeners += $listener
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $listener
        throw $_.Exception
    }

    # script for listening out of for incoming requests
    $listenScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Listener,

            [Parameter(Mandatory = $true)]
            [int]
            $ThreadId
        )

        try {
            while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                # get email
                $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))

                try {
                    try {
                        $Request = $context.Request
                        $Response = $context.Response

                        $TcpEvent = @{
                            Response   = $Response
                            Request    = $Request
                            Lockable   = $PodeContext.Threading.Lockables.Global
                            Endpoint   = @{
                                Protocol = $Request.Scheme
                                Address  = $Request.Address
                                Name     = $context.EndpointName
                            }
                            Parameters = $null
                            Timestamp  = [datetime]::UtcNow
                            Metadata   = @{}
                        }

                        # stop now if the request has an error
                        if ($Request.IsAborted) {
                            throw $Request.Error
                        }

                        # convert the ip
                        $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint)

                        # ensure the request ip is allowed
                        if (!(Test-PodeIPAccess -IP $ip)) {
                            $Response.WriteLine('Your IP address was rejected', $true)
                            Close-PodeTcpClient
                            continue
                        }

                        # has the ip hit the rate limit?
                        if (!(Test-PodeIPLimit -IP $ip)) {
                            $Response.WriteLine('Your IP address has hit the rate limit', $true)
                            Close-PodeTcpClient
                            continue
                        }

                        # deal with tcp call and find the verb, and for the endpoint
                        if ([string]::IsNullOrEmpty($TcpEvent.Request.Body)) {
                            continue
                        }

                        $verb = Find-PodeVerb -Verb $TcpEvent.Request.Body -EndpointName $TcpEvent.Endpoint.Name
                        if ($null -eq $verb) {
                            $verb = Find-PodeVerb -Verb '*' -EndpointName $TcpEvent.Endpoint.Name
                        }

                        if ($null -eq $verb) {
                            continue
                        }

                        # set the route parameters
                        if ($verb.Verb -ine '*') {
                            $TcpEvent.Parameters = @{}
                            if ($TcpEvent.Request.Body -imatch "$($verb.Verb)$") {
                                $TcpEvent.Parameters = $Matches
                            }
                        }

                        # invoke it
                        if ($null -ne $verb.Logic) {
                            $null = Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat
                        }

                        # is the verb auto-close?
                        if ($verb.Connection.Close) {
                            Close-PodeTcpClient
                            continue
                        }

                        # is the verb auto-upgrade to ssl?
                        if ($verb.Connection.UpgradeToSsl) {
                            $Request.UpgradeToSSL()
                        }
                    }
                    catch [System.OperationCanceledException] {}
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                }
                finally {
                    $TcpEvent = $null
                    Close-PodeDisposable -Disposable $context
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    # start the runspace for listening on x-number of threads
    1..$PodeContext.Threads.General | ForEach-Object {
        Add-PodeRunspace -Type Tcp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
    }

    # script to keep tcp server listening until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Listener
        }
    }

    Add-PodeRunspace -Type Tcp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile

    # state where we're running
    return @(foreach ($endpoint in $endpoints) {
            @{
                Url      = $endpoint.Url
                Pool     = $endpoint.Pool
                DualMode = $endpoint.DualMode
            }
        })
}
src\Private\Timers.ps1
function Find-PodeTimer {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeContext.Timers.Items[$Name]
}

function Test-PodeTimersExist {
    return (($null -ne $PodeContext.Timers) -and (($PodeContext.Timers.Enabled) -or ($PodeContext.Timers.Items.Count -gt 0)))
}

function Start-PodeTimerRunspace {
    if (!(Test-PodeTimersExist)) {
        return
    }

    $script = {
        while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
            $_now = [DateTime]::Now

            # only run timers that haven't completed, and have a next trigger in the past
            $PodeContext.Timers.Items.Values | Where-Object {
                !$_.Completed -and ($_.OnStart -or ($_.NextTriggerTime -le $_now))
            } | ForEach-Object {
                $_.OnStart = $false
                $_.Count++

                # set last trigger to current next trigger
                if ($null -ne $_.NextTriggerTime) {
                    $_.LastTriggerTime = $_.NextTriggerTime
                }
                else {
                    $_.LastTriggerTime = [datetime]::Now
                }

                # has the timer completed?
                if (($_.Limit -gt 0) -and ($_.Count -ge $_.Limit)) {
                    $_.Completed = $true
                }

                # next trigger
                if (!$_.Completed) {
                    $_.NextTriggerTime = $_now.AddSeconds($_.Interval)
                }
                else {
                    $_.NextTriggerTime = $null
                }

                # run the timer
                Invoke-PodeInternalTimer -Timer $_
            }

            Start-Sleep -Seconds 1
        }
    }

    Add-PodeRunspace -Type Main -ScriptBlock $script
}

function Invoke-PodeInternalTimer {
    param(
        [Parameter(Mandatory = $true)]
        $Timer,

        [Parameter()]
        [object[]]
        $ArgumentList = $null
    )

    try {
        $global:TimerEvent = @{
            Lockable = $PodeContext.Threading.Lockables.Global
            Sender   = $Timer
            Metadata = @{}
        }

        # add main timer args
        $_args = @()
        if (($null -ne $Timer.Arguments) -and ($Timer.Arguments.Length -gt 0)) {
            $_args += $Timer.Arguments
        }

        # add adhoc timer invoke args
        if (($null -ne $ArgumentList) -and ($ArgumentList.Length -gt 0)) {
            $_args += $ArgumentList
        }

        # invoke timer
        $null = Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat
    }
    catch {
        $_ | Write-PodeErrorLog
    }
}
src\Private\Verbs.ps1
function Find-PodeVerb {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Verb,

        [Parameter()]
        [string]
        $EndpointName
    )

    # if we have a perfect match for the verb, return it
    $found = Get-PodeVerbByLiteral -Verbs $PodeContext.Server.Verbs[$Verb] -EndpointName $EndpointName
    if ($null -ne $found) {
        return $found
    }

    # otherwise, match regex on the verbs (first match only)
    $valid = @(foreach ($key in $PodeContext.Server.Verbs.Keys) {
            if (($key -ine '*') -and ($Verb -imatch "^$($key)$")) {
                $key
                break
            }
        })[0]

    if ($null -eq $valid) {
        return $null
    }

    # is the verb valid for any protocols/endpoints?
    $found = Get-PodeVerbByLiteral -Verbs $PodeContext.Server.Verbs[$valid] -EndpointName $EndpointName
    if ($null -eq $found) {
        return $null
    }

    return $found
}

function Get-PodeVerbByLiteral {
    param(
        [Parameter()]
        [hashtable[]]
        $Verbs,

        [Parameter()]
        [string]
        $EndpointName
    )

    # if verbs is already null/empty just return
    if (($null -eq $Verbs) -or ($Verbs.Length -eq 0)) {
        return $null
    }

    # get the verb
    return (Get-PodeVerbsByLiteral -Verbs $Verbs -EndpointName $EndpointName)
}

function Get-PodeVerbsByLiteral {
    param(
        [Parameter()]
        [hashtable[]]
        $Verbs,

        [Parameter()]
        [string]
        $EndpointName
    )

    # see if a verb has the endpoint name
    if (![string]::IsNullOrWhiteSpace($EndpointName)) {
        foreach ($verb in $Verbs) {
            if ($verb.Endpoint.Name -ieq $EndpointName) {
                return $verb
            }
        }
    }

    # else find first default verb
    foreach ($verb in $Verbs) {
        if ([string]::IsNullOrWhiteSpace($verb.Endpoint.Name)) {
            return $verb
        }
    }

    return $null
}

function Test-PodeVerbAndError {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Verb,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Address
    )

    $found = @($PodeContext.Server.Verbs[$Verb])

    if (($found | Where-Object { ($_.Endpoint.Protocol -ieq $Protocol) -and ($_.Endpoint.Address -ieq $Address) } | Measure-Object).Count -eq 0) {
        return
    }

    $_url = $Protocol
    if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Address)) {
        $_url = "$($_url)://$($Address)"
    }
    elseif (![string]::IsNullOrWhiteSpace($Address)) {
        $_url = $Address
    }

    if ([string]::IsNullOrEmpty($_url)) {
        throw "[Verb] $($Verb): Already defined"
    }
    else {
        throw "[Verb] $($Verb): Already defined for $($_url)"
    }
}
src\Private\WebSockets.ps1
using namespace Pode

function Test-PodeWebSocketsExist {
    return (($null -ne $PodeContext.Server.WebSockets) -and (($PodeContext.Server.WebSockets.Enabled) -or ($PodeContext.Server.WebSockets.Connections.Count -gt 0)))
}

function Find-PodeWebSocket {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.WebSockets.Connections[$Name]
}

function New-PodeWebSocketReceiver {
    if ($null -ne $PodeContext.Server.WebSockets.Receiver) {
        return
    }

    try {
        $receiver = [PodeReceiver]::new($PodeContext.Tokens.Cancellation.Token)
        $receiver.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
        $receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
        $PodeContext.Server.WebSockets.Receiver = $receiver
        $PodeContext.Receivers += $receiver
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $receiver
        throw $_.Exception
    }
}

function Start-PodeWebSocketRunspace {
    if (!(Test-PodeWebSocketsExist)) {
        return
    }

    # script for listening out of for incoming requests
    $receiveScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Receiver,

            [Parameter(Mandatory = $true)]
            [int]
            $ThreadId
        )

        try {
            while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                # get request
                $request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token))

                try {
                    try {
                        $WsEvent = @{
                            Request   = $request
                            Data      = $null
                            Files     = $null
                            Lockable  = $PodeContext.Threading.Lockables.Global
                            Timestamp = [datetime]::UtcNow
                            Metadata  = @{}
                        }

                        # find the websocket definition
                        $websocket = Find-PodeWebSocket -Name $request.WebSocket.Name
                        if ($null -eq $websocket.Logic) {
                            continue
                        }

                        # parse data
                        $result = ConvertFrom-PodeRequestContent -Request $request -ContentType $request.WebSocket.ContentType
                        $WsEvent.Data = $result.Data
                        $WsEvent.Files = $result.Files

                        # invoke websocket script
                        $null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat
                    }
                    catch [System.OperationCanceledException] {}
                    catch {
                        $_ | Write-PodeErrorLog
                        $_.Exception | Write-PodeErrorLog -CheckInnerException
                    }
                }
                finally {
                    $WsEvent = $null
                    Close-PodeDisposable -Disposable $request
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    # start the runspace for listening on x-number of threads
    1..$PodeContext.Threads.WebSockets | ForEach-Object {
        Add-PodeRunspace -Type WebSockets -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ }
    }

    # script to keep websocket server receiving until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory = $true)]
            [ValidateNotNull()]
            $Receiver
        )

        try {
            while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Receiver
        }
    }

    Add-PodeRunspace -Type WebSockets -ScriptBlock $waitScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver } -NoProfile
}
src\Public\Access.ps1
<#
.SYNOPSIS
Create a new type of Access scheme.

.DESCRIPTION
Create a new type of Access scheme, which retrieves the destination/resource's authorisation values which a user needs for access.

.PARAMETER Type
The inbuilt Type of Access this method is for: Role, Group, Scope, User.

.PARAMETER Custom
If supplied, the access Scheme will be flagged as using Custom logic.

.PARAMETER ScriptBlock
An optional ScriptBlock for retrieving authorisation values for the authenticated user, useful if the values reside in an external data store.
This, or Path, is mandatory if using a Custom scheme.

.PARAMETER ArgumentList
An optional array of arguments to supply to the ScriptBlock.

.PARAMETER Path
An optional property Path within the $WebEvent.Auth.User object to extract authorisation values.
The default Path is based on the Access Type, either Roles; Groups; Scopes; or Username.
This, or ScriptBlock, is mandatory if using a Custom scheme.

.EXAMPLE
$role_access = New-PodeAccessScheme -Type Role

.EXAMPLE
$group_access = New-PodeAccessScheme -Type Group -Path 'Metadata.Groups'

.EXAMPLE
$scope_access = New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) }

.EXAMPLE
$custom_access = New-PodeAccessScheme -Custom -Path 'CustomProp'
#>
function New-PodeAccessScheme {
    [CmdletBinding(DefaultParameterSetName = 'Type_Path')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Type_Scriptblock')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Type_Path')]
        [ValidateSet('Role', 'Group', 'Scope', 'User')]
        [string]
        $Type,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')]
        [switch]
        $Custom,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')]
        [Parameter(ParameterSetName = 'Type_Scriptblock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'Custom_Scriptblock')]
        [Parameter(ParameterSetName = 'Type_Scriptblock')]
        [object[]]
        $ArgumentList,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')]
        [Parameter(ParameterSetName = 'Type_Path')]
        [string]
        $Path
    )

    # for custom access a validator is mandatory
    if ($Custom) {
        if ([string]::IsNullOrWhiteSpace($Path) -and (Test-PodeIsEmpty $ScriptBlock)) {
            throw 'A Path or ScriptBlock is required for sourcing the Custom access values'
        }
    }

    # parse using variables in scriptblock
    $scriptObj = $null
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $scriptObj = @{
            Script         = $ScriptBlock
            UsingVariables = $usingScriptVars
        }
    }

    # default path
    if (!$Custom -and (Test-PodeIsEmpty $ScriptBlock) -and [string]::IsNullOrWhiteSpace($Path)) {
        if ($Type -ieq 'user') {
            $Path = 'Username'
        }
        else {
            $Path = "$($Type)s"
        }
    }

    # return scheme
    return @{
        Type        = $Type
        IsCustom    = $Custom.IsPresent
        ScriptBlock = $scriptObj
        Arguments   = $ArgumentList
        Path        = $Path
    }
}

<#
.SYNOPSIS
Add an authorisation Access method.

.DESCRIPTION
Add an authorisation Access method for use with Authentication methods, which will authorise access to Routes.
Or they can be used independant of Authentication/Routes for custom scenarios.

.PARAMETER Name
A unique Name for the Access method.

.PARAMETER Description
A short description used by OpenAPI.

.PARAMETER Scheme
The access Scheme to use for retrieving credentials (From New-PodeAccessScheme).

.PARAMETER ScriptBlock
An optional Scriptblock, which can be used to invoke custom validation logic to verify authorisation.

.PARAMETER ArgumentList
An optional array of arguments to supply to the ScriptBlock.

.PARAMETER Match
An optional inbuilt Match method to use when verifying access to a Route, this only applies when no custom Validator scriptblock is supplied. (Default: One)
"One" will allow access if the User has at least one of the Route's access values.
"All" will allow access only if the User has all the values.
"None" will allow access only if the User has none of the values.

.EXAMPLE
New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'Example'

.EXAMPLE
New-PodeAccessScheme -Type Group -Path 'Metadata.Groups' | Add-PodeAccess -Name 'Example' -Match All

.EXAMPLE
New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) } | Add-PodeAccess -Name 'Example'

.EXAMPLE
New-PodeAccessScheme -Custom -Path 'CustomProp' | Add-PodeAccess -Name 'Example' -ScriptBlock { param($userAccess, $customAccess) return $userAccess.Country -ieq $customAccess.Country }
#>
function Add-PodeAccess {
    [CmdletBinding(DefaultParameterSetName = 'Match')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [string]
        $Description,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Scheme,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [object[]]
        $ArgumentList,

        [Parameter(ParameterSetName = 'Match')]
        [ValidateSet('All', 'One', 'None')]
        [string]
        $Match = 'One'
    )

    # check name unique
    if (Test-PodeAccessExists -Name $Name) {
        throw "Access method already defined: $($Name)"
    }

    # parse using variables in validator scriptblock
    $scriptObj = $null
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $scriptObj = @{
            Script         = $ScriptBlock
            UsingVariables = $usingScriptVars
        }
    }

    # add access object
    $PodeContext.Server.Authorisations.Methods[$Name] = @{
        Name        = $Name
        Description = $Description
        Scheme      = $Scheme
        ScriptBlock = $scriptObj
        Arguments   = $ArgumentList
        Match       = $Match.ToLowerInvariant()
        Cache       = @{}
        Merged      = $false
        Parent      = $null
    }
}

<#
.SYNOPSIS
Let's you merge multiple Access methods together, into a "single" Access method.

.DESCRIPTION
Let's you merge multiple Access methods together, into a "single" Access method.
You can specify if only One or All of the methods need to pass to allow access, and you can also
merge other merged Access methods for more advanced scenarios.

.PARAMETER Name
A unique Name for the Access method.

.PARAMETER Access
Mutliple Access method Names to be merged.

.PARAMETER Valid
How many of the Access methods are required to be valid, One or All. (Default: One)

.EXAMPLE
Merge-PodeAccess -Name MergedAccess -Access RbacAccess, GbacAccess -Valid All
#>
function Merge-PodeAccess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string[]]
        $Access,

        [Parameter()]
        [ValidateSet('One', 'All')]
        [string]
        $Valid = 'One'
    )

    # ensure the name doesn't already exist
    if (Test-PodeAccessExists -Name $Name) {
        throw "Access method already defined: $($Name)"
    }

    # ensure all the access methods exist
    foreach ($accName in $Access) {
        if (!(Test-PodeAccessExists -Name $accName)) {
            throw "Access method does not exist for merging: $($accName)"
        }
    }

    # set parent access
    foreach ($accName in $Access) {
        $PodeContext.Server.Authorisations.Methods[$accName].Parent = $Name
    }

    # add auth method to server
    $PodeContext.Server.Authorisations.Methods[$Name] = @{
        Name    = $Name
        Access  = @($Access)
        PassOne = ($Valid -ieq 'one')
        Cache   = @{}
        Merged  = $true
        Parent  = $null
    }
}

<#
.SYNOPSIS
Assigns Custom Access value(s) to a Route.

.DESCRIPTION
Assigns Custom Access value(s) to a Route.

.PARAMETER Route
The Route to assign the Custom Access value(s).

.PARAMETER Name
The Name of the Access method the Custom Access value(s) are for.

.PARAMETER Value
The Custom Access Value(s)

.EXAMPLE
Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {} -PassThru | Add-PodeAccessCustom -Name 'Example' -Value @{ Country = 'UK' }
#>
function Add-PodeAccessCustom {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [object[]]
        $Value
    )

    begin {
        $routes = @()
    }

    process {
        $routes += $Route
    }

    end {
        foreach ($r in $routes) {
            if ($r.AccessMeta.Custom.ContainsKey($Name)) {
                throw "Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'"
            }

            $r.AccessMeta.Custom[$Name] = $Value
        }
    }
}

<#
.SYNOPSIS
Get one or more Access methods.

.DESCRIPTION
Get one or more Access methods.

.PARAMETER Name
The Name of the Access method. If no name supplied, all methods will be returned.

.EXAMPLE
$methods = Get-PodeAccess

.EXAMPLE
$methods = Get-PodeAccess -Name 'Example'

.EXAMPLE
$methods = Get-PodeAccess -Name 'Example1', 'Example2'
#>
function Get-PodeAccess {
    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    # return all if no Name
    if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) {
        return $PodeContext.Server.Authorisations.Methods.Values
    }

    # return filtered
    return @(foreach ($n in $Name) {
            $PodeContext.Server.Authorisations.Methods[$n]
        })
}

<#
.SYNOPSIS
Test if an Access method exists.

.DESCRIPTION
Test if an Access method exists.

.PARAMETER Name
The Name of the Access method.

.EXAMPLE
if (Test-PodeAccessExists -Name 'Example') { }
#>
function Test-PodeAccessExists {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Authorisations.Methods.ContainsKey($Name)
}

<#
.SYNOPSIS
Test access values for a Source/Destination against an Access method.

.DESCRIPTION
Test access values for a Source/Destination against an Access method.

.PARAMETER Name
The Name of the Access method to use to verify the access.

.PARAMETER Source
An array of Source access values to pass to the Access method for verification against the Destination access values. (ie: User)

.PARAMETER Destination
An array of Destination access values to pass to the Access method for verification. (ie: Route)

.PARAMETER ArgumentList
An optional array of arguments to supply to the Access Scheme's ScriptBlock for retrieving access values.

.EXAMPLE
if (Test-PodeAccess -Name 'Example' -Source 'Developer' -Destination 'Admin') { }
#>
function Test-PodeAccess {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [object[]]
        $Source = $null,

        [Parameter()]
        [object[]]
        $Destination = $null,

        [Parameter()]
        [object[]]
        $ArgumentList = $null
    )

    # get the access method
    $access = $PodeContext.Server.Authorisations.Methods[$Name]

    # authorised if no destination values
    if (($null -eq $Destination) -or ($Destination.Length -eq 0)) {
        return $true
    }

    # if we have no source values, invoke the scriptblock
    if (($null -eq $Source) -or ($Source.Length -eq 0)) {
        if ($null -ne $access.Scheme.ScriptBlock) {
            $_args = $ArgumentList + @($access.Scheme.Arguments)
            $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat
        }
    }

    # check for custom validator, or use default match logic
    if ($null -ne $access.ScriptBlock) {
        $_args = @(, $Source) + @(, $Destination) + @($access.Arguments)
        return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -UsingVariables $access.ScriptBlock.UsingVariables -Return -Splat)
    }

    # not authorised if no source values
    if (($access.Match -ne 'none') -and (($null -eq $Source) -or ($Source.Length -eq 0))) {
        return $false
    }

    # one or all match?
    else {
        switch ($access.Match) {
            'one' {
                foreach ($item in $Source) {
                    if ($item -iin $Destination) {
                        return $true
                    }
                }
            }

            'all' {
                foreach ($item in $Destination) {
                    if ($item -inotin $Source) {
                        return $false
                    }
                }

                return $true
            }

            'none' {
                foreach ($item in $Source) {
                    if ($item -iin $Destination) {
                        return $false
                    }
                }

                return $true
            }
        }
    }

    # default is not authorised
    return $false
}

<#
.SYNOPSIS
Test the currently authenticated User's access against the supplied values.

.DESCRIPTION
Test the currently authenticated User's access against the supplied values. This will be the user in a WebEvent object.

.PARAMETER Name
The Name of the Access method to use to verify the access.

.PARAMETER Value
An array of access values to pass to the Access method for verification against the User.

.EXAMPLE
if (Test-PodeAccessUser -Name 'Example' -Value 'Developer', 'QA') { }
#>
function Test-PodeAccessUser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [object[]]
        $Value
    )

    # get the access method
    $access = $PodeContext.Server.Authorisations.Methods[$Name]

    # get the user
    $user = $WebEvent.Auth.User

    # if there's no scriptblock, try the Path fallback
    if ($null -eq $access.Scheme.Scriptblock) {
        $userAccess = $user
        foreach ($atom in $access.Scheme.Path.Split('.')) {
            $userAccess = $userAccess.($atom)
        }
    }

    # otherwise, invoke scriptblock
    else {
        $_args = @($user) + @($access.Scheme.Arguments)
        $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat
    }

    # is the user authorised?
    return (Test-PodeAccess -Name $Name -Source $userAccess -Destination $Value)
}

<#
.SYNOPSIS
Test the currently authenticated User's access against the access values supplied for the current Route.

.DESCRIPTION
Test the currently authenticated User's access against the access values supplied for the current Route.

.PARAMETER Name
The Name of the Access method to use to verify the access.

.EXAMPLE
if (Test-PodeAccessRoute -Name 'Example') { }
#>
function Test-PodeAccessRoute {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # get the access method
    $access = $PodeContext.Server.Authorisations.Methods[$Name]

    # get route access values
    if ($access.Scheme.IsCustom) {
        $routeAccess = $WebEvent.Route.AccessMeta.Custom[$access.Name]
    }
    else {
        $routeAccess = $WebEvent.Route.AccessMeta[$access.Scheme.Type]
    }

    # if no values then skip
    if (($null -eq $routeAccess) -or ($routeAccess.Length -eq 0)) {
        return $true
    }

    # tests values against user
    return (Test-PodeAccessUser -Name $Name -Value $routeAccess)
}

<#
.SYNOPSIS
Remove a specific Access method.

.DESCRIPTION
Remove a specific Access method.

.PARAMETER Name
The Name of the Access method.

.EXAMPLE
Remove-PodeAccess -Name 'RBAC'
#>
function Remove-PodeAccess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Server.Authorisations.Methods.Remove($Name)
}

<#
.SYNOPSIS
Clear all defined Access methods.

.DESCRIPTION
Clear all defined Access methods.

.EXAMPLE
Clear-PodeAccess
#>
function Clear-PodeAccess {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Authorisations.Methods.Clear()
}

<#
.SYNOPSIS
Adds an access method as global middleware.

.DESCRIPTION
Adds an access method as global middleware.

.PARAMETER Name
The Name of the Middleware.

.PARAMETER Access
The Name of the Access method to use.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.EXAMPLE
Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName

.EXAMPLE
Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName -Route '/api/*'
#>
function Add-PodeAccessMiddleware {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Access,

        [Parameter()]
        [string]
        $Route
    )

    if (!(Test-PodeAccessExists -Name $Access)) {
        throw "Access method does not exist: $($Access)"
    }

    Get-PodeAccessMiddlewareScript |
        New-PodeMiddleware -ArgumentList @{ Name = $Access } |
        Add-PodeMiddleware -Name $Name -Route $Route
}

<#
.SYNOPSIS
Automatically loads access ps1 files

.DESCRIPTION
Automatically loads access ps1 files from either an /access folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeAccess

.EXAMPLE
Use-PodeAccess -Path './my-access'
#>
function Use-PodeAccess {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'access'
}
src\Public\Authentication.ps1
<#
.SYNOPSIS
Create a new type of Authentication scheme.

.DESCRIPTION
Create a new type of Authentication scheme, which is used to parse the Request for user credentials for validating.

.PARAMETER Basic
If supplied, will use the inbuilt Basic Authentication credentials retriever.

.PARAMETER Encoding
The Encoding to use when decoding the Basic Authorization header.

.PARAMETER HeaderTag
The Tag name used in the Authorization header, ie: Basic, Bearer, Digest.

.PARAMETER Form
If supplied, will use the inbuilt Form Authentication credentials retriever.

.PARAMETER UsernameField
The name of the Username Field in the payload to retrieve the username.

.PARAMETER PasswordField
The name of the Password Field in the payload to retrieve the password.

.PARAMETER Custom
If supplied, will allow you to create a Custom Authentication credentials retriever.

.PARAMETER ScriptBlock
The ScriptBlock is used to parse the request and retieve user credentials and other information.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication type's ScriptBlock.

.PARAMETER Name
The Name of an Authentication type - such as Basic or NTLM.

.PARAMETER Description
A short description for security scheme. CommonMark syntax MAY be used for rich text representation

.PARAMETER Realm
The name of scope of the protected area.

.PARAMETER Type
The scheme type for custom Authentication types. Default is HTTP.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER PostValidator
The PostValidator is a scriptblock that is invoked after user validation.

.PARAMETER Digest
If supplied, will use the inbuilt Digest Authentication credentials retriever.

.PARAMETER Bearer
If supplied, will use the inbuilt Bearer Authentication token retriever.

.PARAMETER ClientCertificate
If supplied, will use the inbuilt Client Certificate Authentication scheme.

.PARAMETER ClientId
The Application ID generated when registering a new app for OAuth2.

.PARAMETER ClientSecret
The Application Secret generated when registering a new app for OAuth2 (this is optional when using PKCE).

.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)

.PARAMETER AuthoriseUrl
The OAuth2 Authorisation URL to authenticate a User. This is optional if you're using an InnerScheme like Basic/Form.

.PARAMETER TokenUrl
The OAuth2 Token URL to acquire an access token.

.PARAMETER UserUrl
An optional User profile URL to retrieve a user's details - for OAuth2

.PARAMETER UserUrlMethod
An optional HTTP method to use when calling the User profile URL - for OAuth2 (Default: Post)

.PARAMETER CodeChallengeMethod
An optional method for sending a PKCE code challenge when calling the Authorise URL - for OAuth2 (Default: S256)

.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers - for OAuth2

.PARAMETER OAuth2
If supplied, will use the inbuilt OAuth2 Authentication scheme.

.PARAMETER Scope
An optional array of Scopes for Bearer/OAuth2 Authentication. (These are case-sensitive)

.PARAMETER ApiKey
If supplied, will use the inbuilt API key Authentication scheme.

.PARAMETER Location
The Location to find an API key: Header, Query, or Cookie. (Default: Header)

.PARAMETER LocationName
The Name of the Header, Query, or Cookie to find an API key. (Default depends on Location. Header/Cookie: X-API-KEY, Query: api_key)

.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.

.PARAMETER AsCredential
If supplied, username/password credentials for Basic/Form authentication will instead be supplied as a pscredential object.

.PARAMETER AsJWT
If supplied, the token/key supplied for Bearer/API key authentication will be parsed as a JWT, and the payload supplied instead.

.PARAMETER Secret
An optional Secret, used to sign/verify JWT signatures.

.EXAMPLE
$basic_auth = New-PodeAuthScheme -Basic

.EXAMPLE
$form_auth = New-PodeAuthScheme -Form -UsernameField 'Email'

.EXAMPLE
$custom_auth = New-PodeAuthScheme -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeAuthScheme {
    [CmdletBinding(DefaultParameterSetName = 'Basic')]
    [OutputType([hashtable])]
    param(
        [Parameter(ParameterSetName = 'Basic')]
        [switch]
        $Basic,

        [Parameter(ParameterSetName = 'Basic')]
        [string]
        $Encoding = 'ISO-8859-1',

        [Parameter(ParameterSetName = 'Basic')]
        [Parameter(ParameterSetName = 'Bearer')]
        [Parameter(ParameterSetName = 'Digest')]
        [string]
        $HeaderTag,

        [Parameter(ParameterSetName = 'Form')]
        [switch]
        $Form,

        [Parameter(ParameterSetName = 'Form')]
        [string]
        $UsernameField = 'username',

        [Parameter(ParameterSetName = 'Form')]
        [string]
        $PasswordField = 'password',

        [Parameter(ParameterSetName = 'Custom')]
        [switch]
        $Custom,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
        [ValidateScript({
                if (Test-PodeIsEmpty $_) {
                    throw 'A non-empty ScriptBlock is required for the Custom authentication scheme'
                }

                return $true
            })]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'Custom')]
        [hashtable]
        $ArgumentList,

        [Parameter(ParameterSetName = 'Custom')]
        [string]
        $Name,

        [string]
        $Description,

        [Parameter()]
        [string]
        $Realm,

        [Parameter(ParameterSetName = 'Custom')]
        [ValidateSet('ApiKey', 'Http', 'OAuth2', 'OpenIdConnect')]
        [string]
        $Type = 'Http',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName = 'Custom')]
        [scriptblock]
        $PostValidator = $null,

        [Parameter(ParameterSetName = 'Digest')]
        [switch]
        $Digest,

        [Parameter(ParameterSetName = 'Bearer')]
        [switch]
        $Bearer,

        [Parameter(ParameterSetName = 'ClientCertificate')]
        [switch]
        $ClientCertificate,

        [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)]
        [string]
        $ClientId,

        [Parameter(ParameterSetName = 'OAuth2')]
        [string]
        $ClientSecret,

        [Parameter(ParameterSetName = 'OAuth2')]
        [string]
        $RedirectUrl,

        [Parameter(ParameterSetName = 'OAuth2')]
        [string]
        $AuthoriseUrl,

        [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)]
        [string]
        $TokenUrl,

        [Parameter(ParameterSetName = 'OAuth2')]
        [string]
        $UserUrl,

        [Parameter(ParameterSetName = 'OAuth2')]
        [ValidateSet('Get', 'Post')]
        [string]
        $UserUrlMethod = 'Post',

        [Parameter(ParameterSetName = 'OAuth2')]
        [ValidateSet('plain', 'S256')]
        [string]
        $CodeChallengeMethod = 'S256',

        [Parameter(ParameterSetName = 'OAuth2')]
        [switch]
        $UsePKCE,

        [Parameter(ParameterSetName = 'OAuth2')]
        [switch]
        $OAuth2,

        [Parameter(ParameterSetName = 'ApiKey')]
        [switch]
        $ApiKey,

        [Parameter(ParameterSetName = 'ApiKey')]
        [ValidateSet('Header', 'Query', 'Cookie')]
        [string]
        $Location = 'Header',

        [Parameter(ParameterSetName = 'ApiKey')]
        [string]
        $LocationName,

        [Parameter(ParameterSetName = 'Bearer')]
        [Parameter(ParameterSetName = 'OAuth2')]
        [string[]]
        $Scope,

        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $InnerScheme,

        [Parameter(ParameterSetName = 'Basic')]
        [Parameter(ParameterSetName = 'Form')]
        [switch]
        $AsCredential,

        [Parameter(ParameterSetName = 'Bearer')]
        [Parameter(ParameterSetName = 'ApiKey')]
        [switch]
        $AsJWT,

        [Parameter(ParameterSetName = 'Bearer')]
        [Parameter(ParameterSetName = 'ApiKey')]
        [string]
        $Secret
    )

    # default realm
    $_realm = 'User'

    # convert any middleware into valid hashtables
    $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)

    # configure the auth scheme
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'basic' {
            return @{
                Name          = (Protect-PodeValue -Value $HeaderTag -Default 'Basic')
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthBasicType)
                    UsingVariables = $null
                }
                PostValidator = $null
                Middleware    = $Middleware
                InnerScheme   = $InnerScheme
                Scheme        = 'http'
                Arguments     = @{
                    Description  = $Description
                    HeaderTag    = (Protect-PodeValue -Value $HeaderTag -Default 'Basic')
                    Encoding     = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1')
                    AsCredential = $AsCredential
                }
            }
        }

        'clientcertificate' {
            return @{
                Name          = 'Mutual'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthClientCertificateType)
                    UsingVariables = $null
                }
                PostValidator = $null
                Middleware    = $Middleware
                InnerScheme   = $InnerScheme
                Scheme        = 'http'
                Arguments     = @{}
            }
        }

        'digest' {
            return @{
                Name          = 'Digest'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthDigestType)
                    UsingVariables = $null
                }
                PostValidator = @{
                    Script         = (Get-PodeAuthDigestPostValidator)
                    UsingVariables = $null
                }
                Middleware    = $Middleware
                InnerScheme   = $InnerScheme
                Scheme        = 'http'
                Arguments     = @{
                    HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest')
                }
            }
        }

        'bearer' {
            $secretBytes = $null
            if (![string]::IsNullOrWhiteSpace($Secret)) {
                $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
            }

            return @{
                Name          = 'Bearer'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthBearerType)
                    UsingVariables = $null
                }
                PostValidator = @{
                    Script         = (Get-PodeAuthBearerPostValidator)
                    UsingVariables = $null
                }
                Middleware    = $Middleware
                Scheme        = 'http'
                InnerScheme   = $InnerScheme
                Arguments     = @{
                    Description = $Description
                    HeaderTag   = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer')
                    Scopes      = $Scope
                    AsJWT       = $AsJWT
                    Secret      = $secretBytes
                }
            }
        }

        'form' {
            return @{
                Name          = 'Form'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthFormType)
                    UsingVariables = $null
                }
                PostValidator = $null
                Middleware    = $Middleware
                InnerScheme   = $InnerScheme
                Scheme        = 'http'
                Arguments     = @{
                    Description  = $Description
                    Fields       = @{
                        Username = (Protect-PodeValue -Value $UsernameField -Default 'username')
                        Password = (Protect-PodeValue -Value $PasswordField -Default 'password')
                    }
                    AsCredential = $AsCredential
                }
            }
        }

        'oauth2' {
            if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) {
                throw "OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: $($InnerScheme.Name)"
            }

            if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) {
                throw 'OAuth2 requires an Authorise URL to be supplied'
            }

            if ($UsePKCE -and !(Test-PodeSessionsEnabled)) {
                throw 'Sessions are required to use OAuth2 with PKCE'
            }

            if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) {
                throw 'OAuth2 requires a Client Secret when not using PKCE'
            }
            return @{
                Name          = 'OAuth2'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthOAuth2Type)
                    UsingVariables = $null
                }
                PostValidator = $null
                Middleware    = $Middleware
                Scheme        = 'oauth2'
                InnerScheme   = $InnerScheme
                Arguments     = @{
                    Description = $Description
                    Scopes      = $Scope
                    PKCE        = @{
                        Enabled       = $UsePKCE
                        CodeChallenge = @{
                            Method = $CodeChallengeMethod
                        }
                    }
                    Client      = @{
                        ID     = $ClientId
                        Secret = $ClientSecret
                    }
                    Urls        = @{
                        Redirect  = $RedirectUrl
                        Authorise = $AuthoriseUrl
                        Token     = $TokenUrl
                        User      = @{
                            Url    = $UserUrl
                            Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post')
                        }
                    }
                }
            }
        }

        'apikey' {
            # set default location name
            if ([string]::IsNullOrWhiteSpace($LocationName)) {
                $LocationName = (@{
                        Header = 'X-API-KEY'
                        Query  = 'api_key'
                        Cookie = 'X-API-KEY'
                    })[$Location]
            }

            $secretBytes = $null
            if (![string]::IsNullOrWhiteSpace($Secret)) {
                $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
            }

            return @{
                Name          = 'ApiKey'
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                ScriptBlock   = @{
                    Script         = (Get-PodeAuthApiKeyType)
                    UsingVariables = $null
                }
                PostValidator = $null
                Middleware    = $Middleware
                InnerScheme   = $InnerScheme
                Scheme        = 'apiKey'
                Arguments     = @{
                    Description  = $Description
                    Location     = $Location
                    LocationName = $LocationName
                    AsJWT        = $AsJWT
                    Secret       = $secretBytes
                }
            }
        }

        'custom' {
            $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

            if ($null -ne $PostValidator) {
                $PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState
            }

            return @{
                Name          = $Name
                Realm         = (Protect-PodeValue -Value $Realm -Default $_realm)
                InnerScheme   = $InnerScheme
                Scheme        = $Type.ToLowerInvariant()
                ScriptBlock   = @{
                    Script         = $ScriptBlock
                    UsingVariables = $usingScriptVars
                }
                PostValidator = @{
                    Script         = $PostValidator
                    UsingVariables = $usingPostVars
                }
                Middleware    = $Middleware
                Arguments     = $ArgumentList
            }
        }
    }
}

<#
.SYNOPSIS
Create an OAuth2 auth scheme for Azure AD.

.DESCRIPTION
A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Azure AD.

.PARAMETER Tenant
The Directory/Tenant ID from registering a new app (default: common).

.PARAMETER ClientId
The Client ID from registering a new app.

.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).

.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)

.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.

.EXAMPLE
New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -ClientSecret 1234.abc

.EXAMPLE
New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -UsePKCE
#>
function New-PodeAuthAzureADScheme {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Tenant = 'common',

        [Parameter(Mandatory = $true)]
        [string]
        $ClientId,

        [Parameter()]
        [string]
        $ClientSecret,

        [Parameter()]
        [string]
        $RedirectUrl,

        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $InnerScheme,

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $UsePKCE
    )

    return New-PodeAuthScheme `
        -OAuth2 `
        -ClientId $ClientId `
        -ClientSecret $ClientSecret `
        -AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" `
        -TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" `
        -UserUrl 'https://graph.microsoft.com/oidc/userinfo' `
        -RedirectUrl $RedirectUrl `
        -InnerScheme $InnerScheme `
        -Middleware $Middleware `
        -UsePKCE:$UsePKCE
}

<#
.SYNOPSIS
Create an OAuth2 auth scheme for Twitter.

.DESCRIPTION
A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Twitter apps.

.PARAMETER ClientId
The Client ID from registering a new app.

.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).

.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.

.EXAMPLE
New-PodeAuthTwitterScheme -ClientId some_id -ClientSecret 1234.abc

.EXAMPLE
New-PodeAuthTwitterScheme -ClientId some_id -UsePKCE
#>
function New-PodeAuthTwitterScheme {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $ClientId,

        [Parameter()]
        [string]
        $ClientSecret,

        [Parameter()]
        [string]
        $RedirectUrl,

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $UsePKCE
    )

    return New-PodeAuthScheme `
        -OAuth2 `
        -ClientId $ClientId `
        -ClientSecret $ClientSecret `
        -AuthoriseUrl 'https://twitter.com/i/oauth2/authorize' `
        -TokenUrl 'https://api.twitter.com/2/oauth2/token' `
        -UserUrl 'https://api.twitter.com/2/users/me' `
        -UserUrlMethod 'Get' `
        -RedirectUrl $RedirectUrl `
        -Middleware $Middleware `
        -Scope 'tweet.read', 'users.read' `
        -UsePKCE:$UsePKCE
}

<#
.SYNOPSIS
Adds a custom Authentication method for verifying users.

.DESCRIPTION
Adds a custom Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Scheme
The authentication Scheme to use for retrieving credentials (From New-PodeAuthScheme).

.PARAMETER ScriptBlock
The ScriptBlock defining logic that retrieves and verifys a user.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication's ScriptBlock.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeAuth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Scheme,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if (Test-PodeIsEmpty $_) {
                    throw 'A non-empty ScriptBlock is required for the authentication method'
                }

                return $true
            })]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [switch]
        $Sessionless,

        [switch]
        $SuccessUseOrigin
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied '$($Scheme.Name)' Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add auth method to server
    $PodeContext.Server.Authentications.Methods[$Name] = @{
        Name           = $Name
        Scheme         = $Scheme
        ScriptBlock    = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = $ArgumentList
        Sessionless    = $Sessionless.IsPresent
        Failure        = @{
            Url     = $FailureUrl
            Message = $FailureMessage
        }
        Success        = @{
            Url       = $SuccessUrl
            UseOrigin = $SuccessUseOrigin.IsPresent
        }
        Cache          = @{}
        Merged         = $false
        Parent         = $null
    }

    # if the scheme is oauth2, and there's no redirect, set up a default one
    if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) {
        $path = '/oauth2/callback'
        $Scheme.Arguments.Urls.Redirect = $path
        Add-PodeRoute -Method Get -Path $path -Authentication $Name
    }
}

<#
.SYNOPSIS
Lets you merge multiple Authentication methods together, into a "single" Authentication method.

.DESCRIPTION
Lets you merge multiple Authentication methods together, into a "single" Authentication method.
You can specify if only One or All of the methods need to pass to allow access, and you can also
merge other merged Authentication methods for more advanced scenarios.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Authentication
Multiple Autentication method Names to be merged.

.PARAMETER Valid
How many of the Authentication methods are required to be valid, One or All. (Default: One)

.PARAMETER ScriptBlock
This is mandatory, and only used, when $Valid=All. A scriptblock to merge the mutliple users/headers returned by valid authentications into 1 user/header objects.
This scriptblock will receive a hashtable of all result objects returned from Authentication methods. The key for the hashtable will be the authentication names that passed.

.PARAMETER Default
The Default Authentication method to use as a fallback for Failure URLs and other settings.

.PARAMETER MergeDefault
The Default Authentication method's User details result object to use, when $Valid=All.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
This will be used as fallback for the merged Authentication methods if not set on them.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.
This will be used as fallback for the merged Authentication methods if not set on them.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
This will be used as fallback for the merged Authentication methods if not set on them.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
This will be used as fallback for the merged Authentication methods if not set on them.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
This will be used as fallback for the merged Authentication methods if not set on them.

.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -ScriptBlock { ... }

.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -MergeDefault BasicAuth

.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -FailureUrl 'http://localhost:8080/login'
#>
function Merge-PodeAuth {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [Alias('Auth')]
        [string[]]
        $Authentication,

        [Parameter()]
        [ValidateSet('One', 'All')]
        [string]
        $Valid = 'One',

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $Default,

        [Parameter(ParameterSetName = 'MergeDefault')]
        [string]
        $MergeDefault,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [switch]
        $Sessionless,

        [switch]
        $SuccessUseOrigin
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "Authentication method already defined: $($Name)"
    }

    # ensure all the auth methods exist
    foreach ($authName in $Authentication) {
        if (!(Test-PodeAuthExists -Name $authName)) {
            throw "Authentication method does not exist for merging: $($authName)"
        }
    }

    # ensure the merge default is in the auth list
    if (![string]::IsNullOrEmpty($MergeDefault) -and ($MergeDefault -inotin @($Authentication))) {
        throw "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied"
    }

    # ensure the default is in the auth list
    if (![string]::IsNullOrEmpty($Default) -and ($Default -inotin @($Authentication))) {
        throw "the Default Authentication '$($Default)' is not in the Authentication list supplied"
    }

    # set default
    if ([string]::IsNullOrEmpty($Default)) {
        $Default = $Authentication[0]
    }

    # get auth for default
    $tmpAuth = $PodeContext.Server.Authentications.Methods[$Default]

    # check sessionless from default
    if (!$Sessionless) {
        $Sessionless = $tmpAuth.Sessionless
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # check failure url from default
    if ([string]::IsNullOrEmpty($FailureUrl)) {
        $FailureUrl = $tmpAuth.Failure.Url
    }

    # check failure message from default
    if ([string]::IsNullOrEmpty($FailureMessage)) {
        $FailureMessage = $tmpAuth.Failure.Message
    }

    # check success url from default
    if ([string]::IsNullOrEmpty($SuccessUrl)) {
        $SuccessUrl = $tmpAuth.Success.Url
    }

    # check success use origin from default
    if (!$SuccessUseOrigin) {
        $SuccessUseOrigin = $tmpAuth.Success.UseOrigin
    }

    # deal with using vars in scriptblock
    if (($Valid -ieq 'all') -and [string]::IsNullOrEmpty($MergeDefault)) {
        if ($null -eq $ScriptBlock) {
            throw 'A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All'
        }

        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }
    else {
        if ($null -ne $ScriptBlock) {
            Write-Warning -Message 'The Scriptblock for merged authentications, when Valid=One, will be ignored'
        }
    }

    # set parent auth
    foreach ($authName in $Authentication) {
        $PodeContext.Server.Authentications.Methods[$authName].Parent = $Name
    }

    # add auth method to server
    $PodeContext.Server.Authentications.Methods[$Name] = @{
        Name            = $Name
        Authentications = @($Authentication)
        PassOne         = ($Valid -ieq 'one')
        ScriptBlock     = @{
            Script         = $ScriptBlock
            UsingVariables = $usingVars
        }
        Default         = $Default
        MergeDefault    = $MergeDefault
        Sessionless     = $Sessionless.IsPresent
        Failure         = @{
            Url     = $FailureUrl
            Message = $FailureMessage
        }
        Success         = @{
            Url       = $SuccessUrl
            UseOrigin = $SuccessUseOrigin.IsPresent
        }
        Cache           = @{}
        Merged          = $true
        Parent          = $null
    }
}

<#
.SYNOPSIS
Gets an Authentication method.

.DESCRIPTION
Gets an Authentication method.

.PARAMETER Name
The Name of an Authentication method.

.EXAMPLE
Get-PodeAuth -Name 'Main'
#>
function Get-PodeAuth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # ensure the name exists
    if (!(Test-PodeAuthExists -Name $Name)) {
        throw "Authentication method not defined: $($Name)"
    }

    # get auth method
    return $PodeContext.Server.Authentications.Methods[$Name]
}

<#
.SYNOPSIS
Test if an Authentication method exists.

.DESCRIPTION
Test if an Authentication method exists.

.PARAMETER Name
The Name of the Authentication method.

.EXAMPLE
if (Test-PodeAuthExists -Name BasicAuth) { ... }
#>
function Test-PodeAuthExists {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Authentications.Methods.ContainsKey($Name)
}

<#
.SYNOPSIS
Test and invoke an Authentication method to verify a user.

.DESCRIPTION
Test and invoke an Authentication method to verify a user. This will verify a user's credentials on the request.
When testing OAuth2 methods, the first attempt will trigger a redirect to the provider and $false will be returned.

.PARAMETER Name
The Name of the Authentication method.

.PARAMETER IgnoreSession
If supplied, authentication will be re-verified on each call even if a valid session exists on the request.

.EXAMPLE
if (Test-PodeAuth -Name 'BasicAuth') { ... }

.EXAMPLE
if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... }
#>
function Test-PodeAuth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $IgnoreSession
    )

    # if the session already has a user/isAuth'd, then skip auth - or allow anon
    if (!$IgnoreSession -and (Test-PodeSessionsInUse) -and (Test-PodeAuthUser)) {
        return $true
    }

    try {
        $result = Invoke-PodeAuthValidation -Name $Name
    }
    catch {
        $_ | Write-PodeErrorLog
        return $false
    }

    # did the auth force a redirect?
    if ($result.Redirected) {
        return $false
    }

    # if auth failed, set appropriate response headers/redirects
    if (!$result.Success) {
        return $false
    }

    # successful auth
    return $true
}

<#
.SYNOPSIS
Adds the inbuilt Windows AD Authentication method for verifying users.

.DESCRIPTION
Adds the inbuilt Windows AD Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).

.PARAMETER Fqdn
A custom FQDN for the DNS of the AD you wish to authenticate against. (Alias: Server)

.PARAMETER Domain
(Unix Only) A custom NetBIOS domain name that is prepended onto usernames that are missing it (<Domain>\<Username>).

.PARAMETER SearchBase
(Unix Only) An optional searchbase to refine the LDAP query. This should be the full distinguished name.

.PARAMETER Groups
An array of Group names to only allow access.

.PARAMETER Users
An array of Usernames to only allow access.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user in AD.

.PARAMETER DirectGroups
If supplied, only a user's direct groups will be retrieved rather than all groups recursively.

.PARAMETER OpenLDAP
If supplied, and on Windows, OpenLDAP will be used instead (this is the default for Linux/MacOS).

.PARAMETER ADModule
If supplied, and on Windows, the ActiveDirectory module will be used instead.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.PARAMETER KeepCredential
If suplied pode will save the AD credential as a PSCredential object in $WebEvent.Auth.User.Credential

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth'

.EXAMPLE
New-PodeAuthScheme -Basic | Add-PodeAuthWindowsAd -Name 'WinAuth' -Groups @('Developers')

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' -NoGroups

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'UnixAuth' -Server 'testdomain.company.com' -Domain 'testdomain'
#>
function Add-PodeAuthWindowsAd {
    [CmdletBinding(DefaultParameterSetName = 'Groups')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Scheme,

        [Parameter()]
        [Alias('Server')]
        [string]
        $Fqdn,

        [Parameter()]
        [string]
        $Domain,

        [Parameter()]
        [string]
        $SearchBase,

        [Parameter(ParameterSetName = 'Groups')]
        [string[]]
        $Groups,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $Sessionless,

        [Parameter(ParameterSetName = 'NoGroups')]
        [switch]
        $NoGroups,

        [Parameter(ParameterSetName = 'Groups')]
        [switch]
        $DirectGroups,

        [switch]
        $OpenLDAP,

        [switch]
        $ADModule,

        [switch]
        $SuccessUseOrigin,

        [switch]
        $KeepCredential
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "Windows AD Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # if AD module set, ensure we're on windows and the module is available, then import/export it
    if ($ADModule) {
        Import-PodeAuthADModule
    }

    # set server name if not passed
    if ([string]::IsNullOrWhiteSpace($Fqdn)) {
        $Fqdn = Get-PodeAuthDomainName

        if ([string]::IsNullOrWhiteSpace($Fqdn)) {
            throw 'No domain server name has been supplied for Windows AD authentication'
        }
    }

    # set the domain if not passed
    if ([string]::IsNullOrWhiteSpace($Domain)) {
        $Domain = ($Fqdn -split '\.')[0]
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # add Windows AD auth method to server
    $PodeContext.Server.Authentications.Methods[$Name] = @{
        Scheme      = $Scheme
        ScriptBlock = (Get-PodeAuthWindowsADMethod)
        Arguments   = @{
            Server         = $Fqdn
            Domain         = $Domain
            SearchBase     = $SearchBase
            Users          = $Users
            Groups         = $Groups
            NoGroups       = $NoGroups
            DirectGroups   = $DirectGroups
            KeepCredential = $KeepCredential
            Provider       = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule)
            ScriptBlock    = @{
                Script         = $ScriptBlock
                UsingVariables = $usingVars
            }
        }
        Sessionless = $Sessionless
        Failure     = @{
            Url     = $FailureUrl
            Message = $FailureMessage
        }
        Success     = @{
            Url       = $SuccessUrl
            UseOrigin = $SuccessUseOrigin
        }
        Cache       = @{}
        Merged      = $false
        Parent      = $null
    }
}

<#
.SYNOPSIS
Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests.

.DESCRIPTION
Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.EXAMPLE
Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login'
#>
function Add-PodeAuthSession {
    [CmdletBinding(DefaultParameterSetName = 'Groups')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $SuccessUseOrigin
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions have not been configured'
    }

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "Authentication method already defined: $($Name)"
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # create the auth scheme for getting the session
    $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock {
        param($options)

        # 401 if sessions not used
        if (!(Test-PodeSessionsInUse)) {
            Revoke-PodeSession
            return @{
                Message = 'Sessions are not being used'
                Code    = 401
            }
        }

        # 401 if no authenticated user
        if (!(Test-PodeAuthUser)) {
            Revoke-PodeSession
            return @{
                Message = 'Session not authenticated'
                Code    = 401
            }
        }

        # return user
        return @($WebEvent.Session.Data.Auth)
    }

    # add a custom auth method to return user back
    $method = {
        param($user, $options)
        $result = @{ User = $user }

        # call additional scriptblock if supplied
        if ($null -ne $options.ScriptBlock.Script) {
            $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables
        }

        # return user back
        return $result
    }

    $scheme | Add-PodeAuth `
        -Name $Name `
        -ScriptBlock $method `
        -FailureUrl $FailureUrl `
        -FailureMessage $FailureMessage `
        -SuccessUrl $SuccessUrl `
        -SuccessUseOrigin:$SuccessUseOrigin `
        -ArgumentList @{
        ScriptBlock = @{
            Script         = $ScriptBlock
            UsingVariables = $usingVars
        }
    }
}

<#
.SYNOPSIS
Remove a specific Authentication method.

.DESCRIPTION
Remove a specific Authentication method.

.PARAMETER Name
The Name of the Authentication method.

.EXAMPLE
Remove-PodeAuth -Name 'Login'
#>
function Remove-PodeAuth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Server.Authentications.Methods.Remove($Name)
}

<#
.SYNOPSIS
Clear all defined Authentication methods.

.DESCRIPTION
Clear all defined Authentication methods.

.EXAMPLE
Clear-PodeAuth
#>
function Clear-PodeAuth {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Authentications.Methods.Clear()
}

<#
.SYNOPSIS
Adds an authentication method as global middleware.

.DESCRIPTION
Adds an authentication method as global middleware.

.PARAMETER Name
The Name of the Middleware.

.PARAMETER Authentication
The Name of the Authentication method to use.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.PARAMETER OADefinitionTag
An array of string representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
Use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName

.EXAMPLE
Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName -Route '/api/*'
#>
function Add-PodeAuthMiddleware {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Route,

        [string[]]
        $OADefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag

    if (!(Test-PodeAuthExists -Name $Authentication)) {
        throw "Authentication method does not exist: $($Authentication)"
    }

    Get-PodeAuthMiddlewareScript |
        New-PodeMiddleware -ArgumentList @{ Name = $Authentication } |
        Add-PodeMiddleware -Name $Name -Route $Route

    Set-PodeOAGlobalAuth -DefinitionTag $DefinitionTag -Name $Authentication -Route $Route
}

<#
.SYNOPSIS
Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS.

.DESCRIPTION
Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Groups
An array of Group names to only allow access.

.PARAMETER Users
An array of Usernames to only allow access.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user in AD.

.PARAMETER DirectGroups
If supplied, only a user's direct groups will be retrieved rather than all groups recursively.

.PARAMETER ADModule
If supplied, and on Windows, the ActiveDirectory module will be used instead.

.PARAMETER NoLocalCheck
If supplied, Pode will not at attempt to retrieve local User/Group information for the authenticated user.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth'

.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth' -Groups @('Developers')

.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth' -NoGroups
#>
function Add-PodeAuthIIS {
    [CmdletBinding(DefaultParameterSetName = 'Groups')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Groups')]
        [string[]]
        $Groups,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $Sessionless,

        [Parameter(ParameterSetName = 'NoGroups')]
        [switch]
        $NoGroups,

        [Parameter(ParameterSetName = 'Groups')]
        [switch]
        $DirectGroups,

        [switch]
        $ADModule,

        [switch]
        $NoLocalCheck,

        [switch]
        $SuccessUseOrigin
    )

    # ensure we're on Windows!
    if (!(Test-PodeIsWindows)) {
        throw 'IIS Authentication support is for Windows only'
    }

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "IIS Authentication method already defined: $($Name)"
    }

    # if AD module set, ensure we're on windows and the module is available, then import/export it
    if ($ADModule) {
        Import-PodeAuthADModule
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # create the auth scheme for getting the token header
    $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock {
        param($options)

        $header = 'MS-ASPNETCORE-WINAUTHTOKEN'

        # fail if no header
        if (!(Test-PodeHeader -Name $header)) {
            return @{
                Message = "No $($header) header found"
                Code    = 401
            }
        }

        # return the header for validation
        $token = Get-PodeHeader -Name $header
        return @($token)
    }

    # add a custom auth method to validate the user
    $method = Get-PodeAuthWindowsADIISMethod

    $scheme | Add-PodeAuth `
        -Name $Name `
        -ScriptBlock $method `
        -FailureUrl $FailureUrl `
        -FailureMessage $FailureMessage `
        -SuccessUrl $SuccessUrl `
        -Sessionless:$Sessionless `
        -SuccessUseOrigin:$SuccessUseOrigin `
        -ArgumentList @{
        Users        = $Users
        Groups       = $Groups
        NoGroups     = $NoGroups
        DirectGroups = $DirectGroups
        Provider     = (Get-PodeAuthADProvider -ADModule:$ADModule)
        NoLocalCheck = $NoLocalCheck
        ScriptBlock  = @{
            Script         = $ScriptBlock
            UsingVariables = $usingVars
        }
    }
}

<#
.SYNOPSIS
Adds the inbuilt User File Authentication method for verifying users.

.DESCRIPTION
Adds the inbuilt User File Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).

.PARAMETER FilePath
A path to a users JSON file (Default: ./users.json)

.PARAMETER Groups
An array of Group names to only allow access.

.PARAMETER Users
An array of Usernames to only allow access.

.PARAMETER HmacSecret
An optional secret if the passwords are HMAC SHA256 hashed.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login'

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './custom/path/users.json'
#>
function Add-PodeAuthUserFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Scheme,

        [Parameter()]
        [string]
        $FilePath,

        [Parameter()]
        [string[]]
        $Groups,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter(ParameterSetName = 'Hmac')]
        [string]
        $HmacSecret,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $Sessionless,

        [switch]
        $SuccessUseOrigin
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "User File Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' User File authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # set the file path if not passed
    if ([string]::IsNullOrWhiteSpace($FilePath)) {
        $FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json'
    }
    else {
        $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve
    }

    # ensure the user file exists
    if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) {
        throw "The user file does not exist: $($FilePath)"
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # add Windows AD auth method to server
    $PodeContext.Server.Authentications.Methods[$Name] = @{
        Scheme      = $Scheme
        ScriptBlock = (Get-PodeAuthUserFileMethod)
        Arguments   = @{
            FilePath    = $FilePath
            Users       = $Users
            Groups      = $Groups
            HmacSecret  = $HmacSecret
            ScriptBlock = @{
                Script         = $ScriptBlock
                UsingVariables = $usingVars
            }
        }
        Sessionless = $Sessionless
        Failure     = @{
            Url     = $FailureUrl
            Message = $FailureMessage
        }
        Success     = @{
            Url       = $SuccessUrl
            UseOrigin = $SuccessUseOrigin
        }
        Cache       = @{}
        Merged      = $false
        Parent      = $null
    }
}

<#
.SYNOPSIS
Adds the inbuilt Windows Local User Authentication method for verifying users.

.DESCRIPTION
Adds the inbuilt Windows Local User Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).

.PARAMETER Groups
An array of Group names to only allow access.

.PARAMETER Users
An array of Usernames to only allow access.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.

.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user.

.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth'

.EXAMPLE
New-PodeAuthScheme -Basic | Add-PodeAuthWindowsLocal -Name 'WinAuth' -Groups @('Developers')

.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' -NoGroups
#>
function Add-PodeAuthWindowsLocal {
    [CmdletBinding(DefaultParameterSetName = 'Groups')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Scheme,

        [Parameter(ParameterSetName = 'Groups')]
        [string[]]
        $Groups,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $Sessionless,

        [Parameter(ParameterSetName = 'NoGroups')]
        [switch]
        $NoGroups,

        [switch]
        $SuccessUseOrigin
    )

    # ensure we're on Windows!
    if (!(Test-PodeIsWindows)) {
        throw 'Windows Local Authentication support is for Windows only'
    }

    # ensure the name doesn't already exist
    if (Test-PodeAuthExists -Name $Name) {
        throw "Windows Local Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' Windows Local authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # add Windows Local auth method to server
    $PodeContext.Server.Authentications.Methods[$Name] = @{
        Scheme      = $Scheme
        ScriptBlock = (Get-PodeAuthWindowsLocalMethod)
        Arguments   = @{
            Users       = $Users
            Groups      = $Groups
            NoGroups    = $NoGroups
            ScriptBlock = @{
                Script         = $ScriptBlock
                UsingVariables = $usingVars
            }
        }
        Sessionless = $Sessionless
        Failure     = @{
            Url     = $FailureUrl
            Message = $FailureMessage
        }
        Success     = @{
            Url       = $SuccessUrl
            UseOrigin = $SuccessUseOrigin
        }
        Cache       = @{}
        Merged      = $false
        Parent      = $null
    }
}

<#
.SYNOPSIS
Convert a Header/Payload into a JWT.

.DESCRIPTION
Convert a Header/Payload hashtable into a JWT, with the option to sign it.

.PARAMETER Header
A Hashtable containing the Header information for the JWT.

.PARAMETER Payload
A Hashtable containing the Payload information for the JWT.

.PARAMETER Secret
An Optional Secret for signing the JWT, should be a string or byte[]. This is mandatory if the Header algorithm isn't "none".

.EXAMPLE
ConvertTo-PodeJwt -Header @{ alg = 'none' } -Payload @{ sub = '123'; name = 'John' }

.EXAMPLE
ConvertTo-PodeJwt -Header @{ alg = 'hs256' } -Payload @{ sub = '123'; name = 'John' } -Secret 'abc'
#>
function ConvertTo-PodeJwt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Header,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $Payload,

        [Parameter()]
        $Secret = $null
    )

    # validate header
    if ([string]::IsNullOrWhiteSpace($Header.alg)) {
        throw 'No algorithm supplied in JWT Header'
    }

    # convert the header
    $header64 = ConvertTo-PodeBase64UrlValue -Value ($Header | ConvertTo-Json -Compress)

    # convert the payload
    $payload64 = ConvertTo-PodeBase64UrlValue -Value ($Payload | ConvertTo-Json -Compress)

    # combine
    $jwt = "$($header64).$($payload64)"

    # convert secret to bytes
    if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) {
        $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret)
    }

    # make the signature
    $sig = New-PodeJwtSignature -Algorithm $Header.alg -Token $jwt -SecretBytes $Secret

    # add the signature and return
    $jwt += ".$($sig)"
    return $jwt
}

<#
.SYNOPSIS
Convert and return the payload of a JWT token.

.DESCRIPTION
Convert and return the payload of a JWT token, verifying the signature by default with support to ignore the signature.

.PARAMETER Token
The JWT token.

.PARAMETER Secret
The Secret, as a string or byte[], to verify the token's signature.

.PARAMETER IgnoreSignature
Skip signature verification, and return the decoded payload.

.EXAMPLE
ConvertFrom-PodeJwt -Token "eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY"
#>
function ConvertFrom-PodeJwt {
    [CmdletBinding(DefaultParameterSetName = 'Secret')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Token,

        [Parameter(ParameterSetName = 'Signed')]
        $Secret = $null,

        [Parameter(ParameterSetName = 'Ignore')]
        [switch]
        $IgnoreSignature
    )

    # get the parts
    $parts = ($Token -isplit '\.')

    # check number of parts (should be 3)
    if ($parts.Length -ne 3) {
        throw 'Invalid JWT supplied'
    }

    # convert to header
    $header = ConvertFrom-PodeJwtBase64Value -Value $parts[0]
    if ([string]::IsNullOrWhiteSpace($header.alg)) {
        throw 'Invalid JWT header algorithm supplied'
    }

    # convert to payload
    $payload = ConvertFrom-PodeJwtBase64Value -Value $parts[1]

    # get signature
    if ($IgnoreSignature) {
        return $payload
    }

    $signature = $parts[2]

    # check "none" signature, and return payload if no signature
    $isNoneAlg = ($header.alg -ieq 'none')

    if ([string]::IsNullOrWhiteSpace($signature) -and !$isNoneAlg) {
        throw "No JWT signature supplied for $($header.alg)"
    }

    if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) {
        throw 'Expected no JWT signature to be supplied'
    }

    if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) {
        throw "Expected a signed JWT, 'none' algorithm is not allowed"
    }

    if ($isNoneAlg) {
        return $payload
    }

    # otherwise, we have an alg for the signature, so we need to validate it
    if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) {
        $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret)
    }

    $sig = "$($parts[0]).$($parts[1])"
    $sig = New-PodeJwtSignature -Algorithm $header.alg -Token $sig -SecretBytes $Secret

    if ($sig -ne $parts[2]) {
        throw 'Invalid JWT signature supplied'
    }

    # it's valid return the payload!
    return $payload
}

<#
.SYNOPSIS
Validates JSON Web Tokens (JWT) claims.

.DESCRIPTION
Validates JSON Web Tokens (JWT) claims. Checks time related claims: 'exp' and 'nbf'.

.PARAMETER Payload
Object containing JWT claims. Some of them are:
    - exp (expiration time)
    - nbf (not before)

.EXAMPLE
Test-PodeJwt @{exp = 2696258821 }

.EXAMPLE
Test-PodeJwt -Payload @{nbf = 1696258821 }
#>
function Test-PodeJwt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]
        $Payload
    )

    $now = [datetime]::UtcNow
    $unixStart = [datetime]::new(1970, 1, 1, 0, 0, [DateTimeKind]::Utc)

    # validate expiry
    if (![string]::IsNullOrWhiteSpace($Payload.exp)) {
        if ($now -gt $unixStart.AddSeconds($Payload.exp)) {
            throw 'The JWT has expired'
        }
    }

    # validate not-before
    if (![string]::IsNullOrWhiteSpace($Payload.nbf)) {
        if ($now -lt $unixStart.AddSeconds($Payload.nbf)) {
            throw 'The JWT is not yet valid for use'
        }
    }
}

<#
.SYNOPSIS
Automatically loads auth ps1 files

.DESCRIPTION
Automatically loads auth ps1 files from either a /auth folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeAuth

.EXAMPLE
Use-PodeAuth -Path './my-auth'
#>
function Use-PodeAuth {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'auth'
}

<#
.SYNOPSIS
Builds an OAuth2 scheme using an OpenID Connect Discovery URL.

.DESCRIPTION
Builds an OAuth2 scheme using an OpenID Connect Discovery URL.

.PARAMETER Url
The OpenID Connect Discovery URL, this must end with '/.well-known/openid-configuration' (if missing, it will be automatically appended).

.PARAMETER Scope
A list of optional Scopes to use during the OAuth2 request. (Default: the supported list returned)

.PARAMETER ClientId
The Client ID from registering a new app.

.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).

.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (Default: <host>/oauth2/callback)

.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.

.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.

.EXAMPLE
ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com/.well-known/openid-configuration' -ClientId some_id -UsePKCE

.EXAMPLE
ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com' -ClientId some_id -UsePKCE
#>
function ConvertFrom-PodeOIDCDiscovery {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter(Mandatory = $true)]
        [string]
        $ClientId,

        [Parameter()]
        [string]
        $ClientSecret,

        [Parameter()]
        [string]
        $RedirectUrl,

        [Parameter(ValueFromPipeline = $true)]
        [hashtable]
        $InnerScheme,

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $UsePKCE
    )

    # get the discovery doc
    if (!$Url.EndsWith('/.well-known/openid-configuration')) {
        $Url += '/.well-known/openid-configuration'
    }

    $config = Invoke-RestMethod -Method Get -Uri $Url

    # check it supports the code response_type
    if ($config.response_types_supported -inotcontains 'code') {
        throw "The OAuth2 provider does not support the 'code' response_type"
    }

    # can we have an InnerScheme?
    if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) {
        throw "The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme"
    }

    # scopes
    $scopes = $config.scopes_supported

    if (($null -ne $Scope) -and ($Scope.Length -gt 0)) {
        $scopes = @(foreach ($s in $Scope) {
                if ($s -iin $config.scopes_supported) {
                    $s
                }
            })
    }

    # pkce code challenge method
    $codeMethod = 'S256'
    if ($config.code_challenge_methods_supported -inotcontains $codeMethod) {
        $codeMethod = 'plain'
    }

    return New-PodeAuthScheme `
        -OAuth2 `
        -ClientId $ClientId `
        -ClientSecret $ClientSecret `
        -AuthoriseUrl $config.authorization_endpoint `
        -TokenUrl $config.token_endpoint `
        -UserUrl $config.userinfo_endpoint `
        -RedirectUrl $RedirectUrl `
        -Scope $scopes `
        -InnerScheme $InnerScheme `
        -Middleware $Middleware `
        -CodeChallengeMethod $codeMethod `
        -UsePKCE:$UsePKCE
}

<#
.SYNOPSIS
Test whether the current WebEvent or Session has an authenticated user.

.DESCRIPTION
Test whether the current WebEvent or Session has an authenticated user. Returns true if there is an authenticated user.

.PARAMETER IgnoreSession
If supplied, only the Auth object in the WebEvent will be checked and the Session will be skipped.

.EXAMPLE
if (Test-PodeAuthUser) { ... }
#>
function Test-PodeAuthUser {
    [CmdletBinding()]
    param(
        [switch]
        $IgnoreSession
    )

    # auth middleware
    if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) {
        $auth = $WebEvent.Auth
    }

    # session?
    elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) {
        $auth = $WebEvent.Session.Data.Auth
    }

    # null?
    if (($null -eq $auth) -or ($null -eq $auth.User)) {
        return $false
    }

    return ($null -ne $auth.User)
}

<#
.SYNOPSIS
Get the authenticated user from the WebEvent or Session.

.DESCRIPTION
Get the authenticated user from the WebEvent or Session. This is similar to calling $Webevent.Auth.User.

.PARAMETER IgnoreSession
If supplied, only the Auth object in the WebEvent will be used and the Session will be skipped.

.EXAMPLE
$user = Get-PodeAuthUser
#>
function Get-PodeAuthUser {
    [CmdletBinding()]
    param(
        [switch]
        $IgnoreSession
    )

    # auth middleware
    if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) {
        $auth = $WebEvent.Auth
    }

    # session?
    elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) {
        $auth = $WebEvent.Session.Data.Auth
    }

    # null?
    if (($null -eq $auth) -or ($null -eq $auth.User)) {
        return $null
    }

    return $auth.User
}
src\Public\AutoImport.ps1
<#
.SYNOPSIS
Exports modules that can be auto-imported by Pode, and into its runspaces.

.DESCRIPTION
Exports modules that can be auto-imported by Pode, and into its runspaces.

.PARAMETER Name
The Name(s) of modules to export.

.EXAMPLE
Export-PodeModule -Name Mod1, Mod2
#>
function Export-PodeModule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name
    )

    $PodeContext.Server.AutoImport.Modules.ExportList += @($Name)
}

<#
.SYNOPSIS
Exports snapins that can be auto-imported by Pode, and into its runspaces.

.DESCRIPTION
Exports snapins that can be auto-imported by Pode, and into its runspaces.

.PARAMETER Name
The Name(s) of snapins to export.

.EXAMPLE
Export-PodeSnapin -Name Mod1, Mod2
#>
function Export-PodeSnapin {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name
    )

    # if non-windows or core, fail
    if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
        throw 'Snapins are only supported on Windows PowerShell'
    }

    $PodeContext.Server.AutoImport.Snapins.ExportList += @($Name)
}

<#
.SYNOPSIS
Exports functions that can be auto-imported by Pode, and into its runspaces.

.DESCRIPTION
Exports functions that can be auto-imported by Pode, and into its runspaces.

.PARAMETER Name
The Name(s) of functions to export.

.EXAMPLE
Export-PodeFunction -Name Mod1, Mod2
#>
function Export-PodeFunction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name
    )

    $PodeContext.Server.AutoImport.Functions.ExportList += @($Name)
}

<#
.SYNOPSIS
Exports Secret Vaults that can be auto-imported by Pode, and into its runspaces.

.DESCRIPTION
Exports Secret Vaults that can be auto-imported by Pode, and into its runspaces.

.PARAMETER Name
The Name(s) of a Secret Vault to export.

.PARAMETER Type
The Type of the Secret Vault to import - only option currently is SecretManagement (default: SecretManagement)

.EXAMPLE
Export-PodeSecretVault -Name Vault1, Vault2
#>
function Export-PodeSecretVault {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name,

        [Parameter()]
        [ValidateSet('SecretManagement')]
        [string]
        $Type = 'SecretManagement'
    )

    $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList += @($Name)
}
src\Public\Caching.ps1
<#
.SYNOPSIS
Return the value of a key from the cache. You can use "$value = $cache:key" as well.

.DESCRIPTION
Return the value of a key from the cache, or returns the value plus metadata such as expiry time if required. You can use "$value = $cache:key" as well.

.PARAMETER Key
The Key to be retrieved.

.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)

.PARAMETER Metadata
If supplied, and if supported by the cache storage, an metadata such as expiry times will also be returned.

.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey'

.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'

.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey' -Metadata

.EXAMPLE
$value = $cache:ExampleKey
#>
function Get-PodeCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Storage = $null,

        [switch]
        $Metadata
    )

    # inmem or custom storage?
    if ([string]::IsNullOrEmpty($Storage)) {
        $Storage = $PodeContext.Server.Cache.DefaultStorage
    }

    # use inmem cache
    if ([string]::IsNullOrEmpty($Storage)) {
        return (Get-PodeCacheInternal -Key $Key -Metadata:$Metadata)
    }

    # used custom storage
    if (Test-PodeCacheStorage -Key $Storage) {
        return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Key, $Metadata.IsPresent) -Splat -Return)
    }

    # storage not found!
    throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Key)'"
}

<#
.SYNOPSIS
Set (create/update) a key in the cache. You can use "$cache:key = 'value'" as well.

.DESCRIPTION
Set (create/update) a key in the cache, with an optional TTL value. You can use "$cache:key = 'value'" as well.

.PARAMETER Key
The Key to be set.

.PARAMETER InputObject
The value of the key to be set, can be any object type.

.PARAMETER Ttl
An optional TTL value, in seconds. The default is whatever "Get-PodeCacheDefaultTtl" retuns, which will be 3600 seconds when not set.

.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)

.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue'

.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Storage 'ExampleStorage'

.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Ttl 300

.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject @{ Value = 'ExampleValue' }

.EXAMPLE
@{ Value = 'ExampleValue' } | Set-PodeCache -Key 'ExampleKey'

.EXAMPLE
$cache:ExampleKey = 'ExampleValue'
#>
function Set-PodeCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [int]
        $Ttl = 0,

        [Parameter()]
        [string]
        $Storage = $null
    )

    # use the global settable default here
    if ($Ttl -le 0) {
        $Ttl = $PodeContext.Server.Cache.DefaultTtl
    }

    # inmem or custom storage?
    if ([string]::IsNullOrEmpty($Storage)) {
        $Storage = $PodeContext.Server.Cache.DefaultStorage
    }

    # use inmem cache
    if ([string]::IsNullOrEmpty($Storage)) {
        Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl
    }

    # used custom storage
    elseif (Test-PodeCacheStorage -Key $Storage) {
        $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat
    }

    # storage not found!
    else {
        throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Key)'"
    }
}

<#
.SYNOPSIS
Test if a key exists in the cache.

.DESCRIPTION
Test if a key exists in the cache, and isn't expired.

.PARAMETER Key
The Key to test.

.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)

.EXAMPLE
Test-PodeCache -Key 'ExampleKey'

.EXAMPLE
Test-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'
#>
function Test-PodeCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Storage = $null
    )

    # inmem or custom storage?
    if ([string]::IsNullOrEmpty($Storage)) {
        $Storage = $PodeContext.Server.Cache.DefaultStorage
    }

    # use inmem cache
    if ([string]::IsNullOrEmpty($Storage)) {
        return (Test-PodeCacheInternal -Key $Key)
    }

    # used custom storage
    if (Test-PodeCacheStorage -Key $Storage) {
        return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Key) -Splat -Return)
    }

    # storage not found!
    throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Key)' exists"
}

<#
.SYNOPSIS
Remove a key from the cache.

.DESCRIPTION
Remove a key from the cache.

.PARAMETER Key
The Key to be removed.

.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)

.EXAMPLE
Remove-PodeCache -Key 'ExampleKey'

.EXAMPLE
Remove-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'
#>
function Remove-PodeCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Storage = $null
    )

    # inmem or custom storage?
    if ([string]::IsNullOrEmpty($Storage)) {
        $Storage = $PodeContext.Server.Cache.DefaultStorage
    }

    # use inmem cache
    if ([string]::IsNullOrEmpty($Storage)) {
        Remove-PodeCacheInternal -Key $Key
    }

    # used custom storage
    elseif (Test-PodeCacheStorage -Key $Storage) {
        $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat
    }

    # storage not found!
    else {
        throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Key)'"
    }
}

<#
.SYNOPSIS
Clear all keys from the cache.

.DESCRIPTION
Clear all keys from the cache.

.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)

.EXAMPLE
Clear-PodeCache

.EXAMPLE
Clear-PodeCache -Storage 'ExampleStorage'
#>
function Clear-PodeCache {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Storage = $null
    )

    # inmem or custom storage?
    if ([string]::IsNullOrEmpty($Storage)) {
        $Storage = $PodeContext.Server.Cache.DefaultStorage
    }

    # use inmem cache
    if ([string]::IsNullOrEmpty($Storage)) {
        Clear-PodeCacheInternal
    }

    # used custom storage
    elseif (Test-PodeCacheStorage -Key $Storage) {
        $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear
    }

    # storage not found!
    else {
        throw "Cache storage with name '$($Storage)' not found when attempting to clear cached"
    }
}

<#
.SYNOPSIS
Add a cache storage.

.DESCRIPTION
Add a cache storage.

.PARAMETER Name
The Name of the cache storage.

.PARAMETER Get
A Get ScriptBlock, to retrieve a key's value from the cache, or the value plus metadata if required. Supplied parameters: Key, Metadata.

.PARAMETER Set
A Set ScriptBlock, to set/create/update a key's value in the cache. Supplied parameters: Key, Value, TTL.

.PARAMETER Remove
A Remove ScriptBlock, to remove a key from the cache. Supplied parameters: Key.

.PARAMETER Test
A Test ScriptBlock, to test if a key exists in the cache. Supplied parameters: Key.

.PARAMETER Clear
A Clear ScriptBlock, to remove all keys from the cache. Use an empty ScriptBlock if not supported.

.PARAMETER Default
If supplied, this cache storage will be set as the default storage.

.EXAMPLE
Add-PodeCacheStorage -Name 'ExampleStorage' -Get {} -Set {} -Remove {} -Test {} -Clear {}
#>
function Add-PodeCacheStorage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Get,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Set,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Remove,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Test,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Clear,

        [switch]
        $Default
    )

    # test if storage already exists
    if (Test-PodeCacheStorage -Name $Name) {
        throw "Cache Storage with name '$($Name) already exists"
    }

    # add cache storage
    $PodeContext.Server.Cache.Storage[$Name] = @{
        Name    = $Name
        Get     = $Get
        Set     = $Set
        Remove  = $Remove
        Test    = $Test
        Clear   = $Clear
        Default = $Default.IsPresent
    }

    # is default storage?
    if ($Default) {
        $PodeContext.Server.Cache.DefaultStorage = $Name
    }
}

<#
.SYNOPSIS
Remove a cache storage.

.DESCRIPTION
Remove a cache storage.

.PARAMETER Name
The Name of the cache storage.

.EXAMPLE
Remove-PodeCacheStorage -Name 'ExampleStorage'
#>
function Remove-PodeCacheStorage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Server.Cache.Storage.Remove($Name)
}

<#
.SYNOPSIS
Returns a cache storage.

.DESCRIPTION
Returns a cache storage.

.PARAMETER Name
The Name of the cache storage.

.EXAMPLE
$storage = Get-PodeCacheStorage -Name 'ExampleStorage'
#>
function Get-PodeCacheStorage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Cache.Storage[$Name]
}

<#
.SYNOPSIS
Test if a cache storage has been added/exists.

.DESCRIPTION
Test if a cache storage has been added/exists.

.PARAMETER Name
The Name of the cache storage.

.EXAMPLE
if (Test-PodeCacheStorage -Name 'ExampleStorage') { }
#>
function Test-PodeCacheStorage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Cache.Storage.ContainsKey($Name)
}

<#
.SYNOPSIS
Set a default cache storage.

.DESCRIPTION
Set a default cache storage.

.PARAMETER Name
The Name of the default storage to use for caching.

.EXAMPLE
Set-PodeCacheDefaultStorage -Name 'ExampleStorage'
#>
function Set-PodeCacheDefaultStorage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $PodeContext.Server.Cache.DefaultStorage = $Name
}

<#
.SYNOPSIS
Returns the current default cache Storage name.

.DESCRIPTION
Returns the current default cache Storage name. Empty/null if one isn't set.

.EXAMPLE
$storageName = Get-PodeCacheDefaultStorage
#>
function Get-PodeCacheDefaultStorage {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.Cache.DefaultStorage
}

<#
.SYNOPSIS
Set a default cache TTL.

.DESCRIPTION
Set a default cache TTL.

.PARAMETER Value
A default TTL value, in seconds, to use when setting cache key expiries.

.EXAMPLE
Set-PodeCacheDefaultTtl -Value 3600
#>
function Set-PodeCacheDefaultTtl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [int]
        $Value
    )

    if ($Value -le 0) {
        return
    }

    $PodeContext.Server.Cache.DefaultTtl = $Value
}

<#
.SYNOPSIS
Returns the current default cache TTL value.

.DESCRIPTION
Returns the current default cache TTL value. 3600 seconds is the default TTL if not set.

.EXAMPLE
$ttl = Get-PodeCacheDefaultTtl
#>
function Get-PodeCacheDefaultTtl {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.Cache.DefaultTtl
}
src\Public\Cookies.ps1
<#
.SYNOPSIS
Sets a cookie on the Response.

.DESCRIPTION
Sets a cookie on the Response using the "Set-Cookie" header. You can also set cookies to expire, or being signed.

.PARAMETER Name
The name of the cookie.

.PARAMETER Value
The value of the cookie.

.PARAMETER Secret
If supplied, the secret with which to sign the cookie.

.PARAMETER Duration
The duration, in seconds, before the cookie is expired.

.PARAMETER ExpiryDate
An explicit expiry date for the cookie.

.PARAMETER HttpOnly
Only allow the cookie to be used in browsers.

.PARAMETER Discard
Inform browsers to remove the cookie.

.PARAMETER Secure
Only allow the cookie on secure (HTTPS) connections.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2 -Secret 'hunter2'

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2 -Duration 3600
#>
function Set-PodeCookie {
    [CmdletBinding(DefaultParameterSetName = 'Duration')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Secret,

        [Parameter(ParameterSetName = 'Duration')]
        [int]
        $Duration = 0,

        [Parameter(ParameterSetName = 'ExpiryDate')]
        [datetime]
        $ExpiryDate,

        [switch]
        $HttpOnly,

        [switch]
        $Discard,

        [switch]
        $Secure,

        [switch]
        $Strict
    )

    # sign the value if we have a secret
    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict)
    }

    # create a new cookie
    $cookie = [System.Net.Cookie]::new($Name, $Value)
    $cookie.Secure = $Secure
    $cookie.Discard = $Discard
    $cookie.HttpOnly = $HttpOnly
    $cookie.Path = '/'

    if ($null -ne $ExpiryDate) {
        if ($ExpiryDate.Kind -eq [System.DateTimeKind]::Local) {
            $ExpiryDate = $ExpiryDate.ToUniversalTime()
        }

        $cookie.Expires = $ExpiryDate
    }
    elseif ($Duration -gt 0) {
        $cookie.Expires = [datetime]::UtcNow.AddSeconds($Duration)
    }

    # sets the cookie on the the response
    $WebEvent.PendingCookies[$cookie.Name] = $cookie
    Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    return (ConvertTo-PodeCookie -Cookie $cookie)
}

<#
.SYNOPSIS
Retrieves a cookie from the Request.

.DESCRIPTION
Retrieves a cookie from the Request, with the option to supply a secret to unsign the cookie's value.

.PARAMETER Name
The name of the cookie to retrieve.

.PARAMETER Secret
The secret used to unsign the cookie's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.PARAMETER Raw
If supplied, the cookie returned will be the raw .NET Cookie object for manipulation.

.EXAMPLE
Get-PodeCookie -Name 'Views'

.EXAMPLE
Get-PodeCookie -Name 'Views' -Secret 'hunter2'
#>
function Get-PodeCookie {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict,

        [switch]
        $Raw
    )

    # 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 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
}

<#
.SYNOPSIS
Retrieves the value of a cookie from the Request.

.DESCRIPTION
Retrieves the value of a cookie from the Request, with the option to supply a secret to unsign the cookie's value.

.PARAMETER Name
The name of the cookie to retrieve.

.PARAMETER Secret
The secret used to unsign the cookie's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Get-PodeCookieValue -Name 'Views'

.EXAMPLE
Get-PodeCookieValue -Name 'Views' -Secret 'hunter2'
#>
function Get-PodeCookieValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    $cookie = Get-PodeCookie -Name $Name -Secret $Secret -Strict:$Strict
    if ($null -eq $cookie) {
        return $null
    }

    return $cookie.Value
}

<#
.SYNOPSIS
Tests if a cookie exists on the Request.

.DESCRIPTION
Tests if a cookie exists on the Request.

.PARAMETER Name
The name of the cookie to test for on the Request.

.EXAMPLE
Test-PodeCookie -Name 'Views'
#>
function Test-PodeCookie {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $cookie = $WebEvent.Cookies[$Name]
    return (($null -ne $cookie) -and ![string]::IsNullOrWhiteSpace($cookie.Value))
}

<#
.SYNOPSIS
Removes a cookie from the Response.

.DESCRIPTION
Removes a cookie from the Response, this is done by immediately expiring the cookie and flagging it for discard.

.PARAMETER Name
The name of the cookie to be removed.

.EXAMPLE
Remove-PodeCookie -Name 'Views'
#>
function Remove-PodeCookie {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # get the cookie from the response - if it's not found, get it from the request
    $cookie = $WebEvent.PendingCookies[$Name]
    if ($null -eq $cookie) {
        $cookie = Get-PodeCookie -Name $Name -Raw
    }

    # remove the cookie from the response, and reset it to expire
    if ($null -ne $cookie) {
        $cookie.Discard = $true
        $cookie.Expires = [DateTime]::UtcNow.AddDays(-2)
        $cookie.Path = '/'
        $WebEvent.PendingCookies[$cookie.Name] = $cookie
        Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    }
}

<#
.SYNOPSIS
Tests if a cookie on the Request is validly signed.

.DESCRIPTION
Tests if a cookie on the Request is validly signed, by attempting to unsign it using some secret.

.PARAMETER Name
The name of the cookie to test.

.PARAMETER Secret
A secret to use for attempting to unsign the cookie's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Test-PodeCookieSigned -Name 'Views' -Secret 'hunter2'
#>
function Test-PodeCookieSigned {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    $cookie = $WebEvent.Cookies[$Name]
    if (($null -eq $cookie) -or [string]::IsNullOrEmpty($cookie.Value)) {
        return $false
    }

    return Test-PodeValueSigned -Value $cookie.Value -Secret $Secret -Strict:$Strict
}

<#
.SYNOPSIS
Updates the exipry date of a cookie on the Response.

.DESCRIPTION
Updates the exipry date of a cookie on the Response. This can either be done by suppling a duration, or and explicit expiry date.

.PARAMETER Name
The name of the cookie to extend.

.PARAMETER Duration
The duration, in seconds, to extend the cookie's expiry.

.PARAMETER ExpiryDate
An explicit expiry date for the cookie.

.EXAMPLE
Update-PodeCookieExpiry -Name  'Views' -Duration 1800

.EXAMPLE
Update-PodeCookieExpiry -Name  'Views' -ExpiryDate ([datetime]::UtcNow.AddSeconds(1800))
#>
function Update-PodeCookieExpiry {
    [CmdletBinding(DefaultParameterSetName = 'Duration')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Duration')]
        [int]
        $Duration = 0,

        [Parameter(ParameterSetName = 'ExpiryDate')]
        [datetime]
        $ExpiryDate
    )

    # get the cookie from the response - if it's not found, get it from the request
    $cookie = $WebEvent.PendingCookies[$Name]
    if ($null -eq $cookie) {
        $cookie = Get-PodeCookie -Name $Name -Raw
    }

    # extends the expiry on the cookie
    if ($null -ne $ExpiryDate) {
        if ($ExpiryDate.Kind -eq [System.DateTimeKind]::Local) {
            $ExpiryDate = $ExpiryDate.ToUniversalTime()
        }

        $cookie.Expires = $ExpiryDate
    }
    elseif ($Duration -gt 0) {
        $cookie.Expires = [datetime]::UtcNow.AddSeconds($Duration)
    }

    $cookie.Path = '/'

    # sets the cookie on the the response
    $WebEvent.PendingCookies[$cookie.Name] = $cookie
    Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    return (ConvertTo-PodeCookie -Cookie $cookie)
}

<#
.SYNOPSIS
Stores secrets that can be used to sign cookies.

.DESCRIPTION
Stores secrets that can be used to sign cookies. A global secret can be set for easier retrieval.

.PARAMETER Name
The name of the secret to store.

.PARAMETER Value
The value of the secret to store.

.PARAMETER Global
If flagged, the secret being stored will be set as the global secret.

.EXAMPLE
Set-PodeCookieSecret -Name 'my-secret' -Value 'shhhh!'

.EXAMPLE
Set-PodeCookieSecret -Value 'hunter2' -Global
#>
function Set-PodeCookieSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'General')]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter(ParameterSetName = 'Global')]
        [switch]
        $Global
    )

    if ($Global) {
        $Name = 'global'
    }

    $PodeContext.Server.Cookies.Secrets[$Name] = $Value
}

<#
.SYNOPSIS
Retrieves a stored secret value.

.DESCRIPTION
Retrieves a stored secret value.

.PARAMETER Name
The name of the secret to retrieve.

.PARAMETER Global
If flagged, will return the current global secret value.

.EXAMPLE
Get-PodeCookieSecret -Name 'my-secret'

.EXAMPLE
Get-PodeCookieSecret -Global
#>
function Get-PodeCookieSecret {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'General')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Global')]
        [switch]
        $Global
    )

    if ($Global) {
        return ($PodeContext.Server.Cookies.Secrets['global'])
    }

    return ($PodeContext.Server.Cookies.Secrets[$Name])
}
src\Public\Core.ps1
<#
.SYNOPSIS
Starts a Pode Server with the supplied ScriptBlock.

.DESCRIPTION
Starts a Pode Server with the supplied ScriptBlock.

.PARAMETER ScriptBlock
The main logic for the Server.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Server's logic.
The directory of this file will be used as the Server's root path - unless a specific -RootPath is supplied.

.PARAMETER Interval
For 'Service' type Servers, will invoke the ScriptBlock every X seconds.

.PARAMETER Name
An optional name for the Server (intended for future ideas).

.PARAMETER Threads
The numbers of threads to use for Web, SMTP, and TCP servers.

.PARAMETER RootPath
An override for the Server's root path.

.PARAMETER Request
Intended for Serverless environments, this is Requests details that Pode can parse and use.

.PARAMETER ServerlessType
Optional, this is the serverless type, to define how Pode should run and deal with incoming Requests.

.PARAMETER StatusPageExceptions
An optional value of Show/Hide to control where Stacktraces are shown in the Status Pages.
If supplied this value will override the ShowExceptions setting in the server.psd1 file.

.PARAMETER ListenerType
An optional value to use a custom Socket Listener. The default is Pode's inbuilt listener.
There's the Pode.Kestrel module, so the value here should be "Kestrel" if using that.

.PARAMETER DisableTermination
Disables the ability to terminate the Server.

.PARAMETER Quiet
Disables any output from the Server.

.PARAMETER Browse
Open the web Server's default endpoint in your default browser.

.PARAMETER CurrentPath
Sets the Server's root path to be the current working path - for -FilePath only.

.PARAMETER EnablePool
Tells Pode to configure certain RunspacePools when they're being used adhoc, such as Timers or Schedules.

.PARAMETER EnableBreakpoints
If supplied, any breakpoints created by using Wait-PodeDebugger will be enabled - or disabled if false passed explicitly, or not supplied.

.EXAMPLE
Start-PodeServer { /* logic */ }

.EXAMPLE
Start-PodeServer -Interval 10 { /* logic */ }

.EXAMPLE
Start-PodeServer -Request $LambdaInput -ServerlessType AwsLambda { /* logic */ }
#>
function Start-PodeServer {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [int]
        $Interval = 0,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Threads = 1,

        [Parameter()]
        [string]
        $RootPath,

        [Parameter()]
        $Request,

        [Parameter()]
        [ValidateSet('', 'AzureFunctions', 'AwsLambda')]
        [string]
        $ServerlessType = [string]::Empty,

        [Parameter()]
        [ValidateSet('', 'Hide', 'Show')]
        [string]
        $StatusPageExceptions = [string]::Empty,

        [Parameter()]
        [string]
        $ListenerType = [string]::Empty,

        [Parameter()]
        [ValidateSet('Timers', 'Schedules', 'Tasks', 'WebSockets', 'Files')]
        [string[]]
        $EnablePool,

        [switch]
        $DisableTermination,

        [switch]
        $Quiet,

        [switch]
        $Browse,

        [Parameter(ParameterSetName = 'File')]
        [switch]
        $CurrentPath,

        [switch]
        $EnableBreakpoints
    )

    # ensure the session is clean
    $PodeContext = $null
    $ShowDoneMessage = $true

    try {
        # if we have a filepath, resolve it - and extract a root path from it
        if ($PSCmdlet.ParameterSetName -ieq 'file') {
            $FilePath = Get-PodeRelativePath -Path $FilePath -Resolve -TestPath -JoinRoot -RootPath $MyInvocation.PSScriptRoot

            # if not already supplied, set root path
            if ([string]::IsNullOrWhiteSpace($RootPath)) {
                if ($CurrentPath) {
                    $RootPath = $PWD.Path
                }
                else {
                    $RootPath = Split-Path -Parent -Path $FilePath
                }
            }
        }

        # configure the server's root path
        if (!(Test-PodeIsEmpty $RootPath)) {
            $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath
        }

        # create main context object
        $PodeContext = New-PodeContext `
            -ScriptBlock $ScriptBlock `
            -FilePath $FilePath `
            -Threads $Threads `
            -Interval $Interval `
            -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) `
            -ServerlessType $ServerlessType `
            -ListenerType $ListenerType `
            -EnablePool $EnablePool `
            -StatusPageExceptions $StatusPageExceptions `
            -DisableTermination:$DisableTermination `
            -Quiet:$Quiet `
            -EnableBreakpoints:$EnableBreakpoints

        # set it so ctrl-c can terminate, unless serverless/iis, or disabled
        if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) {
            [Console]::TreatControlCAsInput = $true
        }

        # start the file monitor for interally restarting
        Start-PodeFileMonitor

        # start the server
        Start-PodeInternalServer -Request $Request -Browse:$Browse

        # at this point, if it's just a one-one off script, return
        if (!(Test-PodeServerKeepOpen)) {
            return
        }

        # sit here waiting for termination/cancellation, or to restart the server
        while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) {
            Start-Sleep -Seconds 1

            # get the next key presses
            $key = Get-PodeConsoleKey

            # check for internal restart
            if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) {
                Restart-PodeInternalServer
            }

            # check for open browser
            if (Test-PodeOpenBrowserPressed -Key $key) {
                Invoke-PodeEvent -Type Browser
                Start-Process (Get-PodeEndpointUrl)
            }
        }

        if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) {
            Write-PodeHost '(IIS Shutdown) ' -NoNewline -ForegroundColor Yellow
        }

        Write-PodeHost 'Terminating...' -NoNewline -ForegroundColor Yellow
        Invoke-PodeEvent -Type Terminate
        $PodeContext.Tokens.Cancellation.Cancel()
    }
    catch {
        Invoke-PodeEvent -Type Crash
        $ShowDoneMessage = $false
        throw
    }
    finally {
        Invoke-PodeEvent -Type Stop

        # set output values
        Set-PodeOutputVariables

        # unregister secret vaults
        Unregister-PodeSecretVaults

        # clean the runspaces and tokens
        Close-PodeServerInternal -ShowDoneMessage:$ShowDoneMessage

        # clean the session
        $PodeContext = $null
    }
}

<#
.SYNOPSIS
Closes the Pode server.

.DESCRIPTION
Closes the Pode server.

.EXAMPLE
Close-PodeServer
#>
function Close-PodeServer {
    [CmdletBinding()]
    param()

    $PodeContext.Tokens.Cancellation.Cancel()
}

<#
.SYNOPSIS
Restarts the Pode server.

.DESCRIPTION
Restarts the Pode server.

.EXAMPLE
Restart-PodeServer
#>
function Restart-PodeServer {
    [CmdletBinding()]
    param()

    $PodeContext.Tokens.Restart.Cancel()
}

<#
.SYNOPSIS
Helper wrapper function to start a Pode web server for a static website at the current directory.

.DESCRIPTION
Helper wrapper function to start a Pode web server for a static website at the current directory.

.PARAMETER Threads
The numbers of threads to use for requests.

.PARAMETER RootPath
An override for the Server's root path.

.PARAMETER Address
The IP/Hostname of the endpoint.

.PARAMETER Port
The Port number of the endpoint.

.PARAMETER Https
Start the server using HTTPS, if no certificate details are supplied a self-signed certificate will be generated.

.PARAMETER Certificate
The path to a certificate that can be use to enable HTTPS.

.PARAMETER CertificatePassword
The password for the certificate referenced in CertificateFile.

.PARAMETER CertificateKey
A key file to be paired with a PEM certificate referenced in CertificateFile

.PARAMETER X509Certificate
The raw X509 certificate that can be use to enable HTTPS.

.PARAMETER Path
The URI path for the static Route.

.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.

.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.

.PARAMETER FileBrowser
When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory.

.PARAMETER Browse
Open the web server's default endpoint in your default browser.

.EXAMPLE
Start-PodeStaticServer

.EXAMPLE
Start-PodeStaticServer -Address '127.0.0.3' -Port 8000

.EXAMPLE
Start-PodeStaticServer -Path '/installers' -DownloadOnly
#>
function Start-PodeStaticServer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [int]
        $Threads = 3,

        [Parameter()]
        [string]
        $RootPath = $PWD,

        [Parameter()]
        [string]
        $Address = 'localhost',

        [Parameter()]
        [int]
        $Port = 0,

        [Parameter()]
        [switch]
        $Https,

        [Parameter()]
        [string]
        $Certificate = $null,

        [Parameter()]
        [string]
        $CertificatePassword = $null,

        [Parameter()]
        [string]
        $CertificateKey = $null,

        [Parameter()]
        [X509Certificate]
        $X509Certificate = $null,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [string[]]
        $Defaults,

        [switch]
        $DownloadOnly,

        [switch]
        $FileBrowser,

        [switch]
        $Browse
    )

    Start-PodeServer -RootPath $RootPath -Threads $Threads -Browse:$Browse -ScriptBlock {
        # add either an http or https endpoint
        if ($Https) {
            if ($null -ne $X509Certificate) {
                Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -X509Certificate $X509Certificate
            }
            elseif (![string]::IsNullOrWhiteSpace($Certificate)) {
                Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -Certificate $Certificate -CertificatePassword $CertificatePassword -CertificateKey $CertificateKey
            }
            else {
                Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -SelfSigned
            }
        }
        else {
            Add-PodeEndpoint -Address $Address -Port $Port -Protocol Http
        }

        # add the static route
        Add-PodeStaticRoute -Path $Path -Source (Get-PodeServerPath) -Defaults $Defaults -DownloadOnly:$DownloadOnly -FileBrowser:$FileBrowser
    }
}

<#
.SYNOPSIS
A default server secret that can be for signing values like Session, Cookies, or SSE IDs.

.DESCRIPTION
A default server secret that can be for signing values like Session, Cookies, or SSE IDs. This secret is regenerated
on every server start and restart.

.EXAMPLE
$secret = Get-PodeServerDefaultSecret
#>
function Get-PodeServerDefaultSecret {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.DefaultSecret
}

<#
.SYNOPSIS
The CLI for Pode, to initialise, build and start your Server.

.DESCRIPTION
The CLI for Pode, to initialise, build and start your Server.

.PARAMETER Action
The action to invoke on your Server.

.PARAMETER Dev
Supply when running "pode install", this will install any dev packages defined in your package.json.

.EXAMPLE
pode install -dev

.EXAMPLE
pode build

.EXAMPLE
pode start
#>
function Pode {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('init', 'test', 'start', 'install', 'build')]
        [Alias('a')]
        [string]
        $Action,

        [switch]
        [Alias('d')]
        $Dev
    )

    # default config file name and content
    $file = './package.json'
    $name = Split-Path -Leaf -Path $pwd
    $data = $null

    # default config data that's used to populate on init
    $map = @{
        'name'        = $name
        'version'     = '1.0.0'
        'description' = ''
        'main'        = './server.ps1'
        'scripts'     = @{
            'start'   = './server.ps1'
            'install' = 'yarn install --force --ignore-scripts --modules-folder pode_modules'
            'build'   = 'psake'
            'test'    = 'invoke-pester ./tests/*.ps1'
        }
        'author'      = ''
        'license'     = 'MIT'
    }

    # check and load config if already exists
    if (Test-Path $file) {
        $data = (Get-Content $file | ConvertFrom-Json)
    }

    # quick check to see if the data is required
    if ($Action -ine 'init') {
        if ($null -eq $data) {
            Write-Host 'package.json file not found' -ForegroundColor Red
            return
        }
        else {
            $actionScript = $data.scripts.$Action

            if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ieq 'start') {
                $actionScript = $data.main
            }

            if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ine 'install') {
                Write-Host "package.json does not contain a script for the $($Action) action" -ForegroundColor Yellow
                return
            }
        }
    }
    else {
        if ($null -ne $data) {
            Write-Host 'package.json already exists' -ForegroundColor Yellow
            return
        }
    }

    switch ($Action.ToLowerInvariant()) {
        'init' {
            $v = Read-Host -Prompt "name ($($map.name))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.name = $v }

            $v = Read-Host -Prompt "version ($($map.version))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.version = $v }

            $map.description = Read-Host -Prompt 'description'

            $v = Read-Host -Prompt "entry point ($($map.main))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.main = $v; $map.scripts.start = $v }

            $map.author = Read-Host -Prompt 'author'

            $v = Read-Host -Prompt "license ($($map.license))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.license = $v }

            $map | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 -Force
            Write-Host 'Success, saved package.json' -ForegroundColor Green
        }

        'test' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'start' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'install' {
            if ($Dev) {
                Install-PodeLocalModules -Modules $data.devModules
            }

            Install-PodeLocalModules -Modules $data.modules
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'build' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }
    }
}

<#
.SYNOPSIS
Opens a Web Server up as a Desktop Application.

.DESCRIPTION
Opens a Web Server up as a Desktop Application.

.PARAMETER Title
The title of the Application's window.

.PARAMETER Icon
A path to an icon image for the Application.

.PARAMETER WindowState
The state the Application's window starts, such as Minimized.

.PARAMETER WindowStyle
The border style of the Application's window.

.PARAMETER ResizeMode
Specifies if the Application's window is resizable.

.PARAMETER Height
The height of the window.

.PARAMETER Width
The width of the window.

.PARAMETER EndpointName
The specific endpoint name to use, if you are listening on multiple endpoints.

.PARAMETER HideFromTaskbar
Stops the Application from appearing on the taskbar.

.EXAMPLE
Show-PodeGui -Title 'MyApplication' -WindowState 'Maximized'
#>
function Show-PodeGui {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Title,

        [Parameter()]
        [string]
        $Icon,

        [Parameter()]
        [ValidateSet('Normal', 'Maximized', 'Minimized')]
        [string]
        $WindowState = 'Normal',

        [Parameter()]
        [ValidateSet('None', 'SingleBorderWindow', 'ThreeDBorderWindow', 'ToolWindow')]
        [string]
        $WindowStyle = 'SingleBorderWindow',

        [Parameter()]
        [ValidateSet('CanResize', 'CanMinimize', 'NoResize')]
        [string]
        $ResizeMode = 'CanResize',

        [Parameter()]
        [int]
        $Height = 0,

        [Parameter()]
        [int]
        $Width = 0,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $HideFromTaskbar
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Show-PodeGui' -ThrowError

    # only valid for Windows PowerShell
    if ((Test-PodeIsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6)) {
        throw 'Show-PodeGui is currently only available for Windows PowerShell, and PowerShell 7+ on Windows'
    }

    # enable the gui and set general settings
    $PodeContext.Server.Gui.Enabled = $true
    $PodeContext.Server.Gui.Title = $Title
    $PodeContext.Server.Gui.ShowInTaskbar = !$HideFromTaskbar
    $PodeContext.Server.Gui.WindowState = $WindowState
    $PodeContext.Server.Gui.WindowStyle = $WindowStyle
    $PodeContext.Server.Gui.ResizeMode = $ResizeMode

    # set the window's icon path
    if (![string]::IsNullOrWhiteSpace($Icon)) {
        $PodeContext.Server.Gui.Icon = Get-PodeRelativePath -Path $Icon -JoinRoot -Resolve
        if (!(Test-Path $PodeContext.Server.Gui.Icon)) {
            throw "Path to icon for GUI does not exist: $($PodeContext.Server.Gui.Icon)"
        }
    }

    # set the height of the window
    $PodeContext.Server.Gui.Height = $Height
    if ($PodeContext.Server.Gui.Height -le 0) {
        $PodeContext.Server.Gui.Height = 'auto'
    }

    # set the width of the window
    $PodeContext.Server.Gui.Width = $Width
    if ($PodeContext.Server.Gui.Width -le 0) {
        $PodeContext.Server.Gui.Width = 'auto'
    }

    # set the gui to use a specific listener
    $PodeContext.Server.Gui.EndpointName = $EndpointName

    if (![string]::IsNullOrWhiteSpace($EndpointName)) {
        if (!$PodeContext.Server.Endpoints.ContainsKey($EndpointName)) {
            throw "Endpoint with name '$($EndpointName)' does not exist"
        }

        $PodeContext.Server.Gui.Endpoint = $PodeContext.Server.Endpoints[$EndpointName]
    }
}

<#
.SYNOPSIS
Bind an endpoint to listen for incoming Requests.

.DESCRIPTION
Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates.

.PARAMETER Address
The IP/Hostname of the endpoint (Default: localhost).

.PARAMETER Port
The Port number of the endpoint.

.PARAMETER Hostname
An optional hostname for the endpoint, specifying a hostname restricts access to just the hostname.

.PARAMETER Protocol
The protocol of the supplied endpoint.

.PARAMETER Certificate
The path to a certificate that can be use to enable HTTPS

.PARAMETER CertificatePassword
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

.PARAMETER CertificateThumbprint
A certificate thumbprint to bind onto HTTPS endpoints (Windows).

.PARAMETER CertificateName
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).

.PARAMETER CertificateStoreLocation
The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows).

.PARAMETER X509Certificate
The raw X509 certificate that can be use to enable HTTPS

.PARAMETER TlsMode
The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit).

.PARAMETER Name
An optional name for the endpoint, that can be used with other functions (Default: GUID).

.PARAMETER RedirectTo
The Name of another Endpoint to automatically generate a redirect route for all traffic.

.PARAMETER Description
A quick description of the Endpoint - normally used in OpenAPI.

.PARAMETER Acknowledge
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).

.PARAMETER CRLFMessageEnd
If supplied, TCP endpoints will expect incoming data to end with CRLF.

.PARAMETER Force
Ignore Adminstrator checks for non-localhost endpoints.

.PARAMETER SelfSigned
Create and bind a self-signed certifcate for HTTPS endpoints.

.PARAMETER AllowClientCertificate
Allow for client certificates to be sent on requests.

.PARAMETER PassThru
If supplied, the endpoint created will be returned.

.PARAMETER LookupHostname
If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS.

.PARAMETER DualMode
If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address.
For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address.

.PARAMETER Default
If supplied, this endpoint will be the default one used for internally generating URLs.

.EXAMPLE
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

.EXAMPLE
Add-PodeEndpoint -Address localhost -Protocol Smtp

.EXAMPLE
Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned

.EXAMPLE
Add-PodeEndpoint -Address 127.0.0.2 -Hostname dev.pode.com -Port 8443 -Protocol Https -SelfSigned

.EXAMPLE
Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292'
#>
function Add-PodeEndpoint {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string]
        $Address = 'localhost',

        [Parameter()]
        [int]
        $Port = 0,

        [Parameter()]
        [string]
        $Hostname,

        [Parameter()]
        [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
        [string]
        $Protocol,

        [Parameter(Mandatory = $true, ParameterSetName = 'CertFile')]
        [string]
        $Certificate = $null,

        [Parameter(ParameterSetName = 'CertFile')]
        [string]
        $CertificatePassword = $null,

        [Parameter(ParameterSetName = 'CertFile')]
        [string]
        $CertificateKey = $null,

        [Parameter(Mandatory = $true, ParameterSetName = 'CertThumb')]
        [string]
        $CertificateThumbprint,

        [Parameter(Mandatory = $true, ParameterSetName = 'CertName')]
        [string]
        $CertificateName,

        [Parameter(ParameterSetName = 'CertName')]
        [Parameter(ParameterSetName = 'CertThumb')]
        [System.Security.Cryptography.X509Certificates.StoreName]
        $CertificateStoreName = 'My',

        [Parameter(ParameterSetName = 'CertName')]
        [Parameter(ParameterSetName = 'CertThumb')]
        [System.Security.Cryptography.X509Certificates.StoreLocation]
        $CertificateStoreLocation = 'CurrentUser',

        [Parameter(Mandatory = $true, ParameterSetName = 'CertRaw')]
        [X509Certificate]
        $X509Certificate = $null,

        [Parameter(ParameterSetName = 'CertFile')]
        [Parameter(ParameterSetName = 'CertThumb')]
        [Parameter(ParameterSetName = 'CertName')]
        [Parameter(ParameterSetName = 'CertRaw')]
        [Parameter(ParameterSetName = 'CertSelf')]
        [ValidateSet('Implicit', 'Explicit')]
        [string]
        $TlsMode = 'Implicit',

        [Parameter()]
        [string]
        $Name = $null,

        [Parameter()]
        [string]
        $RedirectTo = $null,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $Acknowledge,

        [Parameter()]
        [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')]
        [string[]]
        $SslProtocol = $null,

        [switch]
        $CRLFMessageEnd,

        [switch]
        $Force,

        [Parameter(ParameterSetName = 'CertSelf')]
        [switch]
        $SelfSigned,

        [switch]
        $AllowClientCertificate,

        [switch]
        $PassThru,

        [switch]
        $LookupHostname,

        [switch]
        $DualMode,

        [switch]
        $Default
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError

    # if RedirectTo is supplied, then a Name is mandatory
    if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) {
        throw 'A Name is required for the endpoint if the RedirectTo parameter is supplied'
    }

    # get the type of endpoint
    $type = Get-PodeEndpointType -Protocol $Protocol

    # are we running as IIS for HTTP/HTTPS? (if yes, force the port, address and protocol)
    $isIIS = ((Test-PodeIsIIS) -and (@('Http', 'Ws') -icontains $type))
    if ($isIIS) {
        $Port = [int]$env:ASPNETCORE_PORT
        $Address = '127.0.0.1'
        $Hostname = [string]::Empty
        $Protocol = $type
    }

    # are we running as Heroku for HTTP/HTTPS? (if yes, force the port, address and protocol)
    $isHeroku = ((Test-PodeIsHeroku) -and (@('Http') -icontains $type))
    if ($isHeroku) {
        $Port = [int]$env:PORT
        $Address = '0.0.0.0'
        $Hostname = [string]::Empty
        $Protocol = $type
    }

    # parse the endpoint for host/port info
    if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) {
        throw "Invalid hostname supplied: $($Hostname)"
    }

    if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) {
        $Hostname = $Address
        $Address = 'localhost'
    }

    if (![string]::IsNullOrWhiteSpace($Hostname) -and $LookupHostname) {
        $Address = (Get-PodeIPAddressesForHostname -Hostname $Hostname -Type All | Select-Object -First 1)
    }

    $_endpoint = Get-PodeEndpointInfo -Address "$($Address):$($Port)"

    # if no name, set to guid, then check uniqueness
    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = New-PodeGuid -Secure
    }

    if ($PodeContext.Server.Endpoints.ContainsKey($Name)) {
        throw "An endpoint with the name '$($Name)' has already been defined"
    }

    # protocol must be https for client certs, or hosted behind a proxy like iis
    if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) {
        throw 'Client certificates are only supported on HTTPS endpoints'
    }

    # explicit tls is only supported for smtp/tcp
    if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) {
        throw 'The Explicit TLS mode is only supported on SMTPS and TCPS endpoints'
    }

    # ack message is only for smtp/tcp
    if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) {
        throw 'The Acknowledge message is only supported on SMTP and TCP endpoints'
    }

    # crlf message end is only for tcp
    if (($type -ine 'tcp') -and $CRLFMessageEnd) {
        throw 'The CRLF message end check is only supported on TCP endpoints'
    }

    # new endpoint object
    $obj = @{
        Name         = $Name
        Description  = $Description
        DualMode     = $DualMode
        Address      = $null
        RawAddress   = $null
        Port         = $null
        IsIPAddress  = $true
        HostName     = $Hostname
        FriendlyName = $Hostname
        Url          = $null
        Ssl          = @{
            Enabled   = (@('https', 'wss', 'smtps', 'tcps') -icontains $Protocol)
            Protocols = $PodeContext.Server.Sockets.Ssl.Protocols
        }
        Protocol     = $Protocol.ToLowerInvariant()
        Type         = $type.ToLowerInvariant()
        Runspace     = @{
            PoolName = (Get-PodeEndpointRunspacePoolName -Protocol $Protocol)
        }
        Default      = $Default.IsPresent
        Certificate  = @{
            Raw                    = $X509Certificate
            SelfSigned             = $SelfSigned
            AllowClientCertificate = $AllowClientCertificate
            TlsMode                = $TlsMode
        }
        Tcp          = @{
            Acknowledge    = $Acknowledge
            CRLFMessageEnd = $CRLFMessageEnd
        }
    }

    # set ssl protocols
    if (!(Test-PodeIsEmpty $SslProtocol)) {
        $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $SslProtocol)
    }

    # set the ip for the context (force to localhost for IIS)
    $obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode
    $obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName)

    if ($obj.IsIPAddress) {
        if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) {
            $obj.FriendlyName = "$($obj.Address)"
        }
        else {
            $obj.FriendlyName = 'localhost'
        }
    }

    # set the port for the context, if 0 use a default port for protocol
    $obj.Port = $_endpoint.Port
    if (([int]$obj.Port) -eq 0) {
        $obj.Port = Get-PodeDefaultPort -Protocol $Protocol -TlsMode $TlsMode
    }

    if ($obj.IsIPAddress) {
        $obj.RawAddress = "$($obj.Address):$($obj.Port)"
    }
    else {
        $obj.RawAddress = "$($obj.FriendlyName):$($obj.Port)"
    }

    # set the url of this endpoint
    $obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)/"

    # if the address is non-local, then check admin privileges
    if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeIsAdminUser)) {
        throw 'Must be running with administrator priviledges to listen on non-localhost addresses'
    }

    # has this endpoint been added before? (for http/https we can just not add it again)
    $exists = ($PodeContext.Server.Endpoints.Values | Where-Object {
        ($_.FriendlyName -ieq $obj.FriendlyName) -and ($_.Port -eq $obj.Port) -and ($_.Ssl.Enabled -eq $obj.Ssl.Enabled) -and ($_.Type -ieq $obj.Type)
        } | Measure-Object).Count

    # if we're dealing with a certificate, attempt to import it
    if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) {
        # fail if protocol is not https
        if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) {
            throw 'Certificate supplied for non-HTTPS/WSS endpoint'
        }

        switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
            'certfile' {
                $obj.Certificate.Raw = Get-PodeCertificateByFile -Certificate $Certificate -Password $CertificatePassword -Key $CertificateKey
            }

            'certthumb' {
                $obj.Certificate.Raw = Get-PodeCertificateByThumbprint -Thumbprint $CertificateThumbprint -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation
            }

            'certname' {
                $obj.Certificate.Raw = Get-PodeCertificateByName -Name $CertificateName -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation
            }

            'certself' {
                $obj.Certificate.Raw = New-PodeSelfSignedCertificate
            }
        }

        # fail if the cert is expired
        if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) {
            throw "The certificate '$($obj.Certificate.Raw.Subject)' has expired: $($obj.Certificate.Raw.NotAfter)"
        }
    }

    if (!$exists) {
        # set server type
        $_type = $type
        if ($_type -iin @('http', 'ws')) {
            $_type = 'http'
        }

        if ($PodeContext.Server.Types -inotcontains $_type) {
            $PodeContext.Server.Types += $_type
        }

        # add the new endpoint
        $PodeContext.Server.Endpoints[$Name] = $obj
        $PodeContext.Server.EndpointsMap["$($obj.Protocol)|$($obj.RawAddress)"] = $Name
    }

    # if RedirectTo is set, attempt to build a redirecting route
    if (!(Test-PodeIsHosted) -and ![string]::IsNullOrWhiteSpace($RedirectTo)) {
        $redir_endpoint = $PodeContext.Server.Endpoints[$RedirectTo]

        # ensure the name exists
        if (Test-PodeIsEmpty $redir_endpoint) {
            throw "An endpoint with the name '$($RedirectTo)' has not been defined for redirecting"
        }

        # build the redirect route
        Add-PodeRoute -Method * -Path * -EndpointName $obj.Name -ArgumentList $redir_endpoint -ScriptBlock {
            param($endpoint)
            Move-PodeResponseUrl -EndpointName $endpoint.Name
        }
    }

    # return the endpoint?
    if ($PassThru) {
        return $obj
    }
}

<#
.SYNOPSIS
Get an Endpoint(s).

.DESCRIPTION
Get an Endpoint(s).

.PARAMETER Address
An Address to filter the endpoints.

.PARAMETER Port
A Port to filter the endpoints.

.PARAMETER Hostname
A Hostname to filter the endpoints.

.PARAMETER Protocol
A Protocol to filter the endpoints.

.PARAMETER Name
Any endpoints Names to filter endpoints.

.EXAMPLE
Get-PodeEndpoint -Address 127.0.0.1

.EXAMPLE
Get-PodeEndpoint -Protocol Http

.EXAMPLE
Get-PodeEndpoint -Name Admin, User
#>
function Get-PodeEndpoint {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Address,

        [Parameter()]
        [int]
        $Port = 0,

        [Parameter()]
        [string]
        $Hostname,

        [Parameter()]
        [ValidateSet('', 'Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
        [string]
        $Protocol,

        [Parameter()]
        [string[]]
        $Name
    )

    if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) {
        $Hostname = $Address
        $Address = 'localhost'
    }

    $endpoints = $PodeContext.Server.Endpoints.Values

    # if we have an address, filter
    if (![string]::IsNullOrWhiteSpace($Address)) {
        if (($Address -eq '*') -or $PodeContext.Server.IsHeroku) {
            $Address = '0.0.0.0'
        }

        if ($PodeContext.Server.IsIIS -or ($Address -ieq 'localhost')) {
            $Address = '127.0.0.1'
        }

        $endpoints = @(foreach ($endpoint in $endpoints) {
                if ($endpoint.Address.ToString() -ine $Address) {
                    continue
                }

                $endpoint
            })
    }

    # if we have a hostname, filter
    if (![string]::IsNullOrWhiteSpace($Hostname)) {
        $endpoints = @(foreach ($endpoint in $endpoints) {
                if ($endpoint.Hostname.ToString() -ine $Hostname) {
                    continue
                }

                $endpoint
            })
    }

    # if we have a port, filter
    if ($Port -gt 0) {
        if ($PodeContext.Server.IsIIS) {
            $Port = [int]$env:ASPNETCORE_PORT
        }

        if ($PodeContext.Server.IsHeroku) {
            $Port = [int]$env:PORT
        }

        $endpoints = @(foreach ($endpoint in $endpoints) {
                if ($endpoint.Port -ne $Port) {
                    continue
                }

                $endpoint
            })
    }

    # if we have a protocol, filter
    if (![string]::IsNullOrWhiteSpace($Protocol)) {
        if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) {
            $Protocol = 'Http'
        }

        $endpoints = @(foreach ($endpoint in $endpoints) {
                if ($endpoint.Protocol -ine $Protocol) {
                    continue
                }

                $endpoint
            })
    }

    # further filter by endpoint names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $endpoints = @(foreach ($_name in $Name) {
                foreach ($endpoint in $endpoints) {
                    if ($endpoint.Name -ine $_name) {
                        continue
                    }

                    $endpoint
                }
            })
    }

    # return
    return $endpoints
}

<#
.SYNOPSIS
Sets the path for a specified default folder type in the Pode server context.

.DESCRIPTION
This function configures the path for one of the Pode server's default folder types: Views, Public, or Errors.
It updates the server's configuration to reflect the new path for the specified folder type.
The function first checks if the provided path exists and is a directory;
if so, it updates the `Server.DefaultFolders` dictionary with the new path.
If the path does not exist or is not a directory, the function throws an error.

The purpose of this function is to allow dynamic configuration of the server's folder paths, which can be useful during server setup or when altering the server's directory structure at runtime.

.PARAMETER Type
The type of the default folder to set the path for. Must be one of 'Views', 'Public', or 'Errors'.
This parameter determines which default folder's path is being set.

.PARAMETER Path
The new file system path for the specified default folder type. This path must exist and be a directory; otherwise, an exception is thrown.

.EXAMPLE
Set-PodeDefaultFolder -Type 'Views' -Path 'C:\Pode\Views'

This example sets the path for the server's default 'Views' folder to 'C:\Pode\Views', assuming this path exists and is a directory.

.EXAMPLE
Set-PodeDefaultFolder -Type 'Public' -Path 'C:\Pode\Public'

This example sets the path for the server's default 'Public' folder to 'C:\Pode\Public'.

#>
function Set-PodeDefaultFolder {

    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('Views', 'Public', 'Errors')]
        [string]
        $Type,

        [Parameter()]
        [string]
        $Path
    )
    if (Test-Path -Path $Path -PathType Container) {
        $PodeContext.Server.DefaultFolders[$Type] = $Path
    }
    else {
        throw "Folder $Path doesn't exist"
    }
}

<#
.SYNOPSIS
Retrieves the path of a specified default folder type from the Pode server context.

.DESCRIPTION
This function returns the path for one of the Pode server's default folder types: Views, Public, or Errors. It accesses the server's configuration stored in the `$PodeContext` variable and retrieves the path for the specified folder type from the `DefaultFolders` dictionary. This function is useful for scripts or modules that need to dynamically access server resources based on the server's current configuration.

.PARAMETER Type
The type of the default folder for which to retrieve the path. The valid options are 'Views', 'Public', or 'Errors'. This parameter determines which folder's path will be returned by the function.

.EXAMPLE
$path = Get-PodeDefaultFolder -Type 'Views'

This example retrieves the current path configured for the server's 'Views' folder and stores it in the `$path` variable.

.EXAMPLE
$path = Get-PodeDefaultFolder -Type 'Public'

This example retrieves the current path configured for the server's 'Public' folder.

.OUTPUTS
String. The file system path of the specified default folder.
#>
function Get-PodeDefaultFolder {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter()]
        [ValidateSet('Views', 'Public', 'Errors')]
        [string]
        $Type
    )

    return $PodeContext.Server.DefaultFolders[$Type]
}

<#
.SYNOPSIS
Attaches a breakpoint which can be used for debugging.

.DESCRIPTION
Attaches a breakpoint which can be used for debugging.

.EXAMPLE
Wait-PodeDebugger
#>
function Wait-PodeDebugger {
    [CmdletBinding()]
    param()

    if (!$PodeContext.Server.Debug.Breakpoints.Enabled) {
        return
    }

    Wait-Debugger
}
src\Public\Events.ps1
<#
.SYNOPSIS
Registers a script to be run when a certain server event occurs within Pode

.DESCRIPTION
Registers a script to be run when a certain server event occurs within Pode, such as Start, Terminate, and Restart.

.PARAMETER Type
The Type of event to be registered.

.PARAMETER Name
A unique Name for the registered event.

.PARAMETER ScriptBlock
A ScriptBlock to invoke when the event is triggered.

.PARAMETER ArgumentList
An array of arguments to supply to the ScriptBlock.

.EXAMPLE
Register-PodeEvent -Type Start -Name 'Event1' -ScriptBlock { }
#>
function Register-PodeEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # error if already registered
    if (Test-PodeEvent -Type $Type -Name $Name) {
        throw "$($Type) event already registered: $($Name)"
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add event
    $PodeContext.Server.Events[$Type][$Name] = @{
        Name           = $Name
        ScriptBlock    = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = $ArgumentList
    }
}

<#
.SYNOPSIS
Unregisters an event that has been registered with the specified Name.

.DESCRIPTION
Unregisters an event that has been registered with the specified Name.

.PARAMETER Type
The Type of the event to unregister.

.PARAMETER Name
The Name of the event to unregister.

.EXAMPLE
Unregister-PodeEvent -Type Start -Name 'Event1'
#>
function Unregister-PodeEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # error if not registered
    if (!(Test-PodeEvent -Type $Type -Name $Name)) {
        throw "No $($Type) event registered: $($Name)"
    }

    # remove event
    $null = $PodeContext.Server.Events[$Type].Remove($Name)
}

<#
.SYNOPSIS
Tests if an event has been registered with the specified Name.

.DESCRIPTION
Tests if an event has been registered with the specified Name.

.PARAMETER Type
The Type of the event to test.

.PARAMETER Name
The Name of the event to test.

.EXAMPLE
Test-PodeEvent -Type Start -Name 'Event1'
#>
function Test-PodeEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Events[$Type].Contains($Name)
}

<#
.SYNOPSIS
Retrieves an event.

.DESCRIPTION
Retrieves an event.

.PARAMETER Type
The Type of event to retrieve.

.PARAMETER Name
The Name of the event to retrieve.

.EXAMPLE
Get-PodeEvent -Type Start -Name 'Event1'
#>
function Get-PodeEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Events[$Type][$Name]
}

<#
.SYNOPSIS
Clears an event of all registered scripts.

.DESCRIPTION
Clears an event of all registered scripts.

.PARAMETER Type
The Type of event to clear.

.EXAMPLE
Clear-PodeEvent -Type Start
#>
function Clear-PodeEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
        [string]
        $Type
    )

    $null = $PodeContext.Server.Events[$Type].Clear()
}

<#
.SYNOPSIS
Automatically loads event ps1 files

.DESCRIPTION
Automatically loads event ps1 files from either a /events folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeEvents

.EXAMPLE
Use-PodeEvents -Path './my-events'
#>
function Use-PodeEvents {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'events'
}
src\Public\FileWatchers.ps1
<#
.SYNOPSIS
Adds a new File Watcher to monitor file changes in a directory.

.DESCRIPTION
Adds a new File Watcher to monitor file changes in a directory.

.PARAMETER Name
An optional Name for the File Watcher. (Default: GUID)

.PARAMETER EventName
An optional EventName to be monitored. Note: '*' refers to all event names. (Default: Changed, Created, Deleted, Renamed)

.PARAMETER Path
The Path to a directory which contains the files to be monitored.

.PARAMETER ScriptBlock
The ScriptBlock defining logic to be run when events are triggered.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the File Watcher's logic.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the File Watcher's ScriptBlock.

.PARAMETER NotifyFilter
The attributes on files to monitor and notify about. (Default: FileName, DirectoryName, LastWrite, CreationTime)

.PARAMETER Exclude
An optional array of file patterns to be excluded.

.PARAMETER Include
An optional array of file patterns to be included. (Default: *.*)

.PARAMETER InternalBufferSize
The InternalBufferSize of the file monitor, used when temporarily storing events. (Default: 8kb)

.PARAMETER NoSubdirectories
If supplied, the File Watcher will only monitor files in the specified directory path, and not in all sub-directories as well.

.PARAMETER PassThru
If supplied, the File Watcher object registered will be returned.

.EXAMPLE
Add-PodeFileWatcher -Path 'C:/Projects/:project/src' -Include '*.ps1' -ScriptBlock {}

.EXAMPLE
Add-PodeFileWatcher -Path 'C:/Websites/:site' -Include '*.config' -EventName Changed -ScriptBlock {}

.EXAMPLE
Add-PodeFileWatcher -Path '/temp/logs' -EventName Created -NotifyFilter CreationTime -ScriptBlock {}

.EXAMPLE
$watcher = Add-PodeFileWatcher -Path '/temp/logs' -Exclude *.txt -ScriptBlock {} -PassThru
#>
function Add-PodeFileWatcher {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter()]
        [string]
        $Name = $null,

        [Parameter()]
        [ValidateSet('Changed', 'Created', 'Deleted', 'Renamed', 'Existed', '*')]
        [string[]]
        $EventName = @('Changed', 'Created', 'Deleted', 'Renamed'),

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [System.IO.NotifyFilters[]]
        $NotifyFilter = @('FileName', 'DirectoryName', 'LastWrite', 'CreationTime'),

        [Parameter()]
        [string[]]
        $Exclude,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Include = '*.*',

        [Parameter()]
        [ValidateRange(4kb, 64kb)]
        [int]
        $InternalBufferSize = 8kb,

        [switch]
        $NoSubdirectories,

        [switch]
        $PassThru
    )

    # set random name
    if ([string]::IsNullOrEmpty($Name)) {
        $Name = New-PodeGuid -Secure
    }

    # set all for * event
    if ('*' -iin $EventName) {
        $EventName = @('Changed', 'Created', 'Deleted', 'Renamed', 'Existed')
    }

    # resolve path if relative
    if (!(Test-PodeIsPSCore)) {
        $Path = Convert-PodePlaceholders -Path $Path -Prepend '%' -Append '%'
    }

    $Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve

    if (!(Test-PodeIsPSCore)) {
        $Path = Convert-PodePlaceholders -Path $Path -Pattern '\%(?<tag>[\w]+)\%' -Prepend ':' -Append ([string]::Empty)
    }

    # resolve path, and test it
    $hasPlaceholders = Test-PodePlaceholders -Path $Path
    if ($hasPlaceholders) {
        $rgxPath = Update-PodeRouteSlashes -Path $Path -NoLeadingSlash
        $rgxPath = Resolve-PodePlaceholders -Path $rgxPath -Slashes
        $Path = $Path -ireplace (Get-PodePlaceholderRegex), '*'
    }

    # test path to make sure it exists
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "The path does not exist: $($Path)"
    }

    # test if we have the file watcher already
    if (Test-PodeFileWatcher -Name $Name) {
        throw "A File Watcher with the name '$($Name)' has already been defined"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # enable the file watcher threads
    $PodeContext.Fim.Enabled = $true

    # resolve the path's widacards if any
    $paths = @($Path)
    if ($Path.Contains('*')) {
        $paths = @(Get-ChildItem -Path $Path -Directory -Force | Select-Object -ExpandProperty FullName)
    }

    # add the file watcher
    $PodeContext.Fim.Items[$Name] = @{
        Name                  = $Name
        Events                = @($EventName)
        Path                  = $Path
        Placeholders          = @{
            Path  = $rgxPath
            Exist = $hasPlaceholders
        }
        Script                = $ScriptBlock
        UsingVariables        = $usingVars
        Arguments             = $ArgumentList
        NotifyFilters         = @($NotifyFilter)
        IncludeSubdirectories = !$NoSubdirectories.IsPresent
        InternalBufferSize    = $InternalBufferSize
        Exclude               = $Exclude
        Include               = $Include
        Paths                 = $paths
    }

    # return?
    if ($PassThru) {
        return $PodeContext.Fim.Items[$Name]
    }
}

<#
.SYNOPSIS
Tests whether the passed File Watcher exists.

.DESCRIPTION
Tests whether the passed File Watcher exists by its name.

.PARAMETER Name
The Name of the File Watcher.

.EXAMPLE
if (Test-PodeFileWatcher -Name WatcherName) { }
#>
function Test-PodeFileWatcher {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (($null -ne $PodeContext.Fim.Items) -and $PodeContext.Fim.Items.ContainsKey($Name))
}

<#
.SYNOPSIS
Returns any defined File Watchers.

.DESCRIPTION
Returns any defined File Watchers.

.PARAMETER Name
An optional File Watcher Name(s) to be returned.

.EXAMPLE
Get-PodeFileWatcher

.EXAMPLE
Get-PodeFileWatcher -Name Name1, Name2
#>
function Get-PodeFileWatcher {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    $watchers = $PodeContext.Fim.Items.Values

    # further filter by file watcher names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $watchers = @(foreach ($_name in $Name) {
                foreach ($watcher in $watchers) {
                    if ($watcher.Name -ine $_name) {
                        continue
                    }

                    $watcher
                }
            })
    }

    # return
    return $watchers
}

<#
.SYNOPSIS
Removes a specific File Watchers.

.DESCRIPTION
Removes a specific File Watchers.

.PARAMETER Name
The Name of the File Watcher to be removed.

.EXAMPLE
Remove-PodeFileWatcher -Name 'Logs'
#>
function Remove-PodeFileWatcher {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Fim.Items.Remove($Name)
}

<#
.SYNOPSIS
Removes all File Watchers.

.DESCRIPTION
Removes all File Watchers.

.EXAMPLE
Clear-PodeFileWatchers
#>
function Clear-PodeFileWatchers {
    [CmdletBinding()]
    param()

    $PodeContext.Fim.Items.Clear()
}

<#
.SYNOPSIS
Automatically loads File Watchers ps1 files

.DESCRIPTION
Automatically loads File Watchers ps1 files from either a /filewatcher folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeFileWatchers

.EXAMPLE
Use-PodeFileWatchers -Path './my-watchers'
#>
function Use-PodeFileWatchers {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'filewatchers'
}
src\Public\Flash.ps1
<#
.SYNOPSIS
Appends a message to the current flash messages stored in the session.

.DESCRIPTION
Appends a message to the current flash messages stored in the session for the supplied name.
The messages per name are stored as an array.

.PARAMETER Name
The name of the flash message to be appended.

.PARAMETER Message
The message to append.

.EXAMPLE
Add-PodeFlashMessage -Name 'error' -Message 'There was an error'
#>
function Add-PodeFlashMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Message
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # append the message against the key
    if ($null -eq $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash = @{}
    }

    if ($null -eq $WebEvent.Session.Data.Flash[$Name]) {
        $WebEvent.Session.Data.Flash[$Name] = @($Message)
    }
    else {
        $WebEvent.Session.Data.Flash[$Name] += @($Message)
    }
}

<#
.SYNOPSIS
Clears all flash messages.

.DESCRIPTION
Clears all of the flash messages currently stored in the session.

.EXAMPLE
Clear-PodeFlashMessages
#>
function Clear-PodeFlashMessages {
    [CmdletBinding()]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # clear all keys
    if ($null -ne $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash = @{}
    }
}

<#
.SYNOPSIS
Returns all flash messages stored against a name, and the clears the messages.

.DESCRIPTION
Returns all of the flash messages, as an array, currently stored for the name within the session.
Once retrieved, the messages are removed from storage.

.PARAMETER Name
The name of the flash messages to return.

.EXAMPLE
Get-PodeFlashMessage -Name 'error'
#>
function Get-PodeFlashMessage {
    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # retrieve messages from session, then delete it
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return @()
    }

    $v = @($WebEvent.Session.Data.Flash[$Name])
    $WebEvent.Session.Data.Flash.Remove($Name)

    if (Test-PodeIsEmpty $v) {
        return @()
    }

    return @($v)
}

<#
.SYNOPSIS
Returns all of the names for each of the messages currently being stored.

.DESCRIPTION
Returns all of the names for each of the messages currently being stored. This does not clear the messages.

.EXAMPLE
Get-PodeFlashMessageNames
#>
function Get-PodeFlashMessageNames {
    [CmdletBinding()]
    [OutputType([string[]])]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # return list of all current keys
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return @()
    }

    return @($WebEvent.Session.Data.Flash.Keys)
}

<#
.SYNOPSIS
Removes flash messages for the supplied name currently being stored.

.DESCRIPTION
Removes flash messages for the supplied name currently being stored.

.PARAMETER Name
The name of the flash messages to remove.

.EXAMPLE
Remove-PodeFlashMessage -Name 'error'
#>
function Remove-PodeFlashMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # remove key from flash messages
    if ($null -ne $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash.Remove($Name)
    }
}

<#
.SYNOPSIS
Tests if there are any flash messages currently being stored for a supplied name.

.DESCRIPTION
Tests if there are any flash messages currently being stored for a supplied name.

.PARAMETER Name
The name of the flash message to check.

.EXAMPLE
Test-PodeFlashMessage -Name 'error'
#>
function Test-PodeFlashMessage {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use Flash messages'
    }

    # return if a key exists as a flash message
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return $false
    }

    return $WebEvent.Session.Data.Flash.ContainsKey($Name)
}
src\Public\Handlers.ps1
<#
.SYNOPSIS
Adds a Handler of a specific Type.

.DESCRIPTION
Adds a Handler of a specific Type.

.PARAMETER Type
The Type of the Handler.

.PARAMETER Name
The Name of the Handler.

.PARAMETER ScriptBlock
The ScriptBlock for the Handler's main logic.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Handler's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Handler's ScriptBlock.

.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeHandler -Type Service -Name 'Looper' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeHandler {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Service', 'Smtp')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeHandler' -ThrowError

    # ensure handler isn't already set
    if ($PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
        throw "[$($Type)] $($Name): Handler already defined"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the handler
    Write-Verbose "Adding Handler: [$($Type)] $($Name)"
    $PodeContext.Server.Handlers[$Type][$Name] += @(@{
            Logic          = $ScriptBlock
            UsingVariables = $usingVars
            Arguments      = $ArgumentList
        })
}

<#
.SYNOPSIS
Remove a specific Handler.

.DESCRIPTION
Remove a specific Handler.

.PARAMETER Type
The type of the Handler to be removed.

.PARAMETER Name
The name of the Handler to be removed.

.EXAMPLE
Remove-PodeHandler -Type Smtp -Name 'Main'
#>
function Remove-PodeHandler {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Service', 'Smtp')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # ensure handler does exist
    if (!$PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
        return
    }

    # remove the handler
    $null = $PodeContext.Server.Handlers[$Type].Remove($Name)
}

<#
.SYNOPSIS
Removes all added Handlers, or Handlers of a specific Type.

.DESCRIPTION
Removes all added Handlers, or Handlers of a specific Type.

.PARAMETER Type
The Type of Handlers to remove.

.EXAMPLE
Clear-PodeHandlers -Type Smtp
#>
function Clear-PodeHandlers {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('', 'Service', 'Smtp')]
        [string]
        $Type
    )

    if (![string]::IsNullOrWhiteSpace($Type)) {
        $PodeContext.Server.Handlers[$Type].Clear()
    }
    else {
        $PodeContext.Server.Handlers.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Handlers[$_].Clear()
        }
    }
}

<#
.SYNOPSIS
Automatically loads handler ps1 files

.DESCRIPTION
Automatically loads handler ps1 files from either a /handler folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeHandlers

.EXAMPLE
Use-PodeHandlers -Path './my-handlers'
#>
function Use-PodeHandlers {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'handlers'
}
src\Public\Headers.ps1
<#
.SYNOPSIS
Appends a header against the Response.

.DESCRIPTION
Appends a header against the Response. If the current context is serverless, then this function acts like Set-PodeHeader.

.PARAMETER Name
The name of the header.

.PARAMETER Value
The value to set against the header.

.PARAMETER Secret
If supplied, the secret with which to sign the header's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Add-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Add-PodeHeader {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    # sign the value if we have a secret
    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict)
    }

    # add the header to the response
    if ($PodeContext.Server.IsServerless) {
        $WebEvent.Response.Headers[$Name] = $Value
    }
    else {
        $WebEvent.Response.Headers.Add($Name, $Value)
    }
}

<#
.SYNOPSIS
Appends multiple headers against the Response.

.DESCRIPTION
Appends multiple headers against the Response. If the current context is serverless, then this function acts like Set-PodeHeaderBulk.

.PARAMETER Values
A hashtable of headers to be appended.

.PARAMETER Secret
If supplied, the secret with which to sign the header values.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Add-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' }
#>
function Add-PodeHeaderBulk {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Values,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    foreach ($key in $Values.Keys) {
        $value = $Values[$key]

        # sign the value if we have a secret
        if (![string]::IsNullOrWhiteSpace($Secret)) {
            $value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict)
        }

        # add the header to the response
        if ($PodeContext.Server.IsServerless) {
            $WebEvent.Response.Headers[$key] = $value
        }
        else {
            $WebEvent.Response.Headers.Add($key, $value)
        }
    }
}

<#
.SYNOPSIS
Tests if a header is present on the Request.

.DESCRIPTION
Tests if a header is present on the Request.

.PARAMETER Name
The name of the header to test.

.EXAMPLE
Test-PodeHeader -Name 'X-AuthToken'
#>
function Test-PodeHeader {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $header = (Get-PodeHeader -Name $Name)
    return (![string]::IsNullOrWhiteSpace($header))
}

<#
.SYNOPSIS
Retrieves the value of a header from the Request.

.DESCRIPTION
Retrieves the value of a header from the Request.

.PARAMETER Name
The name of the header to retrieve.

.PARAMETER Secret
The secret used to unsign the header's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Get-PodeHeader -Name 'X-AuthToken'
#>
function Get-PodeHeader {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    # get the value for the header from the request
    $header = $WebEvent.Request.Headers.$Name

    # 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
}

<#
.SYNOPSIS
Sets a header on the Response, clearing all current values for the header.

.DESCRIPTION
Sets a header on the Response, clearing all current values for the header.

.PARAMETER Name
The name of the header.

.PARAMETER Value
The value to set against the header.

.PARAMETER Secret
If supplied, the secret with which to sign the header's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Set-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Set-PodeHeader {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    # sign the value if we have a secret
    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict)
    }

    # set the header on the response
    if ($PodeContext.Server.IsServerless) {
        $WebEvent.Response.Headers[$Name] = $Value
    }
    else {
        $WebEvent.Response.Headers.Set($Name, $Value)
    }
}

<#
.SYNOPSIS
Sets multiple headers on the Response, clearing all current values for the header.

.DESCRIPTION
Sets multiple headers on the Response, clearing all current values for the header.

.PARAMETER Values
A hashtable of headers to be set.

.PARAMETER Secret
If supplied, the secret with which to sign the header values.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Set-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' }
#>
function Set-PodeHeaderBulk {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Values,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    foreach ($key in $Values.Keys) {
        $value = $Values[$key]

        # sign the value if we have a secret
        if (![string]::IsNullOrWhiteSpace($Secret)) {
            $value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict)
        }

        # set the header on the response
        if ($PodeContext.Server.IsServerless) {
            $WebEvent.Response.Headers[$key] = $value
        }
        else {
            $WebEvent.Response.Headers.Set($key, $value)
        }
    }
}

<#
.SYNOPSIS
Tests if a header on the Request is validly signed.

.DESCRIPTION
Tests if a header on the Request is validly signed, by attempting to unsign it using some secret.

.PARAMETER Name
The name of the header to test.

.PARAMETER Secret
A secret to use for attempting to unsign the header's value.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Test-PodeHeaderSigned -Name 'X-Header-Name' -Secret 'hunter2'
#>
function Test-PodeHeaderSigned {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    $header = Get-PodeHeader -Name $Name
    return Test-PodeValueSigned -Value $header -Secret $Secret -Strict:$Strict
}
src\Public\Logging.ps1
<#
.SYNOPSIS
Create a new method of outputting logs.

.DESCRIPTION
Create a new method of outputting logs.

.PARAMETER Terminal
If supplied, will use the inbuilt Terminal logging output method.

.PARAMETER File
If supplied, will use the inbuilt File logging output method.

.PARAMETER Path
The File Path of where to store the logs.

.PARAMETER Name
The File Name to prepend new log files using.

.PARAMETER EventViewer
If supplied, will use the inbuilt Event Viewer logging output method.

.PARAMETER EventLogName
Optional Log Name for the Event Viewer (Default: Application)

.PARAMETER Source
Optional Source for the Event Viewer (Default: Pode)

.PARAMETER EventID
Optional EventID for the Event Viewer (Default: 0)

.PARAMETER Batch
An optional batch size to write log items in bulk (Default: 1)

.PARAMETER BatchTimeout
An optional batch timeout, in seconds, to send items off for writing if a log item isn't received (Default: 0)

.PARAMETER MaxDays
The maximum number of days to keep logs, before Pode automatically removes them.

.PARAMETER MaxSize
The maximum size of a log file, before Pode starts writing to a new log file.

.PARAMETER Custom
If supplied, will allow you to create a Custom Logging output method.

.PARAMETER ScriptBlock
The ScriptBlock that defines how to output a log item.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logging output method's ScriptBlock.

.EXAMPLE
$term_logging = New-PodeLoggingMethod -Terminal

.EXAMPLE
$file_logging = New-PodeLoggingMethod -File -Path ./logs -Name 'requests'

.EXAMPLE
$custom_logging = New-PodeLoggingMethod -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeLoggingMethod {
    [CmdletBinding(DefaultParameterSetName = 'Terminal')]
    [OutputType([hashtable])]
    param(
        [Parameter(ParameterSetName = 'Terminal')]
        [switch]
        $Terminal,

        [Parameter(ParameterSetName = 'File')]
        [switch]
        $File,

        [Parameter(ParameterSetName = 'File')]
        [string]
        $Path = './logs',

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'EventViewer')]
        [switch]
        $EventViewer,

        [Parameter(ParameterSetName = 'EventViewer')]
        [string]
        $EventLogName = 'Application',

        [Parameter(ParameterSetName = 'EventViewer')]
        [string]
        $Source = 'Pode',

        [Parameter(ParameterSetName = 'EventViewer')]
        [int]
        $EventID = 0,

        [Parameter()]
        [int]
        $Batch = 1,

        [Parameter()]
        [int]
        $BatchTimeout = 0,

        [Parameter(ParameterSetName = 'File')]
        [ValidateScript({
                if ($_ -lt 0) {
                    throw "MaxDays must be 0 or greater, but got: $($_)s"
                }

                return $true
            })]
        [int]
        $MaxDays = 0,

        [Parameter(ParameterSetName = 'File')]
        [ValidateScript({
                if ($_ -lt 0) {
                    throw "MaxSize must be 0 or greater, but got: $($_)s"
                }

                return $true
            })]
        [int]
        $MaxSize = 0,

        [Parameter(ParameterSetName = 'Custom')]
        [switch]
        $Custom,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
        [ValidateScript({
                if (Test-PodeIsEmpty $_) {
                    throw 'A non-empty ScriptBlock is required for the Custom logging output method'
                }

                return $true
            })]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'Custom')]
        [object[]]
        $ArgumentList
    )

    # batch details
    $batchInfo = @{
        Size       = $Batch
        Timeout    = $BatchTimeout
        LastUpdate = $null
        Items      = @()
        RawItems   = @()
    }

    # return info on appropriate logging type
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'terminal' {
            return @{
                ScriptBlock = (Get-PodeLoggingTerminalMethod)
                Batch       = $batchInfo
                Arguments   = @{}
            }
        }

        'file' {
            $Path = (Protect-PodeValue -Value $Path -Default './logs')
            $Path = (Get-PodeRelativePath -Path $Path -JoinRoot)
            $null = New-Item -Path $Path -ItemType Directory -Force

            return @{
                ScriptBlock = (Get-PodeLoggingFileMethod)
                Batch       = $batchInfo
                Arguments   = @{
                    Name          = $Name
                    Path          = $Path
                    MaxDays       = $MaxDays
                    MaxSize       = $MaxSize
                    FileId        = 0
                    Date          = $null
                    NextClearDown = [datetime]::Now.Date
                }
            }
        }

        'eventviewer' {
            # only windows
            if (!(Test-PodeIsWindows)) {
                throw 'Event Viewer logging only supported on Windows'
            }

            # create source
            if (![System.Diagnostics.EventLog]::SourceExists($Source)) {
                $null = [System.Diagnostics.EventLog]::CreateEventSource($Source, $EventLogName)
            }

            return @{
                ScriptBlock = (Get-PodeLoggingEventViewerMethod)
                Batch       = $batchInfo
                Arguments   = @{
                    LogName = $EventLogName
                    Source  = $Source
                    ID      = $EventID
                }
            }
        }

        'custom' {
            $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

            return @{
                ScriptBlock    = $ScriptBlock
                UsingVariables = $usingVars
                Batch          = $batchInfo
                Arguments      = $ArgumentList
            }
        }
    }
}


<#
.SYNOPSIS
Enables Request Logging using a supplied output method.

.DESCRIPTION
Enables Request Logging using a supplied output method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER UsernameProperty
An optional property path within the $WebEvent.Auth.User object for the user's Username. (Default: Username).

.PARAMETER Raw
If supplied, the log item returned will be the raw Request item as a hashtable and not a string (for Custom methods).

.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
#>
function Enable-PodeRequestLogging {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Method,

        [Parameter()]
        [string]
        $UsernameProperty,

        [switch]
        $Raw
    )

    Test-PodeIsServerless -FunctionName 'Enable-PodeRequestLogging' -ThrowError

    $name = Get-PodeRequestLoggingName

    # error if it's already enabled
    if ($PodeContext.Server.Logging.Types.Contains($name)) {
        throw 'Request Logging has already been enabled'
    }

    # ensure the Method contains a scriptblock
    if (Test-PodeIsEmpty $Method.ScriptBlock) {
        throw 'The supplied output Method for Request Logging requires a valid ScriptBlock'
    }

    # username property
    if ([string]::IsNullOrWhiteSpace($UsernameProperty)) {
        $UsernameProperty = 'Username'
    }

    # add the request logger
    $PodeContext.Server.Logging.Types[$name] = @{
        Method      = $Method
        ScriptBlock = (Get-PodeLoggingInbuiltType -Type Requests)
        Properties  = @{
            Username = $UsernameProperty
        }
        Arguments   = @{
            Raw = $Raw
        }
    }
}

<#
.SYNOPSIS
Disables Request Logging.

.DESCRIPTION
Disables Request Logging.

.EXAMPLE
Disable-PodeRequestLogging
#>
function Disable-PodeRequestLogging {
    [CmdletBinding()]
    param()

    Remove-PodeLogger -Name (Get-PodeRequestLoggingName)
}

<#
.SYNOPSIS
Enables Error Logging using a supplied output method.

.DESCRIPTION
Enables Error Logging using a supplied output method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER Levels
The Levels of errors that should be logged (default is Error).

.PARAMETER Raw
If supplied, the log item returned will be the raw Error item as a hashtable and not a string (for Custom methods).

.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
#>
function Enable-PodeErrorLogging {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Method,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug', '*')]
        [string[]]
        $Levels = @('Error'),

        [switch]
        $Raw
    )

    $name = Get-PodeErrorLoggingName

    # error if it's already enabled
    if ($PodeContext.Server.Logging.Types.Contains($name)) {
        throw 'Error Logging has already been enabled'
    }

    # ensure the Method contains a scriptblock
    if (Test-PodeIsEmpty $Method.ScriptBlock) {
        throw 'The supplied output Method for Error Logging requires a valid ScriptBlock'
    }

    # all errors?
    if ($Levels -contains '*') {
        $Levels = @('Error', 'Warning', 'Informational', 'Verbose', 'Debug')
    }

    # add the error logger
    $PodeContext.Server.Logging.Types[$name] = @{
        Method      = $Method
        ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors)
        Arguments   = @{
            Raw    = $Raw
            Levels = $Levels
        }
    }
}

<#
.SYNOPSIS
Disables Error Logging.

.DESCRIPTION
Disables Error Logging.

.EXAMPLE
Disable-PodeErrorLogging
#>
function Disable-PodeErrorLogging {
    [CmdletBinding()]
    param()

    Remove-PodeLogger -Name (Get-PodeErrorLoggingName)
}

<#
.SYNOPSIS
Adds a custom Logging method for parsing custom log items.

.DESCRIPTION
Adds a custom Logging method for parsing custom log items.

.PARAMETER Name
A unique Name for the Logging method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER ScriptBlock
The ScriptBlock defining logic that transforms an item, and returns it for outputting.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logger's ScriptBlock.

.EXAMPLE
New-PodeLoggingMethod -Terminal | Add-PodeLogger -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeLogger {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Method,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if (Test-PodeIsEmpty $_) {
                    throw 'A non-empty ScriptBlock is required for the logging method'
                }

                return $true
            })]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure the name doesn't already exist
    if ($PodeContext.Server.Logging.Types.ContainsKey($Name)) {
        throw "Logging method already defined: $($Name)"
    }

    # ensure the Method contains a scriptblock
    if (Test-PodeIsEmpty $Method.ScriptBlock) {
        throw "The supplied output Method for the '$($Name)' Logging method requires a valid ScriptBlock"
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add logging method to server
    $PodeContext.Server.Logging.Types[$Name] = @{
        Method         = $Method
        ScriptBlock    = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = $ArgumentList
    }
}

<#
.SYNOPSIS
Removes a configured Logging method.

.DESCRIPTION
Removes a configured Logging method.

.PARAMETER Name
The Name of the Logging method.

.EXAMPLE
Remove-PodeLogger -Name 'LogName'
#>
function Remove-PodeLogger {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Server.Logging.Types.Remove($Name)
}

<#
.SYNOPSIS
Clears all Logging methods that have been configured.

.DESCRIPTION
Clears all Logging methods that have been configured.

.EXAMPLE
Clear-PodeLoggers
#>
function Clear-PodeLoggers {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Logging.Types.Clear()
}

<#
.SYNOPSIS
Writes and Exception or ErrorRecord using the inbuilt error logging.

.DESCRIPTION
Writes and Exception or ErrorRecord using the inbuilt error logging.

.PARAMETER Exception
An Exception to write.

.PARAMETER ErrorRecord
An ErrorRecord to write.

.PARAMETER Level
The Level of the error being logged.

.PARAMETER CheckInnerException
If supplied, any exceptions are check for inner exceptions. If one is present, this is also logged.

.EXAMPLE
try { /* logic */ } catch { $_ | Write-PodeErrorLog }

.EXAMPLE
[System.Exception]::new('error message') | Write-PodeErrorLog
#>
function Write-PodeErrorLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')]
        [System.Exception]
        $Exception,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')]
        [string]
        $Level = 'Error',

        [Parameter(ParameterSetName = 'Exception')]
        [switch]
        $CheckInnerException
    )

    # do nothing if logging is disabled, or error logging isn't setup
    $name = Get-PodeErrorLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # do nothing if the error level isn't present
    $levels = @(Get-PodeErrorLoggingLevels)
    if ($levels -inotcontains $Level) {
        return
    }

    # build error object for what we need
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'exception' {
            $item = @{
                Category   = $Exception.Source
                Message    = $Exception.Message
                StackTrace = $Exception.StackTrace
            }
        }

        'error' {
            $item = @{
                Category   = $ErrorRecord.CategoryInfo.ToString()
                Message    = $ErrorRecord.Exception.Message
                StackTrace = $ErrorRecord.ScriptStackTrace
            }
        }
    }

    # add general info
    $item['Server'] = $PodeContext.Server.ComputerName
    $item['Level'] = $Level
    $item['Date'] = [datetime]::Now
    $item['ThreadId'] = [int]$ThreadId

    # add the item to be processed
    $null = $PodeContext.LogsToProcess.Add(@{
            Name = $name
            Item = $item
        })

    # for exceptions, check the inner exception
    if ($CheckInnerException -and ($null -ne $Exception.InnerException) -and ![string]::IsNullOrWhiteSpace($Exception.InnerException.Message)) {
        $Exception.InnerException | Write-PodeErrorLog
    }
}

<#
.SYNOPSIS
Write an object to a configured custom Logging method.

.DESCRIPTION
Write an object to a configured custom Logging method.

.PARAMETER Name
The Name of the Logging method.

.PARAMETER InputObject
The Object to write.

.EXAMPLE
$object | Write-PodeLog -Name 'LogName'
#>
function Write-PodeLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject
    )

    # do nothing if logging is disabled, or logger isn't setup
    if (!(Test-PodeLoggerEnabled -Name $Name)) {
        return
    }

    # add the item to be processed
    $null = $PodeContext.LogsToProcess.Add(@{
            Name = $Name
            Item = $InputObject
        })
}

<#
.SYNOPSIS
Masks values within a log item to protect sensitive information.

.DESCRIPTION
Masks values within a log item, or any string, to protect sensitive information.
Patterns, and the Mask, can be configured via the server.psd1 configuration file.

.PARAMETER Item
The string Item to mask values.

.EXAMPLE
$value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2'
#>
function Protect-PodeLogItem {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string]
        $Item
    )

    # do nothing if there are no masks
    if (Test-PodeIsEmpty $PodeContext.Server.Logging.Masking.Patterns) {
        return $item
    }

    # attempt to apply each mask
    foreach ($mask in $PodeContext.Server.Logging.Masking.Patterns) {
        if ($Item -imatch $mask) {
            # has both keep before/after
            if ($Matches.ContainsKey('keep_before') -and $Matches.ContainsKey('keep_after')) {
                $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
            }

            # has just keep before
            elseif ($Matches.ContainsKey('keep_before')) {
                $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)")
            }

            # has just keep after
            elseif ($Matches.ContainsKey('keep_after')) {
                $Item = ($Item -ireplace $mask, "$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
            }

            # normal mask
            else {
                $Item = ($Item -ireplace $mask, $PodeContext.Server.Logging.Masking.Mask)
            }
        }
    }

    return $Item
}

<#
.SYNOPSIS
Automatically loads logging ps1 files

.DESCRIPTION
Automatically loads logging ps1 files from either a /logging folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeLogging

.EXAMPLE
Use-PodeLogging -Path './my-logging'
#>
function Use-PodeLogging {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'logging'
}
src\Public\Metrics.ps1
<#
.SYNOPSIS
Returns the uptime of the server in milliseconds.

.DESCRIPTION
Returns the uptime of the server in milliseconds. You can optionally return the total uptime regardless of server restarts.

.PARAMETER Total
If supplied, the total uptime of the server will be returned, regardless of restarts.

.EXAMPLE
$currentUptime = Get-PodeServerUptime

.EXAMPLE
$totalUptime = Get-PodeServerUptime -Total
#>
function Get-PodeServerUptime {
    [CmdletBinding()]
    param(
        [switch]
        $Total
    )

    $time = $PodeContext.Metrics.Server.StartTime
    if ($Total) {
        $time = $PodeContext.Metrics.Server.InitialLoadTime
    }

    return [long]([datetime]::UtcNow - $time).TotalMilliseconds
}

<#
.SYNOPSIS
Returns the number of times the server has restarted.

.DESCRIPTION
Returns the number of times the server has restarted.

.EXAMPLE
$restarts = Get-PodeServerRestartCount
#>
function Get-PodeServerRestartCount {
    [CmdletBinding()]
    param()

    return $PodeContext.Metrics.Server.RestartCount
}

<#
.SYNOPSIS
Returns the total number of requests/per status code the Server has receieved.

.DESCRIPTION
Returns the total number of requests/per status code the Server has receieved.

.PARAMETER StatusCode
If supplied, will return the total number of requests for a specific StatusCode.

.PARAMETER Total
If supplied, will return the Total number of Requests.

.EXAMPLE
$totalReqs = Get-PodeServerRequestMetric -Total

.EXAMPLE
$statusReqs = Get-PodeServerRequestMetric

.EXAMPLE
$404Reqs = Get-PodeServerRequestMetric -StatusCode 404
#>
function Get-PodeServerRequestMetric {
    [CmdletBinding(DefaultParameterSetName = 'StatusCode')]
    param(
        [Parameter(ParameterSetName = 'StatusCode')]
        [int]
        $StatusCode = 0,

        [Parameter(ParameterSetName = 'Total')]
        [switch]
        $Total
    )

    if ($Total) {
        return $PodeContext.Metrics.Requests.Total
    }

    if (($StatusCode -le 0)) {
        return $PodeContext.Metrics.Requests.StatusCodes
    }

    $strCode = "$($StatusCode)"
    if (!$PodeContext.Metrics.Requests.StatusCodes.ContainsKey($strCode)) {
        return 0
    }

    return $PodeContext.Metrics.Requests.StatusCodes[$strCode]
}

<#
.SYNOPSIS
Returns the total number of Signal requests the Server has receieved.

.DESCRIPTION
Returns the total number of Signal requests the Server has receieved.

.EXAMPLE
$totalReqs = Get-PodeServerSignalMetric
#>
function Get-PodeServerSignalMetric {
    [CmdletBinding()]
    param()

    return $PodeContext.Metrics.Signals.Total
}

<#
.SYNOPSIS
Returns the count of active requests.

.DESCRIPTION
Returns the count of all, processing, or queued active requests.

.PARAMETER CountType
The count type to return. (Default: Total)

.EXAMPLE
Get-PodeServerActiveRequestMetric

.EXAMPLE
Get-PodeServerActiveRequestMetric -CountType Queued
#>
function Get-PodeServerActiveRequestMetric {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Total', 'Queued', 'Processing')]
        [string]
        $CountType = 'Total'
    )

    switch ($CountType.ToLowerInvariant()) {
        'total' {
            return $PodeContext.Server.Signals.Listener.Contexts.Count
        }

        'queued' {
            return $PodeContext.Server.Signals.Listener.Contexts.QueuedCount
        }

        'processing' {
            return $PodeContext.Server.Signals.Listener.Contexts.ProcessingCount
        }
    }
}

<#
.SYNOPSIS
Returns the count of active signals.

.DESCRIPTION
Returns the count of all, processing, or queued active signals; for either server or client signals.

.PARAMETER Type
The type of signal to return. (Default: Total)

.PARAMETER CountType
The count type to return. (Default: Total)

.EXAMPLE
Get-PodeServerActiveSignalMetric

.EXAMPLE
Get-PodeServerActiveSignalMetric -Type Client -CountType Queued
#>
function Get-PodeServerActiveSignalMetric {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Total', 'Server', 'Client')]
        [string]
        $Type = 'Total',

        [Parameter()]
        [ValidateSet('Total', 'Queued', 'Processing')]
        [string]
        $CountType = 'Total'
    )

    switch ($Type.ToLowerInvariant()) {
        'total' {
            switch ($CountType.ToLowerInvariant()) {
                'total' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.Count + $PodeContext.Server.Signals.Listener.ClientSignals.Count
                }

                'queued' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.QueuedCount + $PodeContext.Server.Signals.Listener.ClientSignals.QueuedCount
                }

                'processing' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.ProcessingCount + $PodeContext.Server.Signals.Listener.ClientSignals.ProcessingCount
                }
            }
        }

        'server' {
            switch ($CountType.ToLowerInvariant()) {
                'total' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.Count
                }

                'queued' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.QueuedCount
                }

                'processing' {
                    return $PodeContext.Server.Signals.Listener.ServerSignals.ProcessingCount
                }
            }
        }

        'client' {
            switch ($CountType.ToLowerInvariant()) {
                'total' {
                    return $PodeContext.Server.Signals.Listener.ClientSignals.Count
                }

                'queued' {
                    return $PodeContext.Server.Signals.Listener.ClientSignals.QueuedCount
                }

                'processing' {
                    return $PodeContext.Server.Signals.Listener.ClientSignals.ProcessingCount
                }
            }
        }
    }
}
src\Public\Middleware.ps1
<#
.SYNOPSIS
Adds an access rule to allow or deny IP addresses.

.DESCRIPTION
Adds an access rule to allow or deny IP addresses.

.PARAMETER Access
The type of access to enable.

.PARAMETER Type
What type of request are we configuring?

.PARAMETER Values
A single, or an array of values.

.EXAMPLE
Add-PodeAccessRule -Access Allow -Type IP -Values '127.0.0.1'

.EXAMPLE
Add-PodeAccessRule -Access Deny -Type IP -Values @('192.168.1.1', '10.10.1.0/24')
#>
function Add-PodeAccessRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Access,

        [Parameter(Mandatory = $true)]
        [ValidateSet('IP')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string[]]
        $Values
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeAccessRule' -ThrowError

    # call the appropriate access method
    switch ($Type.ToLowerInvariant()) {
        'ip' {
            foreach ($ip in $Values) {
                Add-PodeIPAccess -Access $Access -IP $ip
            }
        }
    }
}

<#
.SYNOPSIS
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.

.DESCRIPTION
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.

.PARAMETER Type
What type of request is being rate limited: IP, Route, or Endpoint?

.PARAMETER Values
A single, or an array of values.

.PARAMETER Limit
The maximum number of requests to allow.

.PARAMETER Seconds
The number of seconds to count requests before restarting the count.

.PARAMETER Group
If supplied, groups of IPs in a subnet will be considered as one IP.

.EXAMPLE
Add-PodeLimitRule -Type IP -Values '127.0.0.1' -Limit 10 -Seconds 1

.EXAMPLE
Add-PodeLimitRule -Type IP -Values @('192.168.1.1', '10.10.1.0/24') -Limit 50 -Seconds 1 -Group

.EXAMPLE
Add-PodeLimitRule -Type Route -Values '/downloads' -Limit 5 -Seconds 1
#>
function Add-PodeLimitRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('IP', 'Route', 'Endpoint')]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string[]]
        $Values,

        [Parameter(Mandatory = $true)]
        [int]
        $Limit,

        [Parameter(Mandatory = $true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # call the appropriate limit method
    foreach ($value in $Values) {
        switch ($Type.ToLowerInvariant()) {
            'ip' {
                Test-PodeIsServerless -FunctionName 'Add-PodeLimitRule' -ThrowError
                Add-PodeIPLimit -IP $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }

            'route' {
                Add-PodeRouteLimit -Path $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }

            'endpoint' {
                Add-PodeEndpointLimit -EndpointName $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }
        }
    }
}

<#
.SYNOPSIS
Creates and returns a new secure token for use with CSRF.

.DESCRIPTION
Creates and returns a new secure token for use with CSRF.

.EXAMPLE
$token = New-PodeCsrfToken
#>
function New-PodeCsrfToken {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        throw 'CSRF Middleware has not been initialised'
    }

    # generate a new secret and salt
    $Secret = New-PodeCsrfSecret
    $Salt = (New-PodeSalt -Length 8)

    # return a new token
    return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}

<#
.SYNOPSIS
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.

.DESCRIPTION
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.

.EXAMPLE
$csrf = Get-PodeCsrfMiddleware
Add-PodeRoute -Method Get -Path '/cpu' -Middleware $csrf -ScriptBlock { /* logic */ }
#>
function Get-PodeCsrfMiddleware {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        throw 'CSRF Middleware has not been initialised'
    }

    # return scriptblock for the csrf route middleware to test tokens
    $script = {
        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    return (New-PodeMiddleware -ScriptBlock $script)
}

<#
.SYNOPSIS
Initialises CSRF within Pode for adhoc usage.

.DESCRIPTION
Initialises CSRF within Pode for adhoc usage, with configurable HTTP methods to ignore verification.

.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.

.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.

.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.

.EXAMPLE
Initialize-PodeCsrf -IgnoreMethods @('Get', 'Trace')

.EXAMPLE
Initialize-PodeCsrf -Secret 'some-secret' -UseCookies
#>
function Initialize-PodeCsrf {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $UseCookies
    )

    # check that csrf logic hasn't already been intialised
    if (Test-PodeCsrfConfigured) {
        return
    }

    # if sessions haven't been setup and we're not using cookies, error
    if (!$UseCookies -and !(Test-PodeSessionsEnabled)) {
        throw 'Sessions are required to use CSRF unless you want to use cookies'
    }

    # if we're using cookies, ensure a global secret exists
    if ($UseCookies) {
        $Secret = (Protect-PodeValue -Value $Secret -Default (Get-PodeCookieSecret -Global))

        if (Test-PodeIsEmpty $Secret) {
            throw "When using cookies for CSRF, a Secret is required. You can either supply a Secret, or set the Cookie global secret - (Set-PodeCookieSecret '<value>' -Global)"
        }
    }

    # set the options against the server context
    $PodeContext.Server.Cookies.Csrf = @{
        Name           = 'pode.csrf'
        UseCookies     = $UseCookies
        Secret         = $Secret
        IgnoredMethods = $IgnoreMethods
    }
}

<#
.SYNOPSIS
Enables Middleware for verifying CSRF tokens on Requests.

.DESCRIPTION
Enables Middleware for verifying CSRF tokens on Requests, with configurable HTTP methods to ignore verification.

.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.

.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.

.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.

.EXAMPLE
Enable-PodeCsrfMiddleware -IgnoreMethods @('Get', 'Trace')

.EXAMPLE
Enable-PodeCsrfMiddleware -Secret 'some-secret' -UseCookies
#>
function Enable-PodeCsrfMiddleware {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter(ParameterSetName = 'Cookies')]
        [string]
        $Secret,

        [Parameter(ParameterSetName = 'Cookies')]
        [switch]
        $UseCookies
    )

    Initialize-PodeCsrf -IgnoreMethods $IgnoreMethods -Secret $Secret -UseCookies:$UseCookies

    # return scriptblock for the csrf middleware
    $script = {
        # if the current route method is ignored, just return
        $ignored = @($PodeContext.Server.Cookies.Csrf.IgnoredMethods)
        if (!(Test-PodeIsEmpty $ignored) -and ($ignored -icontains $WebEvent.Method)) {
            return $true
        }

        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    (New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_csrf__'
}

<#
.SYNOPSIS
Adds a custom body parser middleware.

.DESCRIPTION
Adds a custom body parser middleware script for a content-type, which will be used if a payload is sent with a Request.

.PARAMETER ContentType
The ContentType of the custom body parser.

.PARAMETER ScriptBlock
The ScriptBlock that will parse the body content, and return the result.

.EXAMPLE
Add-PodeBodyParser -ContentType 'application/json' -ScriptBlock { param($body) /* parsing logic */ }
#>
function Add-PodeBodyParser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock
    )

    # if a parser for the type already exists, fail
    if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
        throw "There is already a body parser defined for the $($ContentType) content-type"
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    $PodeContext.Server.BodyParsers[$ContentType] = @{
        ScriptBlock    = $ScriptBlock
        UsingVariables = $usingVars
    }
}

<#
.SYNOPSIS
Removes a custom body parser.

.DESCRIPTION
Removes a custom body parser middleware script for a content-type.

.PARAMETER ContentType
The ContentType of the custom body parser.

.EXAMPLE
Remove-PodeBodyParser -ContentType 'application/json'
#>
function Remove-PodeBodyParser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType
    )

    # if there's no parser for the type, return
    if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
        return
    }

    $null = $PodeContext.Server.BodyParsers.Remove($ContentType)
}

<#
.SYNOPSIS
Adds a new Middleware to be invoked before every Route, or certain Routes.

.DESCRIPTION
Adds a new Middleware to be invoked before every Route, or certain Routes. ScriptBlock should return $true to continue execution, or $false to stop.

.PARAMETER Name
The Name of the Middleware.

.PARAMETER ScriptBlock
The Script defining the logic of the Middleware. Should return $true to continue execution, or $false to stop.

.PARAMETER InputObject
A Middleware HashTable from New-PodeMiddleware, or from certain other functions that return Middleware as a HashTable.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.

.OUTPUTS
Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution.

.EXAMPLE
Add-PodeMiddleware -Name 'BlockAgents' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeMiddleware -Name 'CheckEmailOnApi' -Route '/api/*' -ScriptBlock { /* logic */ }
#>
function Add-PodeMiddleware {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'Input', ValueFromPipeline = $true)]
        [hashtable]
        $InputObject,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure name doesn't already exist
    if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
        throw "[Middleware] $($Name): Middleware already defined"
    }

    # if it's a script - call New-PodeMiddleware
    if ($PSCmdlet.ParameterSetName -ieq 'script') {
        $InputObject = (New-PodeMiddlewareInternal `
                -ScriptBlock $ScriptBlock `
                -Route $Route `
                -ArgumentList $ArgumentList `
                -PSSession $PSCmdlet.SessionState)
    }
    else {
        $Route = ConvertTo-PodeRouteRegex -Path $Route
        $InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route
        $InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options
    }

    # ensure we have a script to run
    if (Test-PodeIsEmpty $InputObject.Logic) {
        throw '[Middleware]: No logic supplied in ScriptBlock'
    }

    # set name, and override route/args
    $InputObject.Name = $Name

    # add the logic to array of middleware that needs to be run
    $PodeContext.Server.Middleware += $InputObject
}

<#
.SYNOPSIS
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes.

.DESCRIPTION
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes. ScriptBlock should return $true to continue execution, or $false to stop.

.PARAMETER ScriptBlock
The Script that defines the logic of the Middleware. Should return $true to continue execution, or $false to stop.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.

.OUTPUTS
Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution.

.EXAMPLE
New-PodeMiddleware -ScriptBlock { /* logic */ } -ArgumentList 'Email' | Add-PodeMiddleware -Name 'CheckEmail'
#>
function New-PodeMiddleware {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    return New-PodeMiddlewareInternal `
        -ScriptBlock $ScriptBlock `
        -Route $Route `
        -ArgumentList $ArgumentList `
        -PSSession $PSCmdlet.SessionState
}

<#
.SYNOPSIS
Removes a specific user defined Middleware.

.DESCRIPTION
Removes a specific user defined Middleware.

.PARAMETER Name
The Name of the Middleware to be removed.

.EXAMPLE
Remove-PodeMiddleware -Name 'Sessions'
#>
function Remove-PodeMiddleware {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
}

<#
.SYNOPSIS
Removes all user defined Middleware.

.DESCRIPTION
Removes all user defined Middleware.

.EXAMPLE
Clear-PodeMiddleware
#>
function Clear-PodeMiddleware {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Middleware = @()
}

<#
.SYNOPSIS
Automatically loads middleware ps1 files

.DESCRIPTION
Automatically loads middleware ps1 files from either a /middleware folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeMiddleware

.EXAMPLE
Use-PodeMiddleware -Path './my-middleware'
#>
function Use-PodeMiddleware {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'middleware'
}
src\Public\OAComponents.ps1
<#
.SYNOPSIS
Adds a reusable component for responses.

.DESCRIPTION
Adds a reusable component for responses.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.LINK
https://swagger.io/docs/specification/serialization/

.PARAMETER Name
The reference Name of the response.

.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).

.PARAMETER Headers
The header name and schema the response returns (the schema is created using the Add-PodeOAComponentHeader cmdlet).

.PARAMETER Description
The Description of the response.

.PARAMETER Reference
A Reference Name of an existing component response to use.

.PARAMETER Links
A Response link definition

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAComponentResponse -Name 'OKResponse' -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }

.EXAMPLE
Add-PodeOAComponentResponse -Name 'ErrorResponse' -Content  @{ 'application/json' = 'ErrorSchema' }
#>
function Add-PodeOAComponentResponse {
    [CmdletBinding(DefaultParameterSetName = 'Schema')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Schema')]
        [Alias('ContentSchemas')]
        [hashtable]
        $Content,

        [Parameter(ParameterSetName = 'Schema')]
        [Alias('HeaderSchemas')]
        [AllowEmptyString()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] })]
        $Headers,

        [Parameter(ParameterSetName = 'Schema')]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [string]
        $Reference,

        [Parameter(ParameterSetName = 'Schema')]
        [System.Collections.Specialized.OrderedDictionary ]
        $Links,

        [string[]]
        $DefinitionTag
    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    foreach ($tag in $DefinitionTag) {
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.responses[$Name] = New-PodeOResponseInternal -DefinitionTag $tag  -Params $PSBoundParameters
    }
}


<#
.SYNOPSIS
Adds a reusable component schema

.DESCRIPTION
Adds a reusable component  schema.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.LINK
https://swagger.io/docs/specification/serialization/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER Name
The reference Name of the schema.

.PARAMETER Component
The Component definition (the schema is created using the Property functions).

.PARAMETER Description
A description of the schema

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAComponentSchema -Name 'UserIdSchema' -Component (New-PodeOAIntProperty -Name 'userId' -Object)
#>
function Add-PodeOAComponentSchema {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Alias('Schema')]
        [hashtable]
        $Component,

        [string]
        $Description,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name] = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag)
        if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) {
            try {
                $modifiedComponent = ($Component | ConvertTo-PodeOASchemaProperty  -DefinitionTag $tag) | Resolve-PodeOAReference -DefinitionTag $tag
                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{
                    'available' = $true
                    'schema'    = $modifiedComponent
                    'json'      = $modifiedComponent | ConvertTo-Json -depth $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.depth
                }
            }
            catch {
                if ($_.ToString().StartsWith('Validation of schema with')) {
                    $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{
                        'available' = $false
                    }
                }
            }
        }

        if ($Description) {
            $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name].description = $Description
        }
    }

}


<#
.SYNOPSIS
Adds a reusable component for a Header schema.

.DESCRIPTION
Adds a reusable component for a Header schema.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.LINK
https://swagger.io/docs/specification/serialization/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER Name
The reference Name of the schema.

.PARAMETER Schema
The Schema definition (the schema is created using the Property functions).

.PARAMETER Description
A description of the header

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAComponentHeader -Name 'UserIdSchema' -Schema (New-PodeOAIntProperty -Name 'userId' -Object)
#>
function Add-PodeOAComponentHeader {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Schema,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        $param = [ordered]@{
            'schema' = ($Schema | ConvertTo-PodeOASchemaProperty -NoDescription -DefinitionTag $tag)
        }
        if ( $Description) {
            $param['description'] = $Description
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.headers[$Name] = $param
    }

}




<#
.SYNOPSIS
Adds a reusable component for a request body.

.DESCRIPTION
Adds a reusable component for a request body.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.LINK
https://swagger.io/docs/specification/describing-request-body/

.PARAMETER Name
The reference Name of the request body.

.PARAMETER Content
The content-types and schema the request body accepts (the schema is created using the Property functions).

.PARAMETER Description
A Description of the request body.

.PARAMETER Required
If supplied, the request body will be flagged as required.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }

.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
#>
function Add-PodeOAComponentRequestBody {
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Alias('ContentSchemas')]
        [hashtable]
        $Content,

        [Parameter()]
        [string]
        $Description  ,

        [Parameter()]
        [switch]
        $Required,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        $param = [ordered]@{ content = ($Content | ConvertTo-PodeOAObjectSchema -DefinitionTag $tag) }

        if ($Required.IsPresent) {
            $param['required'] = $Required.IsPresent
        }

        if ( $Description) {
            $param['description'] = $Description
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.requestBodies[$Name] = $param
    }

}

<#
.SYNOPSIS
Adds a reusable component for a request parameter.

.DESCRIPTION
Adds a reusable component for a request parameter.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.LINK
https://swagger.io/docs/specification/describing-parameters/

.PARAMETER Name
The reference Name of the parameter.

.PARAMETER Parameter
The Parameter to use for the component (from ConvertTo-PodeOAParameter)

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query | Add-PodeOAComponentParameter -Name 'UserIdParam'
#>
function Add-PodeOAComponentParameter {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Parameter,

        [string[]]
        $DefinitionTag
    )
 
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        if ([string]::IsNullOrWhiteSpace($Name)) {
            if ($Parameter.name) {
                $Name = $Parameter.name
            }
            else {
                throw 'The Parameter has no name. Please provide a name to this component using -Name property'
            }
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters[$Name] = $Parameter
    }

}

<#
.SYNOPSIS
Adds a reusable example component.

.DESCRIPTION
Adds a reusable example component.

.PARAMETER Name
The Name of the Example.


.PARAMETER Summary
Short description for the example

.PARAMETER Description
Long description for the example.

.PARAMETER Value
Embedded literal example. The  value Parameter and ExternalValue parameter are mutually exclusive.
To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary.

.PARAMETER ExternalValue
A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents.
The -Value parameter and -ExternalValue parameter are mutually exclusive.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.                           |

.EXAMPLE
Add-PodeOAComponentExample -name 'frog-example' -Summary "An example of a frog with a cat's name" -Value @{name = 'Jaguar'; petType = 'Panthera'; color = 'Lion'; gender = 'Male'; breed = 'Mantella Baroni' }

#>
function Add-PodeOAComponentExample {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param(

        [Parameter(Mandatory = $true)]
        [Alias('Title')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [string]
        $Summary,

        [Parameter()]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ParameterSetName = 'Value')]
        [object]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'ExternalValue')]
        [string]
        $ExternalValue,

        [string[]]
        $DefinitionTag
    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    foreach ($tag in $DefinitionTag) {
        $Example = [ordered]@{ }
        if ($Summary) {
            $Example.summary = $Summary
        }
        if ($Description) {
            $Example.description = $Description
        }
        if ($Value) {
            $Example.value = $Value
        }
        elseif ($ExternalValue) {
            $Example.externalValue = $ExternalValue
        }

        $PodeContext.Server.OpenAPI.Definitions[$tag].components.examples[$Name] = $Example
    }
}




<#
.SYNOPSIS
    Adds a reusable response link.

.DESCRIPTION
    The Add-PodeOAComponentResponseLink function is designed to add a new reusable response link

.PARAMETER Name
    Mandatory. A unique name for the response link.
    Must be a valid string composed of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Description
    A brief description of the response link. CommonMark syntax may be used for rich text representation.
    For more information on CommonMark syntax, see [CommonMark Specification](https://spec.commonmark.org/).

.PARAMETER OperationId
    The name of an existing, resolvable OpenAPI Specification (OAS) operation, as defined with a unique `operationId`.
    This parameter is mandatory when using the 'OperationId' parameter set and is mutually exclusive of the `OperationRef` field. It is used to specify the unique identifier of the operation the link is associated with.

.PARAMETER OperationRef
    A relative or absolute URI reference to an OAS operation.
    This parameter is mandatory when using the 'OperationRef' parameter set and is mutually exclusive of the `OperationId` field.
    It MUST point to an Operation Object. Relative `operationRef` values MAY be used to locate an existing Operation Object in the OpenAPI specification.

.PARAMETER Parameters
    A map representing parameters to pass to an operation as specified with `operationId` or identified via `operationRef`.
    The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation.
    Parameter names can be qualified using the parameter location syntax `[{in}.]{name}` for operations that use the same parameter name in different locations (e.g., path.id).

.PARAMETER RequestBody
    A string representing the request body to use as a request body when calling the target.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
    Add-PodeOAComponentResponseLink   -Name 'address' -OperationId 'getUserByName' -Parameters @{'username' = '$request.path.username'}
    Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User'} -Links 'address'
    This example demonstrates creating and adding a link named 'address' associated with the operation 'getUserByName' to an OrderedDictionary of links. The updated dictionary is then used in the 'Add-PodeOAResponse' function to define a response with a status code of 200.

.NOTES
    The function supports adding links either by specifying an 'OperationId' or an 'OperationRef', making it versatile for different OpenAPI specification needs.
    It's important to match the parameters and response structures as per the OpenAPI specification to ensure the correct functionality of the API documentation.
#>

function Add-PodeOAComponentResponseLink {
    [CmdletBinding(DefaultParameterSetName = 'OperationId')]
    param(

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ParameterSetName = 'OperationId')]
        [string]
        $OperationId,

        [Parameter(Mandatory = $true, ParameterSetName = 'OperationRef')]
        [string]
        $OperationRef,

        [Parameter()]
        [hashtable]
        $Parameters,

        [Parameter()]
        [string]
        $RequestBody,

        [string[]]
        $DefinitionTag

    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    foreach ($tag in $DefinitionTag) {
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.links[$Name] = New-PodeOAResponseLinkInternal -Params $PSBoundParameters
    }
}




<#
.SYNOPSIS
    Adds OpenAPI reusable callback configurations.

.DESCRIPTION
    The Add-PodeOACallBack function is used for defining OpenAPI callback configurations for routes in a Pode server.
    It enables setting up API specifications including detailed parameters, request body schemas, and response structures for various HTTP methods.

.PARAMETER Path
    Specifies the callback path, usually a relative URL.
    The key that identifies the Path Item Object is a runtime expression evaluated in the context of a runtime HTTP request/response to identify the URL for the callback request.
    A simple example is `$request.body#/url`.
    The runtime expression allows complete access to the HTTP message, including any part of a body that a JSON Pointer (RFC6901) can reference.
    More information on JSON Pointer can be found at [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901).

.PARAMETER Name
Alias for 'Name'. A unique identifier for the callback.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Method
    Defines the HTTP method for the callback (e.g., GET, POST, PUT). Supports standard HTTP methods and a wildcard (*) for all methods.

.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).

.PARAMETER RequestBody
Defines the schema of the request body. Can be set using New-PodeOARequestBody.

.PARAMETER Responses
Defines the possible responses for the callback. Can be set using New-PodeOAResponse.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAComponentCallBack -Title 'test' -Path '{$request.body#/id}' -Method Post `
    -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) `
    -Response (
        New-PodeOAResponse -StatusCode 200 -Description 'Successful operation'  -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet'  -Array)
        New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
        New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
        New-PodeOAResponse -Default -Description 'Something is wrong'
    )
Add-PodeOACallBack -Reference 'test'
    This example demonstrates adding a POST callback to handle a request body and define various responses based on different status codes.


.NOTES
Ensure that the provided parameters match the expected schema and formats of Pode and OpenAPI specifications.
The function is useful for dynamically configuring and documenting API callbacks in a Pode server environment.
#>

function Add-PodeOAComponentCallBack {
    param (

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [hashtable[]]
        $Parameters,

        [hashtable]
        $RequestBody,

        [hashtable]
        $Responses,

        [string[]]
        $DefinitionTag
    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    foreach ($tag in $DefinitionTag) {
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.callbacks.$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag
    }
}


<#
.SYNOPSIS
Sets metadate for the supplied route.

.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.

.LINK
https://swagger.io/docs/specification/paths-and-operations/

.PARAMETER Name
    Alias for 'Name'. A unique identifier for the route.
    It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Path
The URI path for the Route.

.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.

.PARAMETER Servers
A list of external endpoint. created with New-PodeOAServerEndpoint

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAExternalRoute -PassThru -Method Get -Path '/peta/:id' -Servers (
    New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
    New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
    ) |
        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   -ComponentSchema 'Pet' -array }) -PassThru |
        Add-PodeOAResponse -Default  -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
.EXAMPLE
    Add-PodeOAComponentPathItem -PassThru -Method Get -Path '/peta/:id'  -ScriptBlock {
            Write-PodeJsonResponse -Value 'done' -StatusCode 200
        } | Add-PodeOAExternalRoute -PassThru   -Servers (
        New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
        New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
        ) |
        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   -ComponentSchema 'Pet' -array }) -PassThru |
        Add-PodeOAResponse -Default  -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAComponentPathItem {
    param(

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true )]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [switch]
        $PassThru,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    $refRoute = @{
        Method      = $Method.ToLower()
        NotPrepared = $true
        OpenApi     = @{
            Responses      = $null
            Parameters     = $null
            RequestBody    = $null
            callbacks      = [ordered]@{}
            Authentication = @()
        }
    }
    foreach ($tag in $DefinitionTag) {
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag  ) {
            throw 'The feature reusable component pathItems is not available in OpenAPI v3.0.x'
        }
        #add the default OpenApi responses
        if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) {
            $refRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].components.pathItems[$Name] = $refRoute
    }

    if ($PassThru) {
        return $refRoute
    }
}





<#
.SYNOPSIS
Check the OpenAPI version

.DESCRIPTION
Check the OpenAPI version for a specific OpenAPI Definition


.PARAMETER Version
The version number to compare

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Test-PodeOAVersion -Version 3.1 -DefinitionTag 'default'
#>

function Test-PodeOAVersion {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet( 3.1 , 3.0 )]
        [decimal]
        $Version,

        [Parameter(Mandatory = $true)]
        [string[] ]
        $DefinitionTag
    )

    return $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version -eq $Version
}

<#
.SYNOPSIS
Check the OpenAPI component exist

.DESCRIPTION
Check the OpenAPI component exist

.PARAMETER Field
The component type

.PARAMETER Name
The component Name

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Test-PodeOAComponent -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'
#>

function Test-PodeOAComponent {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems' )]
        [string]
        $Field,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field].keys -ccontains $Name)) {
            return $false
        }
    }
    if (!$ThrowException.IsPresent) {
        return $true
    }
}

<#
.SYNOPSIS
Remove an OpenAPI component if exist

.DESCRIPTION
Remove an OpenAPI component if exist

.PARAMETER Field
The component type

.PARAMETER Name
The component Name

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Remove-PodeOAComponent -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'
#>
function Remove-PodeOAComponent {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems'  )]
        [string]
        $Field,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [string[]]
        $DefinitionTag
    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    foreach ($tag in $DefinitionTag) {
        if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field ].keys -ccontains $Name)) {
            $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
}

src\Public\OAProperties.ps1

<#
.SYNOPSIS
Creates a new OpenAPI New-PodeOAMultiTypeProperty property.

.DESCRIPTION
Creates a new OpenAPI multi type property, for Schemas or Parameters.
OpenAPI version 3.1 is required to use this cmdlet.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Type
The parameter types

.PARAMETER Format
The inbuilt OpenAPI Format  . (Default: Any)

.PARAMETER CustomFormat
The name of a custom OpenAPI Format  . (Default: None)
(String type only)

.PARAMETER Default
The default value of the property. (Default: $null)

.PARAMETER Pattern
A Regex pattern that the string must match.
(String type only)

.PARAMETER Description
A Description of the property.

.PARAMETER Minimum
The minimum value of the number.
(Integer,Number types only)

.PARAMETER Maximum
The maximum value of the number.
(Integer,Number types only)

.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.
(Integer,Number types only)

.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.
(Integer,Number types only)

.PARAMETER MultiplesOf
The number must be in multiples of the supplied value.
(Integer,Number types only)

.PARAMETER Properties
An array of other int/string/etc properties wrap up as an object.
(Object type only)

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Enum
An optional array of values that this property can only be set to.

.PARAMETER Required
If supplied, the string will be treated as Required where supported.

.PARAMETER Deprecated
If supplied, the string will be treated as Deprecated where supported.

.PARAMETER Object
If supplied, the string will be automatically wrapped in an object.

.PARAMETER Nullable
If supplied, the string will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the string will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the string will be included in a request but not in a response

.PARAMETER MinLength
If supplied, the string will be restricted to minimal length of characters.

.PARAMETER  MaxLength
If supplied, the string will be restricted to maximal length of characters.

.PARAMETER NoProperties
If supplied, no properties are allowed in the object.
If no properties are assigned to the object and the NoProperties parameter is not set the object accept any property.(Object type only)

.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an object.
(Object type only)

.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an object.
(Object type only)

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.

.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOAMultiTypeProperty -Name 'userType' -type integer,boolean

.EXAMPLE
New-PodeOAMultiTypeProperty -Name 'password' -type string,object -Format Password -Properties (New-PodeOAStringProperty -Name 'password' -Format Password)
#>
function New-PodeOAMultiTypeProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $ParamsList,

        [Parameter(Mandatory)]
        [ValidateSet( 'integer', 'number', 'string', 'object', 'boolean' )]
        [string]
        $Type,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter( ParameterSetName = 'Array')]
        [Parameter(ParameterSetName = 'Inbuilt')]
        [ValidateSet('', 'Int32', 'Int64', 'Double', 'Float', 'Binary', 'Base64', 'Byte', 'Date', 'Date-Time', 'Password', 'Email', 'Uuid', 'Uri', 'Hostname', 'Ipv4', 'Ipv6')]
        [string]
        $Format,

        [Parameter( ParameterSetName = 'Array')]
        [Parameter(ParameterSetName = 'Custom')]
        [string]
        $CustomFormat,

        [Parameter()]
        $Default,

        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [hashtable[]]
        $Properties,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [double]
        $Minimum,

        [Parameter()]
        [double]
        $Maximum,

        [Parameter()]
        [switch]
        $ExclusiveMaximum,

        [Parameter()]
        [switch]
        $ExclusiveMinimum,

        [Parameter()]
        [double]
        $MultiplesOf,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [Parameter()]
        [object[]]
        $Enum,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Object,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [Parameter()]
        [int]
        $MinLength,

        [Parameter()]
        [int]
        $MaxLength,

        [switch]
        $NoProperties,

        [int]
        $MinProperties,

        [int]
        $MaxProperties,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems,

        [string]
        $DiscriminatorProperty,

        [hashtable]
        $DiscriminatorMapping
    )
    begin {
        $param = New-PodeOAPropertyInternal   -Params $PSBoundParameters

        if ($type -contains 'string') {
            if (![string]::IsNullOrWhiteSpace($CustomFormat)) {
                $_format = $CustomFormat
            }
            elseif ($Format) {
                $_format = $Format
            }


            if ($Format -or $CustomFormat) {
                $param.format = $_format.ToLowerInvariant()
            }
        }
        if ($type -contains 'object') {
            if ($NoProperties) {
                if ($Properties -or $MinProperties -or $MaxProperties) {
                    throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties'
                }
                $param.properties = @($null)
            }
            elseif ($Properties) {
                $param.properties = $Properties
            }
            else {
                $param.properties = @()
            }
            if ($DiscriminatorProperty) {
                $param.discriminator = @{
                    'propertyName' = $DiscriminatorProperty
                }
                if ($DiscriminatorMapping) {
                    $param.discriminator.mapping = $DiscriminatorMapping
                }
            }
            elseif ($DiscriminatorMapping) {
                throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
            }
        }
        if ($type -contains 'boolean') {
            if ($Default) {
                if ([bool]::TryParse($Default, [ref]$null) -or $Enum -icontains $Default) {
                    $param.default = $Default
                }
                else {
                    throw "The default value is not a boolean and it's not part of the enum"
                }
            }
        }
        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}
<#
.SYNOPSIS
Creates a new OpenAPI integer property.

.DESCRIPTION
Creates a new OpenAPI integer property, for Schemas or Parameters.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Format
The inbuilt OpenAPI Format of the integer. (Default: Any)

.PARAMETER Default
The default value of the property. (Default: 0)

.PARAMETER Minimum
The minimum value of the integer. (Default: Int.Min)

.PARAMETER Maximum
The maximum value of the integer. (Default: Int.Max)

.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.

.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.

.PARAMETER MultiplesOf
The integer must be in multiples of the supplied value.

.PARAMETER Description
A Description of the property.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Enum
An optional array of values that this property can only be set to.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.

.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.

.PARAMETER Object
If supplied, the integer will be automatically wrapped in an object.

.PARAMETER Nullable
If supplied, the integer will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the integer will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the integer will be included in a request but not in a response

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.


.EXAMPLE
New-PodeOAIntProperty -Name 'age' -Required
Creates a required integer property named 'age'.

.EXAMPLE
New-PodeOAIntProperty -Name 'count' -Minimum 0 -Maximum 10 -Default 5 -Description 'Item count'
Creates an integer property 'count' with a minimum value of 0, maximum of 10, default value of 5, and a description.

.EXAMPLE
New-PodeOAIntProperty -Name 'quantity' -XmlName 'Quantity' -XmlNamespace 'http://example.com/quantity' -XmlPrefix 'q'
Creates an integer property 'quantity' with a custom XML element name 'Quantity', using a specified namespace and prefix.

.EXAMPLE
New-PodeOAIntProperty -Array -XmlItemName 'unit' -XmlName 'units' | Add-PodeOAComponentSchema -Name 'Units'
Generates a schema where the integer property is treated as an array, with each array item named 'unit' in XML, and the array itself represented with the XML name 'units'.


#>
function New-PodeOAIntProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true)]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('', 'Int32', 'Int64')]
        [string]
        $Format,

        [Parameter()]
        [int]
        $Default,

        [Parameter()]
        [int]
        $Minimum,

        [Parameter()]
        [int]
        $Maximum,

        [Parameter()]
        [switch]
        $ExclusiveMaximum,

        [Parameter()]
        [switch]
        $ExclusiveMinimum,

        [Parameter()]
        [int]
        $MultiplesOf,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [Parameter()]
        [int[]]
        $Enum,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Object,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems
    )
    begin {
        $param = New-PodeOAPropertyInternal -type 'integer' -Params $PSBoundParameters

        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}

<#
.SYNOPSIS
Creates a new OpenAPI number property.

.DESCRIPTION
Creates a new OpenAPI number property, for Schemas or Parameters.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Format
The inbuilt OpenAPI Format of the number. (Default: Any)

.PARAMETER Default
The default value of the property. (Default: 0)

.PARAMETER Minimum
The minimum value of the number. (Default: Double.Min)

.PARAMETER Maximum
The maximum value of the number. (Default: Double.Max)

.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.

.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.

.PARAMETER MultiplesOf
The number must be in multiples of the supplied value.

.PARAMETER Description
A Description of the property.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Enum
An optional array of values that this property can only be set to.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.

.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.

.PARAMETER Object
If supplied, the number will be automatically wrapped in an object.

.PARAMETER Nullable
If supplied, the number will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the number will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the number will be included in a request but not in a response

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOANumberProperty -Name 'gravity' -Default 9.8
#>
function New-PodeOANumberProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('', 'Double', 'Float')]
        [string]
        $Format,

        [Parameter()]
        [double]
        $Default,

        [Parameter()]
        [double]
        $Minimum,

        [Parameter()]
        [double]
        $Maximum,

        [Parameter()]
        [switch]
        $ExclusiveMaximum,

        [Parameter()]
        [switch]
        $ExclusiveMinimum,

        [Parameter()]
        [double]
        $MultiplesOf,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [Parameter()]
        [double[]]
        $Enum,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Object,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems
    )
    begin {
        $param = New-PodeOAPropertyInternal -type 'number' -Params $PSBoundParameters

        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}

<#
.SYNOPSIS
Creates a new OpenAPI string property.

.DESCRIPTION
Creates a new OpenAPI string property, for Schemas or Parameters.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Format
The inbuilt OpenAPI Format of the string. (Default: Any)

.PARAMETER CustomFormat
The name of a custom OpenAPI Format of the string. (Default: None)

.PARAMETER Default
The default value of the property. (Default: $null)

.PARAMETER Pattern
A Regex pattern that the string must match.

.PARAMETER Description
A Description of the property.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Enum
An optional array of values that this property can only be set to.

.PARAMETER Required
If supplied, the string will be treated as Required where supported.

.PARAMETER Deprecated
If supplied, the string will be treated as Deprecated where supported.

.PARAMETER Object
If supplied, the string will be automatically wrapped in an object.

.PARAMETER Nullable
If supplied, the string will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the string will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the string will be included in a request but not in a response

.PARAMETER MinLength
If supplied, the string will be restricted to minimal length of characters.

.PARAMETER  MaxLength
If supplied, the string will be restricted to maximal length of characters.

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOAStringProperty -Name 'userType' -Default 'admin'

.EXAMPLE
New-PodeOAStringProperty -Name 'password' -Format Password
#>
function New-PodeOAStringProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter( ParameterSetName = 'Array')]
        [Parameter(ParameterSetName = 'Inbuilt')]
        [ValidateSet('', 'Binary', 'Base64', 'Byte', 'Date', 'Date-Time', 'Password', 'Email', 'Uuid', 'Uri', 'Hostname', 'Ipv4', 'Ipv6')]
        [string]
        $Format,

        [Parameter( ParameterSetName = 'Array')]
        [Parameter(ParameterSetName = 'Custom')]
        [string]
        $CustomFormat,

        [Parameter()]
        [string]
        $Default,

        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [Parameter()]
        [string[]]
        $Enum,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Object,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [Parameter()]
        [int]
        $MinLength,

        [Parameter()]
        [int]
        $MaxLength,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems
    )
    begin {
        if (![string]::IsNullOrWhiteSpace($CustomFormat)) {
            $_format = $CustomFormat
        }
        elseif ($Format) {
            $_format = $Format
        }
        $param = New-PodeOAPropertyInternal -type 'string' -Params $PSBoundParameters

        if ($Format -or $CustomFormat) {
            $param.format = $_format.ToLowerInvariant()
        }

        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}

<#
.SYNOPSIS
Creates a new OpenAPI boolean property.

.DESCRIPTION
Creates a new OpenAPI boolean property, for Schemas or Parameters.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Default
The default value of the property. (Default: $false)

.PARAMETER Description
A Description of the property.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Enum
An optional array of values that this property can only be set to.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.

.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.

.PARAMETER Object
If supplied, the boolean will be automatically wrapped in an object.

.PARAMETER Nullable
If supplied, the boolean will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the boolean will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the boolean will be included in a request but not in a response

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOABoolProperty -Name 'enabled' -Required
#>
function New-PodeOABoolProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(

        [Parameter(ValueFromPipeline = $true, DontShow = $true)]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Default,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [Parameter()]
        [string[]]
        $Enum,

        [switch]
        $Required,

        [switch]
        $Deprecated,

        [switch]
        $Object,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems
    )
    begin {
        $param = New-PodeOAPropertyInternal -type 'boolean' -Params $PSBoundParameters

        if ($Default) {
            if ([bool]::TryParse($Default, [ref]$null) -or $Enum -icontains $Default) {
                $param.default = $Default
            }
            else {
                throw "The default value is not a boolean and it's not part of the enum"
            }
        }

        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}

<#
.SYNOPSIS
Creates a new OpenAPI object property from other properties.

.DESCRIPTION
Creates a new OpenAPI object property from other properties, for Schemas or Parameters.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Properties
An array of other int/string/etc properties wrap up as an object.

.PARAMETER Description
A Description of the property.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER Example
An example of a parameter value

.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER Nullable
If supplied, the object will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the object will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the object will be included in a request but not in a response

.PARAMETER NoProperties
If supplied, no properties are allowed in the object. If no properties are assigned to the object and the NoProperties parameter is not set the object accept any property

.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an object.

.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an object.

.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.

.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.

.PARAMETER Array
If supplied, the object will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.

.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOAObjectProperty -Name 'user' -Properties @('<ARRAY_OF_PROPERTIES>')

.EXAMPLE
New-PodeOABoolProperty -Name 'enabled' -Required|
    New-PodeOAObjectProperty  -Name 'extraProperties'  -AdditionalProperties [ordered]@{
        "property1" = @{ "type" = "string"; "description" = "Description for property1" };
        "property2" = @{ "type" = "integer"; "format" = "int32" }
}
#>
function New-PodeOAObjectProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(

        [Parameter(ValueFromPipeline = $true, DontShow = $true , Position = 0 )]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [Alias('Title')]
        [string]
        $Name,

        [Parameter()]
        [hashtable[]]
        $Properties,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [string]
        $ExternalDoc,

        [Parameter()]
        [object]
        $Example,

        [switch]
        $Deprecated,

        [switch]
        $Required,

        [switch]
        $Nullable,

        [switch]
        $ReadOnly,

        [switch]
        $WriteOnly,

        [switch]
        $NoProperties,

        [int]
        $MinProperties,

        [int]
        $MaxProperties,

        [switch]
        $NoAdditionalProperties,

        [hashtable]
        $AdditionalProperties,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(  Mandatory, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems,

        [string]
        $DiscriminatorProperty,

        [hashtable]
        $DiscriminatorMapping
    )
    begin {
        $param = New-PodeOAPropertyInternal -type 'object' -Params $PSBoundParameters
        if ($NoProperties) {
            if ($Properties -or $MinProperties -or $MaxProperties) {
                throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties'
            }
            $param.properties = @($null)
            $PropertiesFromPipeline = $false
        }
        elseif ($Properties) {
            $param.properties = $Properties
            $PropertiesFromPipeline = $false
        }
        else {
            $param.properties = @()
            $PropertiesFromPipeline = $true
        }
        if ($DiscriminatorProperty) {
            $param.discriminator = @{
                'propertyName' = $DiscriminatorProperty
            }
            if ($DiscriminatorMapping) {
                $param.discriminator.mapping = $DiscriminatorMapping
            }
        }
        elseif ($DiscriminatorMapping) {
            throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
        }
        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            if ($PropertiesFromPipeline) {
                $param.properties += $ParamsList

            }
            else {
                $collectedInput.AddRange($ParamsList)
            }
        }
    }

    end {
        if ($PropertiesFromPipeline) {
            return $param
        }
        elseif ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}


<#
.SYNOPSIS
Creates a new OpenAPI object combining schemas and properties.

.DESCRIPTION
Creates a new OpenAPI object combining schemas and properties.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline an object definition

.PARAMETER Type
Define the type of validation between the objects
oneOf – validates the value against exactly one of the subschemas
allOf – validates the value against all the subschemas
anyOf – validates the value against any (one or more) of the subschemas

.PARAMETER ObjectDefinitions
An array of object definitions that are used for independent validation but together compose a single object.

.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.

.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.

.EXAMPLE
Add-PodeOAComponentSchema -Name 'Pets' -Component (  Merge-PodeOAProperty  -Type OneOf -ObjectDefinitions @( 'Cat','Dog') -Discriminator "petType")


.EXAMPLE
Add-PodeOAComponentSchema -Name 'Cat' -Component (
        Merge-PodeOAProperty  -Type AllOf -ObjectDefinitions @( 'Pet', ( New-PodeOAObjectProperty -Properties @(
                (New-PodeOAStringProperty -Name 'huntingSkill' -Description 'The measured skill for hunting' -Enum @(  'clueless', 'lazy', 'adventurous', 'aggressive'))
                ))
        ))
#>
function Merge-PodeOAProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(

        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $ParamsList,

        [Parameter(Mandatory)]
        [ValidateSet('OneOf', 'AnyOf', 'AllOf')]
        [string]
        $Type,

        [Parameter()]
        [System.Object[]]
        $ObjectDefinitions,

        [string]
        $DiscriminatorProperty,

        [hashtable]
        $DiscriminatorMapping
    )
    begin {

        $param = [ordered]@{}
        switch ($type.ToLower()) {
            'oneof' {
                $param.type = 'oneOf'
            }
            'anyof' {
                $param.type = 'anyOf'
            }
            'allof' {
                $param.type = 'allOf'
            }
        }

        $param.schemas = @()
        if ($ObjectDefinitions) {
            foreach ($schema in $ObjectDefinitions) {
                if ($schema -is [System.Object[]] -or ($schema -is [hashtable] -and
                (($schema.type -ine 'object') -and !$schema.object))) {
                    throw "Only properties of type Object can be associated with $($param.type)"
                }
                $param.schemas += $schema
            }
        }
        if ($DiscriminatorProperty) {
            if ($type.ToLower() -eq 'allof' ) {
                throw 'Discriminator parameter is not compatible with allOf'
            }
            $param.discriminator = @{
                'propertyName' = $DiscriminatorProperty
            }
            if ($DiscriminatorMapping) {
                $param.discriminator.mapping = $DiscriminatorMapping
            }
        }
        elseif ($DiscriminatorMapping) {
            throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
        }

    }
    process {
        if ($ParamsList) {
            if ($ParamsList.type -ine 'object' -and !$ParamsList.object) {
                throw "Only properties of type Object can be associated with $type"
            }
            $param.schemas += $ParamsList
        }
    }

    end {
        return $param
    }
}


<#
.SYNOPSIS
Creates a OpenAPI schema reference property.

.DESCRIPTION
Creates a new OpenAPI component schema reference from another OpenAPI schema.

.LINK
https://swagger.io/docs/specification/basic-structure/

.LINK
https://swagger.io/docs/specification/data-models/

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER Name
The Name of the property.

.PARAMETER Reference
An component schema name.

.PARAMETER Description
A Description of the property.

.PARAMETER Example
An example of a parameter value

.PARAMETER Deprecated
If supplied, the schema will be treated as Deprecated where supported.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.

.PARAMETER Array
If supplied, the schema will be treated as an array of objects.

.PARAMETER Nullable
If supplied, the schema will be treated as Nullable.

.PARAMETER ReadOnly
If supplied, the schema will be included in a response but not in a request

.PARAMETER WriteOnly
If supplied, the schema will be included in a request but not in a response

.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an schema.

.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an schema.

.PARAMETER Array
If supplied, the schema will be treated as an array of objects.

.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique

.PARAMETER MinItems
If supplied, specify minimum length of an array

.PARAMETER MaxItems
If supplied, specify maximum length of an array

.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.

.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.

.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.

.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.

.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.

.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.

.EXAMPLE
New-PodeOAComponentSchemaProperty -Name 'Config' -Component "MyConfigSchema"
#>
function New-PodeOAComponentSchemaProperty {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    param(

        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $ParamsList,

        [Parameter()]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Reference,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $Description,

        [string]
        $XmlName,

        [string]
        $XmlNamespace,

        [string]
        $XmlPrefix,

        [switch]
        $XmlAttribute,

        [Parameter(  ParameterSetName = 'Array')]
        [string]
        $XmlItemName,

        [Parameter(  ParameterSetName = 'Array')]
        [switch]
        $XmlWrapped,

        [Parameter(ParameterSetName = 'Array')]
        [object]
        $Example,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $Deprecated,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $Required,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $Nullable,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $ReadOnly,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $WriteOnly,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinProperties,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxProperties,

        [Parameter(Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems
    )
    begin {
        $param = New-PodeOAPropertyInternal -type 'schema' -Params $PSBoundParameters
        if (! $param.Name) {
            $param.Name = $Reference
        }
        $param.schema = $Reference
        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ParamsList) {
            $collectedInput.AddRange($ParamsList)
        }
    }
    end {
        if ($collectedInput) {
            return $collectedInput + $param
        }
        else {
            return $param
        }
    }
}


if (!(Test-Path Alias:New-PodeOASchemaProperty)) {
    New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty
}
src\Public\OpenApi.ps1
<#
.SYNOPSIS
Enables the OpenAPI default route in Pode.

.DESCRIPTION
Enables the OpenAPI default route in Pode, as well as setting up details like Title and API Version.

.PARAMETER Path
An optional custom route path to access the OpenAPI definition. (Default: /openapi)

.PARAMETER Title
The Title of the API. (Deprecated -  Use Add-PodeOAInfo)

.PARAMETER Version
The Version of the API.   (Deprecated -  Use Add-PodeOAInfo)
The OpenAPI Specification is versioned using Semantic Versioning 2.0.0 (semver) and follows the semver specification.
https://semver.org/spec/v2.0.0.html

.PARAMETER Description
A short description of the API. (Deprecated -  Use Add-PodeOAInfo)
CommonMark syntax MAY be used for rich text representation.
https://spec.commonmark.org/

.PARAMETER OpenApiVersion
Specify OpenApi Version (default: 3.0.3)

.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to the route.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.

.PARAMETER ServerEndpoint
If supplied, will be used as URL base to generate the OpenAPI definition.
The parameter is created by New-PodeOpenApiServerEndpoint

.PARAMETER Mode
Define the way the OpenAPI definition file is accessed, the value can be View or Download. (Default: View)

.PARAMETER NoCompress
If supplied, generate the OpenApi Json version in human readible form.

.PARAMETER MarkupLanguage
Define the default markup language for the OpenApi spec ('Json', 'Json-Compress', 'Yaml')

.PARAMETER EnableSchemaValidation
If supplied enable Test-PodeOAJsonSchemaCompliance  cmdlet that provide support for opeapi parameter schema validation

.PARAMETER Depth
Define the default  depth used by any JSON,YAML OpenAPI conversion (default 20)

.PARAMETER DisableMinimalDefinitions
If supplied the OpenApi decument will include only the route validated by Set-PodeOARouteInfo. Any other not OpenApi route will be excluded.

.PARAMETER NoDefaultResponses
If supplied, it will disable the default OpenAPI response with the new provided.

.PARAMETER DefaultResponses
If supplied, it will replace the default OpenAPI response with the new provided.(Default: @{'200' = @{ description = 'OK' };'default' = @{ description = 'Internal server error' }} )

.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*'

.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*' -RestrictRoutes

.EXAMPLE
Enable-PodeOpenApi -Path '/docs/openapi'   -NoCompress -Mode 'Donwload' -DisableMinimalDefinitions
#>
function Enable-PodeOpenApi {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string]
        $Path = '/openapi',

        [Parameter(ParameterSetName = 'Deprecated')]
        [string]
        $Title,

        [Parameter(ParameterSetName = 'Deprecated')]
        [ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'Deprecated')]
        [string]
        $Description,

        [ValidateSet('3.1.0', '3.0.3', '3.0.2', '3.0.1', '3.0.0')]
        [string]
        $OpenApiVersion = '3.0.3',

        [ValidateNotNullOrEmpty()]
        [string]
        $RouteFilter = '/*',

        [string[]]
        $EndpointName,

        [object[]]
        $Middleware,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [switch]
        $RestrictRoutes,

        [ValidateSet('View', 'Download')]
        [String]
        $Mode = 'view',

        [ValidateSet('Json', 'Json-Compress', 'Yaml')]
        [String]
        $MarkupLanguage = 'Json',

        [switch]
        $EnableSchemaValidation,

        [ValidateRange(1, 100)]
        [int]
        $Depth = 20,

        [switch]
        $DisableMinimalDefinitions,

        [Parameter(Mandatory, ParameterSetName = 'DefaultResponses')]
        [hashtable]
        $DefaultResponses,

        [Parameter(Mandatory, ParameterSetName = 'NoDefaultResponses')]
        [switch]
        $NoDefaultResponses,

        [string]
        $DefinitionTag

    )
    if (Test-PodeIsEmpty -Value $DefinitionTag) {
        $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
    }
    if ($Description -or $Version -or $Title) {
        if (! $Version) {
            $Version = '0.0.0'
        }
        Write-PodeHost -ForegroundColor Yellow "WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead."
    }
    if ( $DefinitionTag -ine $PodeContext.Server.OpenAPI.DefaultDefinitionTag ) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag] = Get-PodeOABaseObject
    }
    $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enableMinimalDefinitions = !$DisableMinimalDefinitions.IsPresent


    # initialise openapi info
    $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Version = $OpenApiVersion
    $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Path = $Path
    if ($OpenApiVersion.StartsWith('3.0')) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version = 3.0
    }
    elseif ($OpenApiVersion.StartsWith('3.1')) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version = 3.1
    }

    $meta = @{
        RouteFilter    = $RouteFilter
        RestrictRoutes = $RestrictRoutes
        NoCompress     = ($MarkupLanguage -ine 'Json-Compress')
        Mode           = $Mode
        MarkupLanguage = $MarkupLanguage
        DefinitionTag  = $DefinitionTag
    }
    if ( $Title) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title = $Title
    }
    if ($Version) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version = $Version
    }

    if ($Description ) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description = $Description
    }

    if ( $EnableSchemaValidation.IsPresent) {
        #Test-Json has been introduced with version 6.1.0
        if ($PSVersionTable.PSVersion -ge [version]'6.1.0') {
            $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation = $EnableSchemaValidation.IsPresent
        }
        else {
            throw 'Schema validation required Powershell version 6.1.0 or greater'
        }
    }

    if ( $Depth) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth = $Depth
    }


    $openApiCreationScriptBlock = {
        param($meta)
        $format = $WebEvent.Query['format']
        $mode = $WebEvent.Query['mode']
        $DefinitionTag = $meta.DefinitionTag

        if (!$mode) {
            $mode = $meta.Mode
        }
        elseif (@('download', 'view') -inotcontains $mode) {
            Write-PodeHtmlResponse -Value "Mode $mode not valid" -StatusCode 400
            return
        }
        if ($WebEvent.path -ilike '*.json') {
            if ($format) {
                Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description 'Format query not valid when the file extension is used'
                return
            }
            $format = 'json'
        }
        elseif ($WebEvent.path -ilike '*.yaml') {
            if ($format) {
                Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description 'Format query not valid when the file extension is used'
                return
            }
            $format = 'yaml'
        }
        elseif (!$format) {
            $format = $meta.MarkupLanguage.ToLower()
        }
        elseif (@('yaml', 'json', 'json-Compress') -inotcontains $format) {
            Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description "Format $format not valid"
            return
        }

        if (($mode -ieq 'download')  ) {
            # Set-PodeResponseAttachment -Path
            Add-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=openapi.$format"
        }

        # generate the openapi definition
        $def = Get-PodeOpenApiDefinitionInternal `
            -EndpointName $WebEvent.Endpoint.Name `
            -DefinitionTag $DefinitionTag `
            -MetaInfo $meta

        # write the openapi definition
        if ($format -ieq 'yaml') {
            if ($mode -ieq 'view') {
                Write-PodeTextResponse -Value (ConvertTo-PodeYaml -InputObject $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth) -ContentType 'text/x-yaml; charset=utf-8'
            }
            else {
                Write-PodeYamlResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
            }
        }
        else {
            Write-PodeJsonResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -NoCompress:$meta.NoCompress
        }
    }

    # add the OpenAPI route
    Add-PodeRoute -Method Get -Path $Path -ArgumentList $meta -Middleware $Middleware `
        -ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
        -Authentication $Authentication -Role $Role -Scope $Scope -Group $Group

    Add-PodeRoute -Method Get -Path "$Path.json" -ArgumentList $meta -Middleware $Middleware `
        -ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
        -Authentication $Authentication -Role $Role -Scope $Scope -Group $Group

    Add-PodeRoute -Method Get -Path "$Path.yaml" -ArgumentList $meta -Middleware $Middleware `
        -ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
        -Authentication $Authentication -Role $Role -Scope $Scope -Group $Group

    #set new DefaultResponses
    if ($NoDefaultResponses.IsPresent) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = @{}
    }
    elseif ($DefaultResponses) {
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = $DefaultResponses
    }
    $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enabled = $true
}




<#
.SYNOPSIS
Creates an OpenAPI Server Object.

.DESCRIPTION
Creates an OpenAPI Server Object.

.PARAMETER Url
A URL to the target host.  This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served.
Variable substitutions will be made when a variable is named in `{`brackets`}`.

.PARAMETER Description
An optional string describing the host designated by the URL. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation.

.PARAMETER Variables
A map between a variable name and its value.  The value is used for substitution in the server's URL template.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test server'

.EXAMPLE
Add-PodeOAServerEndpoint -Url '/api' -Description 'My local server'

.EXAMPLE
Add-PodeOAServerEndpoint -Url "https://{username}.gigantic-server.com:{port}/{basePath}" -Description "The production API server" `
    -Variable   @{
        username = @{
            default = 'demo'
            description = 'this value is assigned by the service provider, in this example gigantic-server.com'
        }
        port = @{
            enum = @('System.Object[]')  # Assuming 'System.Object[]' is a placeholder for actual values
            default = 8443
        }
        basePath = @{
            default = 'v2'
        }
    }
}
#>
function Add-PodeOAServerEndpoint {
    param (
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^(https?://|/).+')]
        [string]
        $Url,

        [string]
        $Description,

        [System.Collections.Specialized.OrderedDictionary]
        $Variables,

        [string[]]
        $DefinitionTag
    )

    if (Test-PodeIsEmpty -Value $DefinitionTag) {
        $DefinitionTag = @($PodeContext.Server.OpenAPI.SelectedDefinitionTag)
    }
    foreach ($tag in $DefinitionTag) {
        if (! $PodeContext.Server.OpenAPI.Definitions[$tag].servers) {
            $PodeContext.Server.OpenAPI.Definitions[$tag].servers = @()
        }
        $lUrl = [ordered]@{url = $Url }
        if ($Description) {
            $lUrl.description = $Description
        }

        if ($Variables) {
            $lUrl.variables = $Variables
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].servers += $lUrl
    }
}




<#
.SYNOPSIS
Gets the OpenAPI definition.

.DESCRIPTION
Gets the OpenAPI definition for custom use in routes, or other functions.

.PARAMETER Format
Return the definition  in a specific format 'Json', 'Json-Compress', 'Yaml', 'HashTable'

.PARAMETER Title
The Title of the API. (Default: the title supplied in Enable-PodeOpenApi)

.PARAMETER Version
The Version of the API. (Default: the version supplied in Enable-PodeOpenApi)

.PARAMETER Description
A Description of the API. (Default: the description supplied into Enable-PodeOpenApi)

.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)

.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.

.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
$defInJson = Get-PodeOADefinition -Json
#>
function Get-PodeOADefinition {
    [CmdletBinding()]
    param(
        [ValidateSet('Json', 'Json-Compress', 'Yaml', 'HashTable')]
        [string]
        $Format = 'HashTable',

        [string]
        $Title,

        [string]
        $Version,

        [string]
        $Description,

        [ValidateNotNullOrEmpty()]
        [string]
        $RouteFilter = '/*',

        [switch]
        $RestrictRoutes,

        [string]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    $meta = @{
        RouteFilter    = $RouteFilter
        RestrictRoutes = $RestrictRoutes
    }
    if ($RestrictRoutes) {
        $meta = @{
            RouteFilter    = $RouteFilter
            RestrictRoutes = $RestrictRoutes
        }
    }
    else {
        $meta = @{}
    }
    if ($Title) {
        $meta.Title = $Title
    }
    if ($Version) {
        $meta.Version = $Version
    }
    if ($Description) {
        $meta.Description = $Description
    }

    $oApi = Get-PodeOpenApiDefinitionInternal  -MetaInfo $meta -EndpointName $WebEvent.Endpoint.Name -DefinitionTag $DefinitionTag

    switch ($Format.ToLower()) {
        'json' {
            return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
        }
        'json-compress' {
            return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -Compress
        }
        'yaml' {
            return ConvertTo-PodeYaml -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
        }
        Default {
            return $oApi
        }
    }
}

<#
.SYNOPSIS
Adds a response definition to the supplied route.

.DESCRIPTION
Adds a response definition to the supplied route.

.PARAMETER Route
The route to add the response definition, usually from -PassThru on Add-PodeRoute.

.PARAMETER StatusCode
The HTTP StatusCode for the response.To define a range of response codes, this field MAY contain the uppercase wildcard character `X`.
For example, `2XX` represents all response codes between `[200-299]`. Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`.
If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.

.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).
Alias: ContentSchemas

.PARAMETER Headers
The header name and schema the response returns (the schema is created using Add-PodeOAComponentHeader cmd-let).
Alias: HeaderSchemas

.PARAMETER Description
A Description of the response. (Default: the HTTP StatusCode description)

.PARAMETER Reference
A Reference Name of an existing component response to use.

.PARAMETER Links
A Response link definition

.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.


.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }

.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = 'UserIdSchema' }

.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'OKResponse'
#>
function Add-PodeOAResponse {
    [CmdletBinding(DefaultParameterSetName = 'Schema')]
    [OutputType([hashtable[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [ValidatePattern('^([1-5][0-9][0-9]|[1-5]XX)$')]
        [string]
        $StatusCode,

        [Parameter(ParameterSetName = 'Schema')]
        [Parameter(ParameterSetName = 'SchemaDefault')]
        [Alias('ContentSchemas')]
        [hashtable]
        $Content,

        [Alias('HeaderSchemas')]
        [AllowEmptyString()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] -or $_ -is [ordered] })]
        $Headers,

        [Parameter(Mandatory = $false, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $false, ParameterSetName = 'SchemaDefault')]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [Parameter(ParameterSetName = 'ReferenceDefault')]
        [string]
        $Reference,

        [Parameter(Mandatory = $true, ParameterSetName = 'ReferenceDefault')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
        [switch]
        $Default,

        [Parameter(ParameterSetName = 'Schema')]
        [Parameter(ParameterSetName = 'SchemaDefault')]
        [System.Collections.Specialized.OrderedDictionary ]
        $Links,

        [switch]
        $PassThru,

        [string[]]
        $DefinitionTag
    )

    if ($null -eq $Route) { throw 'Add-PodeOAResponse - The parameter -Route cannot be NULL.' }

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    # override status code with default
    if ($Default) {
        $code = 'default'
    }
    else {
        $code = "$($StatusCode)"
    }

    # add the respones to the routes
    foreach ($r in @($Route)) {
        foreach ($tag in $DefinitionTag) {
            if (! $r.OpenApi.Responses.$tag) {
                $r.OpenApi.Responses.$tag = @{}
            }
            $r.OpenApi.Responses.$tag[$code] = New-PodeOResponseInternal  -DefinitionTag $tag -Params $PSBoundParameters
        }
    }

    if ($PassThru) {
        return $Route
    }

}


<#
.SYNOPSIS
Remove a response definition from the supplied route.

.DESCRIPTION
Remove a response definition from the supplied route.

.PARAMETER Route
The route to remove the response definition, usually from -PassThru on Add-PodeRoute.

.PARAMETER StatusCode
The HTTP StatusCode for the response to remove.

.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.EXAMPLE
Add-PodeRoute -PassThru | Remove-PodeOAResponse -StatusCode 200

.EXAMPLE
Add-PodeRoute -PassThru | Remove-PodeOAResponse -StatusCode 201 -Default
#>
function Remove-PodeOAResponse {
    [CmdletBinding()]
    [OutputType([hashtable[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory = $true)]
        [int]
        $StatusCode,

        [switch]
        $Default,

        [switch]
        $PassThru
    )

    if ($null -eq $Route) { throw 'The parameter -Route cannot be NULL.' }

    # override status code with default
    $code = "$($StatusCode)"
    if ($Default) {
        $code = 'default'
    }
    # remove the respones from the routes
    foreach ($r in @($Route)) {
        if ($r.OpenApi.Responses.ContainsKey($code)) {
            $null = $r.OpenApi.Responses.Remove($code)
        }
    }

    if ($PassThru) {
        return $Route
    }

}

<#
.SYNOPSIS
Sets the definition of a request for a route.

.DESCRIPTION
Sets the definition of a request for a route.

.PARAMETER Route
The route to set a request definition, usually from -PassThru on Add-PodeRoute.

.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).

.PARAMETER RequestBody
The Request Body definition the request uses (from New-PodeOARequestBody).

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody')
#>
function Set-PodeOARequest {
    [CmdletBinding()]
    [OutputType([hashtable[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [hashtable[]]
        $Parameters,

        [hashtable]
        $RequestBody,

        [switch]
        $PassThru
    )

    if ($null -eq $Route) { throw 'Set-PodeOARequest - The parameter -Route cannot be NULL.' }

    foreach ($r in @($Route)) {

        if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) {
            $r.OpenApi.Parameters = @($Parameters)
        }

        if ($null -ne $RequestBody) {
            $r.OpenApi.RequestBody = $RequestBody
        }

    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Creates a Request Body definition for routes.

.DESCRIPTION
Creates a Request Body definition for routes from the supplied content-types and schemas.

.PARAMETER Reference
A reference name from an existing component request body.
Alias: Reference

.PARAMETER Content
The content of the request body. The key is a media type or media type range and the value describes it.
For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
Alias: ContentSchemas

.PARAMETER Description
A brief description of the request body. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.

.PARAMETER Required
Determines if the request body is required in the request. Defaults to false.

.PARAMETER Properties
Use to force the use of the properties keyword under a schema. Commonly used to specify a multipart/form-data multi file

.PARAMETER Examples
Supplied an Example of the media type.  The example object SHOULD be in the correct format as specified by the media type.
The `example` field is mutually exclusive of the `examples` field.
Furthermore, if referencing a `schema` which contains an example, the `example` value SHALL _override_ the example provided by the schema.

.PARAMETER Encoding
This parameter give you control over the serialization of parts of multipart request bodies.
This attribute is only applicable to multipart and application/x-www-form-urlencoded request bodies.
Use New-PodeOAEncodingObject to define the encode

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
New-PodeOARequestBody -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }

.EXAMPLE
New-PodeOARequestBody -Content @{ 'application/json' = 'UserIdSchema' }

.EXAMPLE
New-PodeOARequestBody -Reference 'UserIdBody'

.EXAMPLE
New-PodeOARequestBody -Content @{'multipart/form-data' =
                    New-PodeOAStringProperty -name 'id' -format 'uuid' |
                        New-PodeOAObjectProperty -name 'address' -NoProperties |
                        New-PodeOAObjectProperty -name 'historyMetadata' -Description 'metadata in XML format' -NoProperties |
                        New-PodeOAStringProperty -name 'profileImage' -Format Binary |
                        New-PodeOAObjectProperty
                    } -Encoding (
                        New-PodeOAEncodingObject -Name 'historyMetadata' -ContentType 'application/xml; charset=utf-8' |
                            New-PodeOAEncodingObject -Name 'profileImage' -ContentType 'image/png, image/jpeg' -Headers (
                                New-PodeOAIntProperty -name 'X-Rate-Limit-Limit' -Description 'The number of allowed requests in the current period' -Default 3 -Enum @(1,2,3)
                            )
                        )
#>
function New-PodeOARequestBody {
    [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [string]
        $Reference,

        [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
        [Alias('ContentSchemas')]
        [hashtable]
        $Content,

        [Parameter(ParameterSetName = 'BuiltIn')]
        [string]
        $Description,

        [Parameter(ParameterSetName = 'BuiltIn')]
        [switch]
        $Required,

        [Parameter(ParameterSetName = 'BuiltIn')]
        [switch]
        $Properties,

        [System.Collections.Specialized.OrderedDictionary]
        $Examples,

        [hashtable[]]
        $Encoding,

        [string[]]
        $DefinitionTag

    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    if ($Example -and $Examples) {
        throw 'Parameter -Examples and -Example are mutually exclusive'
    }
    $result = @{}
    foreach ($tag in $DefinitionTag) {
        switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
            'builtin' {
                $param = @{content = ConvertTo-PodeOAObjectSchema -DefinitionTag $tag -Content $Content -Properties:$Properties }

                if ($Required.IsPresent) {
                    $param['required'] = $Required.IsPresent
                }

                if ( $Description) {
                    $param['description'] = $Description
                }
                if ($Examples) {
                    if ( $Examples.'*/*') {
                        $Examples['"*/*"'] = $Examples['*/*']
                        $Examples.Remove('*/*')
                    }
                    foreach ($k in  $Examples.Keys ) {
                        if (!$param.content.ContainsKey($k)) {
                            $param.content[$k] = @{}
                        }
                        $param.content.$k.examples = $Examples.$k
                    }
                }
            }

            'reference' {
                Test-PodeOAComponentInternal -Field requestBodies -DefinitionTag $tag -Name $Reference -PostValidation
                $param = @{
                    '$ref' = "#/components/requestBodies/$Reference"
                }
            }
        }
        if ($Encoding) {
            if (([string]$Content.keys[0]) -match '(?i)^(multipart.*|application\/x-www-form-urlencoded)$' ) {
                $r = @{}
                foreach ( $e in $Encoding) {
                    $key = [string]$e.Keys
                    $elems = @{}
                    foreach ($v in $e[$key].Keys) {
                        if ($v -ieq 'headers') {
                            $elems.headers = ConvertTo-PodeOAHeaderProperty -Headers $e[$key].headers
                        }
                        else {
                            $elems.$v = $e[$key].$v
                        }
                    }
                    $r.$key = $elems
                }
                $param.Content.$($Content.keys[0]).encoding = $r
            }
            else {
                throw 'The encoding attribute is only applicable to multipart and application/x-www-form-urlencoded request bodies.'
            }
        }
        $result[$tag] = $param
    }

    return $result
}

<#
.SYNOPSIS
Validate a parameter with a provided schema.

.DESCRIPTION
Validate the parameter of a method against it's own schema

.PARAMETER Json
The object in Json format to validate

.PARAMETER SchemaReference
The schema name to use to validate the property.

.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.OUTPUTS
result: true if the object is validate positively
message: any validation issue

.EXAMPLE
$UserInfo = Test-PodeOAJsonSchemaCompliance -Json $UserInfo -SchemaReference 'UserIdSchema'}

#>

function Test-PodeOAJsonSchemaCompliance {
    param (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $Json,

        [Parameter(Mandatory = $true)]
        [string]
        $SchemaReference,

        [string]
        $DefinitionTag
    )
    if ($DefinitionTag) {
        if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $DefinitionTag)) {
            throw "DefinitionTag $DefinitionTag is not defined"
        }
    }
    else {
        $DefinitionTag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag
    }

    if ($Json -isnot [string]) {
        $json = ConvertTo-Json -InputObject $Json -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
    }

    if (!$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation) {
        throw 'Test-PodeOAComponentchema need to be enabled using `Enable-PodeOpenApi -EnableSchemaValidation` '
    }
    if (!(Test-PodeOAComponentSchemaJson -Name $SchemaReference -DefinitionTag $DefinitionTag)) {
        throw "The OpenApi component schema in Json doesn't exist: $SchemaReference"
    }
    if ($PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson[$SchemaReference].available) {
        [string[]] $message = @()
        $result = Test-Json -Json $Json -Schema $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson[$SchemaReference].json -ErrorVariable jsonValidationErrors -ErrorAction SilentlyContinue
        if ($jsonValidationErrors) {
            foreach ($item in $jsonValidationErrors) {
                $message += $item
            }
        }
    }
    else {
        $result = $false
        $message = 'Validation of schema with oneof or anyof is not supported'
    }

    return @{result = $result; message = $message }
}

<#
.SYNOPSIS
Converts an OpenAPI property into a Request Parameter.

.DESCRIPTION
Converts an OpenAPI property (such as from New-PodeOAIntProperty) into a Request Parameter.

.PARAMETER In
Where in the Request can the parameter be found?

.PARAMETER Property
The Property that need converting (such as from New-PodeOAIntProperty).

.PARAMETER Reference
The name of an existing component parameter to be reused.
Alias: ComponentParameter

.PARAMETER Name
Assign a name to the parameter

.PARAMETER ContentType
The content-types to be use with  component schema

.PARAMETER Schema
The component schema to use.

.PARAMETER Description
A Description of the property.

.PARAMETER Explode
If supplied, controls how arrays are serialized in query parameters

.PARAMETER AllowReserved
If supplied, determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding.
This property only applies to parameters with an in value of query. The default value is false.

.PARAMETER Required
If supplied, the object will be treated as Required where supported.(Applicable only to ContentSchema)

.PARAMETER AllowEmptyValue
If supplied, allow the parameter to be empty

.PARAMETER Style
If supplied, defines how multiple values are delimited. Possible styles depend on the parameter location: path, query, header or cookie.

.PARAMETER Deprecated
If supplied, specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false.

.PARAMETER Example
Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present.
The Example parameter is mutually exclusive of the Examples parameter.
Furthermore, if referencing a Schema  that contains an example, the Example value SHALL _override_ the example provided by the schema.
To represent examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary.

.PARAMETER Examples
Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding.
The Examples parameter is mutually exclusive of the Example parameter.
Furthermore, if referencing a Schema that contains an example, the Examples value SHALL _override_ the example provided by the schema.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query

.EXAMPLE
ConvertTo-PodeOAParameter -Reference 'UserIdParam'

.EXAMPLE
ConvertTo-PodeOAParameter  -In Header -ContentSchemas @{ 'application/json' = 'UserIdSchema' }

#>
function ConvertTo-PodeOAParameter {
    [CmdletBinding(DefaultParameterSetName = 'Reference')]
    param(
        [Parameter( Mandatory = $true, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Properties')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ContentSchema')]
        [Parameter( Mandatory = $true, ParameterSetName = 'ContentProperties')]
        [ValidateSet('Cookie', 'Header', 'Path', 'Query')]
        [string]
        $In,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Properties')]
        [Parameter( Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ContentProperties')]
        [ValidateNotNull()]
        [hashtable]
        $Property,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [Alias('ComponentParameter')]
        [string]
        $Reference,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter(ParameterSetName = 'Properties')]
        [Parameter(ParameterSetName = 'ContentSchema')]
        [Parameter(  ParameterSetName = 'ContentProperties')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ContentSchema')]
        [Alias('ComponentSchema')]
        [String]
        $Schema,

        [Parameter( Mandatory = $true, ParameterSetName = 'ContentSchema')]
        [Parameter( Mandatory = $true, ParameterSetName = 'ContentProperties')]
        [String]
        $ContentType,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Parameter( ParameterSetName = 'ContentProperties')]
        [String]
        $Description,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Switch]
        $Explode,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Parameter( ParameterSetName = 'ContentProperties')]
        [Switch]
        $Required,

        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Switch]
        $AllowEmptyValue,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Switch]
        $AllowReserved,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Parameter( ParameterSetName = 'ContentProperties')]
        [object]
        $Example,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Parameter( ParameterSetName = 'ContentProperties')]
        [System.Collections.Specialized.OrderedDictionary]
        $Examples,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'Properties')]
        [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
        [string]
        $Style,

        [Parameter( ParameterSetName = 'Schema')]
        [Parameter( ParameterSetName = 'ContentSchema')]
        [Parameter( ParameterSetName = 'Properties')]
        [Parameter( ParameterSetName = 'ContentProperties')]
        [Switch]
        $Deprecated,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    if ($PSCmdlet.ParameterSetName -ieq 'ContentSchema' -or $PSCmdlet.ParameterSetName -ieq 'Schema') {
        if (Test-PodeIsEmpty $Schema) {
            return $null
        }
        Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Schema -PostValidation
        if (!$Name ) {
            $Name = $Schema
        }
        $prop = [ordered]@{
            in   = $In.ToLowerInvariant()
            name = $Name
        }
        if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders) {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Schema  -Append
        }
        if ($AllowEmptyValue.IsPresent ) {
            $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
        }
        if ($Required.IsPresent ) {
            $prop['required'] = $Required.IsPresent
        }
        if ($Description ) {
            $prop.description = $Description
        }
        if ($Deprecated.IsPresent ) {
            $prop.deprecated = $Deprecated.IsPresent
        }
        if ($ContentType ) {
            # ensure all content types are valid
            if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') {
                throw "Invalid content-type found for schema: $($type)"
            }
            $prop.content = [ordered]@{
                $ContentType = [ordered]@{
                    schema = [ordered]@{
                        '$ref' = "#/components/schemas/$($Schema )"
                    }
                }
            }
            if ($Example ) {
                $prop.content.$ContentType.example = $Example
            }
            elseif ($Examples) {
                $prop.content.$ContentType.examples = $Examples
            }
        }
        else {
            $prop.schema = [ordered]@{
                '$ref' = "#/components/schemas/$($Schema )"
            }
            if ($Style) {
                switch ($in.ToLower()) {
                    'path' {
                        if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'query' {
                        if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'header' {
                        if (@('Simple' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'cookie' {
                        if (@('Form' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                }
                $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1)
            }

            if ($Explode.IsPresent ) {
                $prop['explode'] = $Explode.IsPresent
            }

            if ($AllowEmptyValue.IsPresent ) {
                $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
            }

            if ($AllowReserved.IsPresent) {
                $prop['allowReserved'] = $AllowReserved.IsPresent
            }

        }
    }
    elseif ($PSCmdlet.ParameterSetName -ieq 'Reference') {
        # return a reference
        Test-PodeOAComponentInternal -Field parameters  -DefinitionTag $DefinitionTag  -Name $Reference -PostValidation
        $prop = [ordered]@{
            '$ref' = "#/components/parameters/$Reference"
        }
        foreach ($tag in $DefinitionTag) {
            if ($PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters.$Reference.In -eq 'Header' -and $PodeContext.Server.Security.autoHeaders) {
                Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Reference -Append
            }
        }
    }
    else {

        if (!$Name ) {
            if ($Property.name) {
                $Name = $Property.name
            }
            else {
                throw 'Parameter requires a Name'
            }
        }
        if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders -and $Name ) {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value  $Name  -Append
        }

        # build the base parameter
        $prop = [ordered]@{
            in   = $In.ToLowerInvariant()
            name = $Name
        }
        $sch = [ordered]@{}
        if ($Property.array) {
            $sch.type = 'array'
            $sch.items = [ordered]@{
                type = $Property.type
            }
            if ($Property.format) {
                $sch.items.format = $Property.format
            }
        }
        else {
            $sch.type = $Property.type
            if ($Property.format) {
                $sch.format = $Property.format
            }
        }
        if ($ContentType) {
            if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') {
                throw "Invalid content-type found for schema: $($type)"
            }
            $prop.content = [ordered]@{
                $ContentType = [ordered] @{
                    schema = $sch
                }
            }
        }
        else {
            $prop.schema = $sch
        }

        if ($Example -and $Examples) {
            throw '-Example and -Examples are mutually exclusive'
        }
        if ($AllowEmptyValue.IsPresent ) {
            $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
        }

        if ($Description ) {
            $prop.description = $Description
        }
        elseif ($Property.description) {
            $prop.description = $Property.description
        }

        if ($Required.IsPresent ) {
            $prop.required = $Required.IsPresent
        }
        elseif ($Property.required) {
            $prop.required = $Property.required
        }

        if ($Deprecated.IsPresent ) {
            $prop.deprecated = $Deprecated.IsPresent
        }
        elseif ($Property.deprecated) {
            $prop.deprecated = $Property.deprecated
        }

        if (!$ContentType) {
            if ($Style) {
                switch ($in.ToLower()) {
                    'path' {
                        if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'query' {
                        if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'header' {
                        if (@('Simple' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                    'cookie' {
                        if (@('Form' ) -inotcontains $Style) {
                            throw "OpenApi request Style cannot be $Style for a $in parameter"
                        }
                        break
                    }
                }
                $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1)
            }

            if ($Explode.IsPresent ) {
                $prop['explode'] = $Explode.IsPresent
            }

            if ($AllowReserved.IsPresent) {
                $prop['allowReserved'] = $AllowReserved.IsPresent
            }

            if ($Example ) {
                $prop['example'] = $Example
            }
            elseif ($Examples) {
                $prop['examples'] = $Examples
            }

            if ($Property.default -and !$prop.required ) {
                $prop.schema['default'] = $Property.default
            }

            if ($Property.enum) {
                if ($Property.array) {
                    $prop.schema.items['enum'] = $Property.enum
                }
                else {
                    $prop.schema['enum'] = $Property.enum
                }
            }
        }
        else {
            if ($Example ) {
                $prop.content.$ContentType.example = $Example
            }
            elseif ($Examples) {
                $prop.content.$ContentType.examples = $Examples
            }
        }
    }

    if ($In -ieq 'Path' -and !$prop.required ) {
        Throw "If the parameter location is 'Path', the switch parameter `-Required` is required"
    }

    return $prop
}

<#
.SYNOPSIS
Sets metadate for the supplied route.

.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.

.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.

.PARAMETER Summary
A quick Summary of the route.

.PARAMETER Description
A longer Description of the route.

.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER OperationId
Sets the OperationId of the route.

.PARAMETER Tags
An array of Tags for the route, mostly for grouping.

.PARAMETER Deprecated
If supplied, the route will be flagged as deprecated.

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARouteInfo -Summary 'A quick summary' -Tags 'Admin'
#>
function Set-PodeOARouteInfo {
    [CmdletBinding()]
    [OutputType([hashtable[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [string]
        $Summary,

        [string]
        $Description,

        [System.Collections.Specialized.OrderedDictionary]
        $ExternalDoc,

        [string]
        $OperationId,

        [string[]]
        $Tags,

        [switch]
        $Deprecated,

        [switch]
        $PassThru,

        [string[]]
        $DefinitionTag
    )

    if ($null -eq $Route) { throw 'Set-PodeOARouteInfo - The parameter -Route cannot be NULL.' }

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($r in @($Route)) {

        $r.OpenApi.DefinitionTag = $DefinitionTag

        if ($Summary) {
            $r.OpenApi.Summary = $Summary
        }
        if ($Description) {
            $r.OpenApi.Description = $Description
        }
        if ($OperationId) {
            if ($Route.Count -gt 1) {
                throw "OperationID:$OperationId has to be unique and cannot be applied to an array."
            }
            foreach ($tag in $DefinitionTag) {
                if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) {
                    throw "OperationID:$OperationId has to be unique."
                }
                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId += $OperationId
            }
            $r.OpenApi.OperationId = $OperationId
        }
        if ($Tags) {
            $r.OpenApi.Tags = $Tags
        }

        if ($ExternalDocs) {
            $r.OpenApi.ExternalDocs = $ExternalDoc
        }

        $r.OpenApi.Swagger = $true
        if ($Deprecated.IsPresent) {
            $r.OpenApi.Deprecated = $Deprecated.IsPresent
        }
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, RapiPdf or Bookmarks.

.DESCRIPTION
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, RapiPdf  or Bookmarks.

.LINK
https://github.com/mrin9/RapiPdf

.LINK
https://github.com/Authress-Engineering/openapi-explorer

.LINK
https://github.com/stoplightio/elements

.LINK
https://github.com/rapi-doc/RapiDoc

.LINK
https://github.com/Redocly/redoc

.LINK
https://github.com/swagger-api/swagger-ui

.PARAMETER Type
The Type of OpenAPI viewer to use.

.PARAMETER Path
The route Path where the docs can be accessed. (Default: "/$Type")

.PARAMETER OpenApiUrl
The URL where the OpenAPI definition can be retrieved. (Default is the OpenAPI path from Enable-PodeOpenApi)

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied.

.PARAMETER Title
The title of the web page. (Default is the OpenAPI title from Enable-PodeOpenApi)

.PARAMETER DarkMode
If supplied, the page will be rendered using a dark theme (this is not supported for all viewers).

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Bookmarks
If supplied, create a new documentation bookmarks page

.PARAMETER Editor
If supplied, enable the Swagger-Editor

.PARAMETER NoAdvertise
If supplied, it is not going to state the documentation URL at the startup of the server

.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Enable-PodeOAViewer -Type Swagger -DarkMode

.EXAMPLE
Enable-PodeOAViewer -Type ReDoc -Title 'Some Title' -OpenApi 'http://some-url/openapi'

.EXAMPLE
Enable-PodeOAViewer -Bookmarks

Adds a route that enables a viewer to display with links to any documentation tool associated with the OpenApi.
#>
function Enable-PodeOAViewer {
    [CmdletBinding(DefaultParameterSetName = 'Doc')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Doc')]
        [ValidateSet('Swagger', 'ReDoc', 'RapiDoc', 'StopLight', 'Explorer', 'RapiPdf' )]
        [string]
        $Type,

        [string]
        $Path,

        [string]
        $OpenApiUrl,

        [object[]]
        $Middleware,

        [string]
        $Title,

        [switch]
        $DarkMode,

        [string[]]
        $EndpointName,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter(Mandatory = $true, ParameterSetName = 'Bookmarks')]
        [switch]
        $Bookmarks,

        [Parameter( ParameterSetName = 'Bookmarks')]
        [switch]
        $NoAdvertise,

        [Parameter(Mandatory = $true, ParameterSetName = 'Editor')]
        [switch]
        $Editor,

        [string]
        $DefinitionTag
    )
    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    # error if there's no OpenAPI URL
    $OpenApiUrl = Protect-PodeValue -Value $OpenApiUrl -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Path
    if ([string]::IsNullOrWhiteSpace($OpenApiUrl)) {
        throw "No OpenAPI URL supplied for $($Type)"
    }

    # fail if no title
    $Title = Protect-PodeValue -Value $Title -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.Title
    if ([string]::IsNullOrWhiteSpace($Title)) {
        throw "No title supplied for $($Type) page"
    }

    if ($Editor.IsPresent) {
        # set a default path
        $Path = Protect-PodeValue -Value $Path -Default '/editor'
        if ([string]::IsNullOrWhiteSpace($Title)) {
            throw "No route path supplied for $($Type) page"
        }
        if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) {
            throw "This version on Swagger-Editor doesn't support OpenAPI 3.1"
        }
        # setup meta info
        $meta = @{
            Title             = $Title
            OpenApi           = "$($OpenApiUrl)?format=yaml"
            DarkMode          = $DarkMode
            DefinitionTag     = $DefinitionTag
            SwaggerEditorDist = 'https://unpkg.com/swagger-editor-dist@4'
        }
        Add-PodeRoute -Method Get -Path $Path `
            -Middleware $Middleware -ArgumentList $meta `
            -EndpointName $EndpointName -Authentication $Authentication `
            -Role $Role -Scope $Scope -Group $Group `
            -ScriptBlock {
            param($meta)
            $Data = @{
                Title             = $meta.Title
                OpenApi           = $meta.OpenApi
                SwaggerEditorDist = $meta.SwaggerEditorDist
            }

            $podeRoot = Get-PodeModuleMiscPath
            Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-swagger-editor.html.pode')) -Data $Data
        }

        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer['editor'] = $Path
    }
    elseif ($Bookmarks.IsPresent) {
        # set a default path
        $Path = Protect-PodeValue -Value $Path -Default '/bookmarks'
        if ([string]::IsNullOrWhiteSpace($Title)) {
            throw "No route path supplied for $($Type) page"
        }
        # setup meta info
        $meta = @{
            Title         = $Title
            OpenApi       = $OpenApiUrl
            DarkMode      = $DarkMode
            DefinitionTag = $DefinitionTag
        }

        $route = Add-PodeRoute -Method Get -Path $Path `
            -Middleware $Middleware -ArgumentList $meta `
            -EndpointName $EndpointName -Authentication $Authentication `
            -Role $Role -Scope $Scope -Group $Group `
            -PassThru -ScriptBlock {
            param($meta)
            $Data = @{
                Title   = $meta.Title
                OpenApi = $meta.OpenApi
            }
            $DefinitionTag = $meta.DefinitionTag
            foreach ($type in $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer.Keys) {
                $Data[$type] = $true
                $Data["$($type)_path"] = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer[$type]
            }

            $podeRoot = Get-PodeModuleMiscPath
            Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-doc-bookmarks.html.pode')) -Data $Data
        }
        if (! $NoAdvertise.IsPresent) {
            $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.bookmarks = @{
                path       = $Path
                route      = @()
                openApiUrl = $OpenApiUrl
            }
            $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.bookmarks.route += $route
        }

    }
    else {
        if ($Type -ieq 'RapiPdf' -and (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag)) {
            throw "The Document tool RapidPdf doesn't support OpenAPI 3.1"
        }
        # set a default path
        $Path = Protect-PodeValue -Value $Path -Default "/$($Type.ToLowerInvariant())"
        if ([string]::IsNullOrWhiteSpace($Title)) {
            throw "No route path supplied for $($Type) page"
        }
        # setup meta info
        $meta = @{
            Type     = $Type.ToLowerInvariant()
            Title    = $Title
            OpenApi  = $OpenApiUrl
            DarkMode = $DarkMode
        }
        $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer[$($meta.Type)] = $Path
        # add the viewer route
        Add-PodeRoute -Method Get -Path $Path -Middleware $Middleware -ArgumentList $meta `
            -EndpointName $EndpointName -Authentication $Authentication `
            -Role $Role -Scope $Scope -Group $Group `
            -ScriptBlock {
            param($meta)
            $podeRoot = Get-PodeModuleMiscPath
            if ( $meta.DarkMode) { $Theme = 'dark' } else { $Theme = 'light' }
            Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, "default-$($meta.Type).html.pode")) -Data @{
                Title    = $meta.Title
                OpenApi  = $meta.OpenApi
                DarkMode = $meta.DarkMode
                Theme    = $Theme
            }
        }
    }

}


<#
.SYNOPSIS
Define an external docs reference.

.DESCRIPTION
Define an external docs reference.

.PARAMETER url
The link to the external documentation

.PARAMETER Description
A Description of the external documentation.

.EXAMPLE
$swaggerDoc = New-PodeOAExternalDoc  -Description 'Find out more about Swagger' -Url 'http://swagger.io'

Add-PodeRoute -PassThru | Set-PodeOARouteInfo -Summary 'A quick summary' -Tags 'Admin' -ExternalDoc $swaggerDoc

.EXAMPLE
$swaggerDoc = New-PodeOAExternalDoc    -Description 'Find out more about Swagger' -Url 'http://swagger.io'
Add-PodeOATag -Name 'user' -Description 'Operations about user' -ExternalDoc $swaggerDoc
#>
function New-PodeOAExternalDoc {
    param(

        [Parameter(Mandatory = $true)]
        [ValidateScript({ $_ -imatch '^https?://.+' })]
        $Url,

        [string]
        $Description
    )
    $param = [ordered]@{}

    if ($Description) {
        $param.description = $Description
    }
    $param['url'] = $Url
    return $param
}



<#
.SYNOPSIS
Add an external docs reference to the OpenApi document.

.DESCRIPTION
Add an external docs reference to the OpenApi document.

.PARAMETER ExternalDoc
An externalDoc object


.PARAMETER Name
The Name of the reference.

.PARAMETER url
The link to the external documentation

.PARAMETER Description
A Description of the external documentation.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAExternalDoc  -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io'

.EXAMPLE
$ExtDoc = New-PodeOAExternalDoc  -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io'
$ExtDoc|Add-PodeOAExternalDoc
#>
function Add-PodeOAExternalDoc {
    [CmdletBinding(DefaultParameterSetName = 'Pipe')]
    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Pipe')]
        [System.Collections.Specialized.OrderedDictionary ]
        $ExternalDoc,

        [Parameter(Mandatory = $true, ParameterSetName = 'NewRef')]
        [ValidateScript({ $_ -imatch '^https?://.+' })]
        $Url,

        [Parameter(ParameterSetName = 'NewRef')]
        [string]
        $Description,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        if ($PSCmdlet.ParameterSetName -ieq 'NewRef') {
            $param = [ordered]@{url = $Url }
            if ($Description) {
                $param.description = $Description
            }
            $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $param
        }
        else {
            $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $ExternalDoc
        }
    }

}


<#
.SYNOPSIS
Creates a OpenAPI Tag reference property.

.DESCRIPTION
Creates a new OpenAPI tag reference.

.PARAMETER Name
The Name of the tag.

.PARAMETER Description
A Description of the tag.

.PARAMETER ExternalDoc
If supplied, the tag references an existing external documentation reference.
The parameter is created by Add-PodeOAExternalDoc

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOATag -Name 'store' -Description 'Access to Petstore orders' -ExternalDoc 'SwaggerDocs'
#>
function Add-PodeOATag {
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [string]
        $Description,

        [System.Collections.Specialized.OrderedDictionary]
        $ExternalDoc,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($tag in $DefinitionTag) {
        $param = [ordered]@{
            'name' = $Name
        }

        if ($Description) {
            $param.description = $Description
        }

        if ($ExternalDoc) {
            $param.externalDocs = $ExternalDoc
        }

        $PodeContext.Server.OpenAPI.Definitions[$tag].tags[$Name] = $param
    }
}


<#
.SYNOPSIS
Creates an OpenAPI metadata.

.DESCRIPTION
Creates an OpenAPI metadata like TermOfService, license and so on.
The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.

.PARAMETER Title
The Title of the API.

.PARAMETER Version
The Version of the API.
The OpenAPI Specification is versioned using Semantic Versioning 2.0.0 (semver) and follows the semver specification.
https://semver.org/spec/v2.0.0.html

.PARAMETER Description
A short description of the API.
CommonMark syntax MAY be used for rich text representation.
https://spec.commonmark.org/

.PARAMETER TermsOfService
A URL to the Terms of Service for the API. MUST be in the format of a URL.

.PARAMETER LicenseName
The license name used for the API.

.PARAMETER LicenseUrl
A URL to the license used for the API. MUST be in the format of a URL.

.PARAMETER ContactName
The identifying name of the contact person/organization.

.PARAMETER ContactEmail
The email address of the contact person/organization. MUST be in the format of an email address.

.PARAMETER ContactUrl
The URL pointing to the contact information. MUST be in the format of a URL.

.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAInfo -TermsOfService 'http://swagger.io/terms/' -License 'Apache 2.0' -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail '[email protected]' -ContactUrl 'http://example.com/support'
#>

function Add-PodeOAInfo {
    param(
        [string]
        $Title,

        [ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$')]
        [string]
        $Version ,

        [string]
        $Description,

        [ValidateScript({ $_ -imatch '^https?://.+' })]
        [string]
        $TermsOfService,

        [string]
        $LicenseName,

        [ValidateScript({ $_ -imatch '^https?://.+' })]
        [string]
        $LicenseUrl,

        [string]
        $ContactName,

        [ValidateScript({ $_ -imatch '^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$' })]
        [string]
        $ContactEmail,

        [ValidateScript({ $_ -imatch '^https?://.+' })]
        [string]
        $ContactUrl,

        [string]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    $Info = [ordered]@{}

    if ($LicenseName) {
        $Info.license = [ordered]@{
            'name' = $LicenseName
        }
    }
    if ($LicenseUrl) {
        if ( $Info.license ) {
            $Info.license.url = $LicenseUrl
        }
        else {
            throw 'The OpenAPI property license.name is required. Use -LicenseName'
        }
    }


    if ($Title) {
        $Info.title = $Title
    }
    elseif (  $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title) {
        $Info.title = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title
    }

    if ($Version) {
        $Info.version = $Version
    }
    elseif ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version) {
        $Info.version = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version
    }
    else {
        $Info.version = '1.0.0'
    }

    if ($Description ) {
        $Info.description = $Description
    }
    elseif ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description) {
        $Info.description = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description
    }

    if ($TermsOfService) {
        $Info['termsOfService'] = $TermsOfService
    }

    if ($ContactName -or $ContactEmail -or $ContactUrl ) {
        $Info['contact'] = [ordered]@{}

        if ($ContactName) {
            $Info['contact'].name = $ContactName
        }

        if ($ContactEmail) {
            $Info['contact'].email = $ContactEmail
        }

        if ($ContactUrl) {
            $Info['contact'].url = $ContactUrl
        }
    }
    $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info = $Info

}


<#
.SYNOPSIS
Creates a new OpenAPI example.

.DESCRIPTION
Creates a new OpenAPI example.

.PARAMETER ParamsList
Used to pipeline multiple properties

.PARAMETER MediaType
The Media Type associated with the Example.

.PARAMETER Name
The Name of the Example.

.PARAMETER Summary
Short description for the example


.PARAMETER Description
Long description for the example.

.PARAMETER Reference
A reference to a reusable component example

.PARAMETER Value
Embedded literal example. The  value Parameter and ExternalValue parameter are mutually exclusive.
To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary.

.PARAMETER ExternalValue
A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents.
The -Value parameter and -ExternalValue parameter are mutually exclusive.                                |

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
New-PodeOAExample -ContentMediaType 'text/plain' -Name 'user' -Summary = 'User Example in Plain text' -ExternalValue = 'http://foo.bar/examples/user-example.txt'
.EXAMPLE
$example =
    New-PodeOAExample -ContentMediaType 'application/json' -Name 'user' -Summary = 'User Example' -ExternalValue = 'http://foo.bar/examples/user-example.json'  |
        New-PodeOAExample -ContentMediaType 'application/xml' -Name 'user' -Summary = 'User Example in XML' -ExternalValue = 'http://foo.bar/examples/user-example.xml'

#>
function New-PodeOAExample {
    [CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
    [OutputType([System.Collections.Specialized.OrderedDictionary ])]

    param(
        [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Inbuilt')]
        [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Reference')]
        [System.Collections.Specialized.OrderedDictionary ]
        $ParamsList,

        [string]
        $MediaType,

        [Parameter(Mandatory = $true, ParameterSetName = 'Inbuilt')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter( ParameterSetName = 'Inbuilt')]
        [string]
        $Summary,

        [Parameter( ParameterSetName = 'Inbuilt')]
        [string]
        $Description,

        [Parameter(  ParameterSetName = 'Inbuilt')]
        [object]
        $Value,

        [Parameter(  ParameterSetName = 'Inbuilt')]
        [string]
        $ExternalValue,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Reference,

        [string[]]
        $DefinitionTag
    )
    begin {

        if (Test-PodeIsEmpty -Value $DefinitionTag) {
            $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
        }

        if ($PSCmdlet.ParameterSetName -ieq 'Reference') {
            Test-PodeOAComponentInternal -Field examples -DefinitionTag $DefinitionTag -Name $Reference -PostValidation
            $Name = $Reference
            $Example = [ordered]@{'$ref' = "#/components/examples/$Reference" }
        }
        else {
            if ( $ExternalValue -and $Value) {
                throw '-Value or -ExternalValue are mutually exclusive'
            }
            $Example = [ordered]@{ }
            if ($Summary) {
                $Example.summary = $Summary
            }
            if ($Description) {
                $Example.description = $Description
            }
            if ($Value) {
                $Example.value = $Value
            }
            elseif ($ExternalValue) {
                $Example.externalValue = $ExternalValue
            }
            else {
                throw '-Value or -ExternalValue are mandatory'
            }
        }
        $param = [ordered]@{}
        if ($MediaType) {
            $param.$MediaType = [ordered]@{
                $Name = $Example
            }
        }
        else {
            $param.$Name = $Example
        }
    }
    process {
    }
    end {
        if ($ParamsList) {
            if ($ParamsList.keys -contains $param.Keys[0]) {
                $param.Values[0].GetEnumerator() | ForEach-Object { $ParamsList[$param.Keys[0]].$($_.Key) = $_.Value }
            }
            else {
                $param.GetEnumerator() | ForEach-Object { $ParamsList[$_.Key] = $_.Value }
            }
            return $ParamsList
        }
        else {
            return [System.Collections.Specialized.OrderedDictionary] $param
        }
    }
}

<#
.SYNOPSIS
Adds a single encoding definition applied to a single schema property.

.DESCRIPTION
A single encoding definition applied to a single schema property.

.PARAMETER EncodingList
Used by pipe

.PARAMETER Title
The Name of the associated encoded property .

.PARAMETER ContentType
Content-Type for encoding a specific property. Default value depends on the property type: for `string` with `format` being `binary` – `application/octet-stream`;
for other primitive types – `text/plain`; for `object` - `application/json`; for `array` – the default is defined based on the inner type.
The value can be a specific media type (e.g. `application/json`), a wildcard media type (e.g. `image/*`), or a comma-separated list of the two types.

.PARAMETER Headers
A map allowing additional information to be provided as headers, for example `Content-Disposition`.
`Content-Type` is described separately and SHALL be ignored in this section.
This property SHALL be ignored if the request body media type is not a `multipart`.

.PARAMETER Style
Describes how a specific property value will be serialized depending on its type.  See [Parameter Object](#parameterObject) for details on the [`style`](#parameterStyle) property.
The behavior follows the same values as `query` parameters, including default values.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.

.PARAMETER Explode
When enabled, property values of type `array` or `object` generate separate parameters for each value of the array, or key-value-pair of the map.  For other types of properties this property has no effect.
When [`style`](#encodingStyle) is `form`, the `Explode` is set to `true`.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.

.PARAMETER AllowReserved
Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) `:/?#[]@!$&'()*+,;=` to be included without percent-encoding.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.

.EXAMPLE

New-PodeOAEncodingObject -Name 'profileImage' -ContentType 'image/png, image/jpeg' -Headers (
                                New-PodeOAIntProperty -name 'X-Rate-Limit-Limit' -Description 'The number of allowed requests in the current period' -Default 3 -Enum @(1,2,3) -Maximum 3
                            )
#>
function New-PodeOAEncodingObject {
    param (
        [Parameter(ValueFromPipeline = $true, DontShow = $true )]
        [hashtable[]]
        $EncodingList,

        [Parameter(Mandatory = $true)]
        [Alias('Name')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Title,

        [string]
        $ContentType,

        [hashtable[]]
        $Headers,

        [ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
        [string]
        $Style,

        [switch]
        $Explode,

        [switch]
        $AllowReserved
    )
    begin {

        $encoding = [ordered]@{
            $Title = [ordered]@{}
        }
        if ($ContentType) {
            $encoding.$Title.contentType = $ContentType
        }
        if ($Style) {
            $encoding.$Title.style = $Style
        }

        if ($Headers) {
            $encoding.$Title.headers = $Headers
        }

        if ($Explode.IsPresent ) {
            $encoding.$Title.explode = $Explode.IsPresent
        }
        if ($AllowReserved.IsPresent ) {
            $encoding.$Title.allowReserved = $AllowReserved.IsPresent
        }

        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($EncodingList) {
            $collectedInput.AddRange($EncodingList)
        }
    }

    end {
        if ($collectedInput) {
            return $collectedInput + $encoding
        }
        else {
            return $encoding
        }
    }
}


<#
.SYNOPSIS
Adds OpenAPI callback configurations to routes in a Pode web application.

.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.

.DESCRIPTION
The Add-PodeOACallBack function is used for defining OpenAPI callback configurations for routes in a Pode server.
It enables setting up API specifications including detailed parameters, request body schemas, and response structures for various HTTP methods.

.PARAMETER Path
Specifies the callback path, usually a relative URL.
The key that identifies the Path Item Object is a runtime expression evaluated in the context of a runtime HTTP request/response to identify the URL for the callback request.
A simple example is `$request.body#/url`.
The runtime expression allows complete access to the HTTP message, including any part of a body that a JSON Pointer (RFC6901) can reference.
More information on JSON Pointer can be found at [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901).

.PARAMETER Name
Alias for 'Name'. A unique identifier for the callback.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Reference
A reference to a reusable CallBack component.

.PARAMETER Method
Defines the HTTP method for the callback (e.g., GET, POST, PUT). Supports standard HTTP methods and a wildcard (*) for all methods.

.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).

.PARAMETER RequestBody
Defines the schema of the request body. Can be set using New-PodeOARequestBody.

.PARAMETER Responses
Defines the possible responses for the callback. Can be set using New-PodeOAResponse.

.PARAMETER DefinitionTag
A array of string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.EXAMPLE
    Add-PodeOACallBack -Title 'test' -Path '{$request.body#/id}' -Method Post `
        -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) `
        -Response (
            New-PodeOAResponse -StatusCode 200 -Description 'Successful operation'  -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet'  -Array)
            New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
            New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
            New-PodeOAResponse -Default -Description 'Something is wrong'
        )
    This example demonstrates adding a POST callback to handle a request body and define various responses based on different status codes.

.NOTES
    Ensure that the provided parameters match the expected schema and formats of Pode and OpenAPI specifications.
    The function is useful for dynamically configuring and documenting API callbacks in a Pode server environment.
#>

function Add-PodeOACallBack {
    [CmdletBinding(DefaultParameterSetName = 'inbuilt')]
    [OutputType([hashtable[]])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory = $true , ParameterSetName = 'inbuilt')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Reference')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [string]
        $Reference,

        [Parameter(Mandatory = $true , ParameterSetName = 'inbuilt')]
        [string]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'inbuilt')]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(ParameterSetName = 'inbuilt')]
        [hashtable[]]
        $Parameters,

        [Parameter(ParameterSetName = 'inbuilt')]
        [hashtable]
        $RequestBody,

        [Parameter(ParameterSetName = 'inbuilt')]
        [hashtable]
        $Responses,

        [switch]
        $PassThru,

        [string[]]
        $DefinitionTag
    )

    if ($null -eq $Route) { throw 'Add-PodeOACallBack - The parameter -Route cannot be NULL.' }

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    foreach ($r in @($Route)) {
        foreach ($tag in $DefinitionTag) {
            if ($Reference) {
                Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation
                if (!$Name) {
                    $Name = $Reference
                }
                if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
                    $r.OpenApi.CallBacks[$tag] = [ordered]@{}
                }
                $r.OpenApi.CallBacks[$tag].$Name = @{
                    '$ref' = "#/components/callbacks/$Reference"
                }
            }
            else {
                if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
                    $r.OpenApi.CallBacks[$tag] = [ordered]@{}
                }
                $r.OpenApi.CallBacks[$tag].$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag
            }
        }
    }

    if ($PassThru) {
        return $Route
    }
}

<#
.SYNOPSIS
Adds a response definition to the Callback.

.DESCRIPTION
Adds a response definition to the Callback.

.PARAMETER ResponseList
Hidden parameter used to pipe multiple CallBacksResponses

.PARAMETER StatusCode
The HTTP StatusCode for the response.To define a range of response codes, this field MAY contain the uppercase wildcard character `X`.
For example, `2XX` represents all response codes between `[200-299]`. Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`.
If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.

.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).
Alias: ContentSchemas

.PARAMETER Headers
The header name and schema the response returns (the schema is created using Add-PodeOAComponentHeader cmd-let).
Alias: HeaderSchemas

.PARAMETER Description
A Description of the response. (Default: the HTTP StatusCode description)

.PARAMETER Reference
A Reference Name of an existing component response to use.

.PARAMETER Links
A Response link definition

.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Content (  New-PodeOAContentMediaType -ContentMediaType 'application/json' -Content(New-PodeOAIntProperty -Name 'userId' -Object) )

.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = 'UserIdSchema' }

.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Reference 'OKResponse'

.EXAMPLE
Add-PodeOACallBack -Title 'test' -Path '$request.body#/id' -Method Post  -RequestBody (
        New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentMediaType '*/*' -Content (New-PodeOAStringProperty -Name 'id'))
    ) `
    -Response (
        New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet'  -Array) |
            New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
                New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
            New-PodeOAResponse -Default   -Description 'Something is wrong'
            )
#>

function New-PodeOAResponse {
    [CmdletBinding(DefaultParameterSetName = 'Schema')]
    [OutputType([hashtable])]
    param(
        [Parameter(ValueFromPipeline = $true , DontShow = $true )]
        [hashtable]
        $ResponseList,

        [Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [ValidatePattern('^([1-5][0-9][0-9]|[1-5]XX)$')]
        [string]
        $StatusCode,

        [Parameter(ParameterSetName = 'Schema')]
        [Parameter(ParameterSetName = 'SchemaDefault')]
        [Alias('ContentSchemas')]
        [hashtable]
        $Content,

        [Alias('HeaderSchemas')]
        [AllowEmptyString()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] })]
        $Headers,

        [Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
        [string]
        $Description  ,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [Parameter(ParameterSetName = 'ReferenceDefault')]
        [string]
        $Reference,

        [Parameter(Mandatory = $true, ParameterSetName = 'ReferenceDefault')]
        [Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
        [switch]
        $Default,

        [Parameter(ParameterSetName = 'Schema')]
        [Parameter(ParameterSetName = 'SchemaDefault')]
        [System.Collections.Specialized.OrderedDictionary ]
        $Links,

        [string[]]
        $DefinitionTag
    )
    begin {

        if (Test-PodeIsEmpty -Value $DefinitionTag) {
            $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
        }

        # override status code with default
        if ($Default) {
            $code = 'default'
        }
        else {
            $code = "$($StatusCode)"
        }
        $response = @{}
    }
    process {
        foreach ($tag in $DefinitionTag) {
            if (! $response.$tag) {
                $response.$tag = [ordered] @{}
            }
            $response[$tag][$code] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters
        }
    }
    end {
        if ($ResponseList) {
            foreach ($tag in $DefinitionTag) {
                if (! $ResponseList.ContainsKey( $tag) ) {
                    $ResponseList[$tag] = [ordered] @{}
                }
                $response[$tag].GetEnumerator() | ForEach-Object { $ResponseList[$tag][$_.Key] = $_.Value }
            }
            return $ResponseList
        }
        else {
            return  $response
        }
    }
}

<#
.SYNOPSIS
    Creates media content type definitions for OpenAPI specifications.

.DESCRIPTION
    The New-PodeOAContentMediaType function generates media content type definitions suitable for use in OpenAPI specifications. It supports various media types and allows for the specification of content as either a single object or an array of objects.

.PARAMETER MediaType
    An array of strings specifying the media types to be defined. Media types should conform to standard MIME types (e.g., 'application/json', 'image/png'). The function validates these media types against a regular expression to ensure they are properly formatted.

.PARAMETER Content
    The content definition for the media type. This could be an object representing the structure of the content expected for the specified media types.

.PARAMETER Array
    A switch parameter, used in the 'Array' parameter set, to indicate that the content should be treated as an array.

.PARAMETER UniqueItems
    A switch parameter, used in the 'Array' parameter set, to specify that items in the array should be unique.

.PARAMETER MinItems
    Used in the 'Array' parameter set to specify the minimum number of items that should be present in the array.

.PARAMETER MaxItems
    Used in the 'Array' parameter set to specify the maximum number of items that should be present in the array.

.PARAMETER Title
    Used in the 'Array' parameter set to provide a title for the array content.

.PARAMETER Upload
    If provided configure the media for an upload changing the result based on the OpenApi version

.PARAMETER ContentEncoding
    Define the content encoding for upload (Default Binary)

.PARAMETER PartContentMediaType
    Define the content encoding for multipart upload

.EXAMPLE
    Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -ScriptBlock {
        Write-PodeJsonResponse -Value 'done' -StatusCode 200
    } | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma separated strings' -Tags 'pet' -OperationId 'findPetsByStatus' -PassThru |
        Set-PodeOARequest -PassThru -Parameters @(
            (New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query)
        ) |
        Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru |
        Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value'
    This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'.

.EXAMPLE
    $content = @{ type = 'string' }
    $mediaType = 'application/json'
    New-PodeOAContentMediaType -MediaType $mediaType -Content $content
    This example creates a media content type definition for 'application/json' with a simple string content type.

.EXAMPLE
    $content = @{ type = 'object'; properties = @{ name = @{ type = 'string' } } }
    $mediaTypes = 'application/json', 'application/xml'
    New-PodeOAContentMediaType -MediaType $mediaTypes -Content $content -Array -MinItems 1 -MaxItems 5 -Title 'UserList'
    This example demonstrates defining an array of objects for both 'application/json' and 'application/xml' media types, with a specified range for the number of items and a title.

.EXAMPLE
    Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -ScriptBlock {
        Write-PodeJsonResponse -Value 'done' -StatusCode 200
    } | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma separated strings' -Tags 'pet' -OperationId 'findPetsByStatus' -PassThru |
        Set-PodeOARequest -PassThru -Parameters @(
            (New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query)
        ) |
        Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru |
        Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value'
    This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'.

.NOTES
    This function is useful for dynamically creating media type specifications in OpenAPI documentation, providing flexibility in defining the expected content structure for different media types.
#>

function New-PodeOAContentMediaType {
    [CmdletBinding(DefaultParameterSetName = 'inbuilt')]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [string[]]
        $MediaType = '*/*',

        [object]
        $Content,

        [Parameter(  Mandatory = $true, ParameterSetName = 'Array')]
        [switch]
        $Array,

        [Parameter(ParameterSetName = 'Array')]
        [switch]
        $UniqueItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MinItems,

        [Parameter(ParameterSetName = 'Array')]
        [int]
        $MaxItems,

        [Parameter(ParameterSetName = 'Array')]
        [string]
        $Title,

        [Parameter(Mandatory = $true, ParameterSetName = 'Upload')]
        [switch]
        $Upload,

        [Parameter(  ParameterSetName = 'Upload')]
        [ValidateSet('Binary', 'Base64')]
        [string]
        $ContentEncoding = 'Binary',

        [Parameter(  ParameterSetName = 'Upload')]
        [string]
        $PartContentMediaType

    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
    $props = [ordered]@{}
    foreach ($media in $MediaType) {
        if ($media -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$') {
            throw "Invalid content-type found for schema: $($media)"
        }
        if ($Upload.IsPresent) {
            if ( $media -ieq 'multipart/form-data' -and $Content) {
                $Content = @{'__upload' = @{
                        'content'              = $Content
                        'partContentMediaType' = $PartContentMediaType
                    }
                }
            }
            else {
                $Content = @{'__upload' = @{
                        'contentEncoding' = $ContentEncoding
                    }
                }

            }
        }
        else {
            if ($null -eq $Content ) {
                $Content = @{}
            }
        }
        if ($Array.IsPresent) {
            $props[$media] = @{
                __array   = $true
                __content = $Content
                __upload  = $Upload
            }
            if ($MinItems) {
                $props[$media].__minItems = $MinItems
            }
            if ($MaxItems) {
                $props[$media].__maxItems = $MaxItems
            }
            if ($Title) {
                $props[$media].__title = $Title
            }
            if ($UniqueItems.IsPresent) {
                $props[$media].__uniqueItems = $UniqueItems.IsPresent
            }

        }
        else {
            $props[$media] = $Content
        }
    }
    return $props
}


<#
.SYNOPSIS
    Adds a response link to an existing list of OpenAPI response links.

.DESCRIPTION
    The New-PodeOAResponseLink function is designed to add a new response link to an existing OrderedDictionary of OpenAPI response links.
    It can be used to define complex response structures with links to other operations or references, and it supports adding multiple links through pipeline input.

.PARAMETER LinkList
    An OrderedDictionary of existing response links.
    This parameter is intended for use with pipeline input, allowing the function to add multiple links to the collection.
    It is hidden from standard help displays to emphasize its use primarily in pipeline scenarios.

.PARAMETER Name
    Mandatory. A unique name for the response link.
    Must be a valid string composed of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Description
    A brief description of the response link. CommonMark syntax may be used for rich text representation.
    For more information on CommonMark syntax, see [CommonMark Specification](https://spec.commonmark.org/).

.PARAMETER OperationId
    The name of an existing, resolvable OpenAPI Specification (OAS) operation, as defined with a unique `operationId`.
    This parameter is mandatory when using the 'OperationId' parameter set and is mutually exclusive of the `OperationRef` field. It is used to specify the unique identifier of the operation the link is associated with.

.PARAMETER OperationRef
    A relative or absolute URI reference to an OAS operation.
    This parameter is mandatory when using the 'OperationRef' parameter set and is mutually exclusive of the `OperationId` field.
    It MUST point to an Operation Object. Relative `operationRef` values MAY be used to locate an existing Operation Object in the OpenAPI specification.

.PARAMETER Reference
A Reference Name of an existing component link to use.

.PARAMETER Parameters
    A map representing parameters to pass to an operation as specified with `operationId` or identified via `operationRef`.
    The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation.
    Parameter names can be qualified using the parameter location syntax `[{in}.]{name}` for operations that use the same parameter name in different locations (e.g., path.id).

.PARAMETER RequestBody
    A string representing the request body to use as a request body when calling the target.

.PARAMETER DefinitionTag
    An Array of strings representing the unique tag for the API specification.
    This tag helps distinguish between different versions or types of API specifications within the application.
    You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
    $links = New-PodeOAResponseLink -LinkList $links -Name 'address' -OperationId 'getUserByName' -Parameters @{'username' = '$request.path.username'}
    Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User'} -Links $links
    This example demonstrates creating and adding a link named 'address' associated with the operation 'getUserByName' to an OrderedDictionary of links. The updated dictionary is then used in the 'Add-PodeOAResponse' function to define a response with a status code of 200.

.NOTES
    The function supports adding links either by specifying an 'OperationId' or an 'OperationRef', making it versatile for different OpenAPI specification needs.
    It's important to match the parameters and response structures as per the OpenAPI specification to ensure the correct functionality of the API documentation.
#>
function New-PodeOAResponseLink {
    [CmdletBinding(DefaultParameterSetName = 'OperationId')]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(ValueFromPipeline = $true , DontShow = $true )]
        [System.Collections.Specialized.OrderedDictionary ]
        $LinkList,

        [Parameter( Mandatory = $false, ParameterSetName = 'Reference')]
        [Parameter( Mandatory = $true, ParameterSetName = 'OperationRef')]
        [Parameter( Mandatory = $true, ParameterSetName = 'OperationId')]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter( ParameterSetName = 'OperationRef')]
        [Parameter( ParameterSetName = 'OperationId')]
        [string]
        $Description,

        [Parameter(Mandatory = $true, ParameterSetName = 'OperationId')]
        [string]
        $OperationId,

        [Parameter(Mandatory = $true, ParameterSetName = 'OperationRef')]
        [string]
        $OperationRef,

        [Parameter( ParameterSetName = 'OperationRef')]
        [Parameter( ParameterSetName = 'OperationId')]
        [hashtable]
        $Parameters,

        [Parameter( ParameterSetName = 'OperationRef')]
        [Parameter( ParameterSetName = 'OperationId')]
        [string]
        $RequestBody,

        [Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
        [string]
        $Reference,

        [string[]]
        $DefinitionTag

    )
    begin {

        if (Test-PodeIsEmpty -Value $DefinitionTag) {
            $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
        }
        if ($Reference) {
            Test-PodeOAComponentInternal -Field links -DefinitionTag $DefinitionTag -Name $Reference  -PostValidation
            if (!$Name) {
                $Name = $Reference
            }
            $link = [ordered]@{
                $Name = @{
                    '$ref' = "#/components/links/$Reference"
                }
            }
        }
        else {
            $link = [ordered]@{
                $Name = New-PodeOAResponseLinkInternal -Params $PSBoundParameters
            }
        }
    }
    process {
    }
    end {
        if ($LinkList) {
            $link.GetEnumerator() | ForEach-Object { $LinkList[$_.Key] = $_.Value }
            return $LinkList
        }
        else {
            return [System.Collections.Specialized.OrderedDictionary] $link
        }
    }

}





<#
.SYNOPSIS
Sets metadate for the supplied route.

.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.

.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.

.PARAMETER Path
The URI path for the Route.

.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.

.PARAMETER Servers
A list of external endpoint. created with New-PodeOAServerEndpoint

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAExternalRoute -PassThru -Method Get -Path '/peta/:id' -Servers (
    New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
    New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
    ) |
        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   -ComponentSchema 'Pet' -array }) -PassThru |
        Add-PodeOAResponse -Default  -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
.EXAMPLE
    Add-PodeRoute -PassThru -Method Get -Path '/peta/:id'  -ScriptBlock {
            Write-PodeJsonResponse -Value 'done' -StatusCode 200
        } | Add-PodeOAExternalRoute -PassThru   -Servers (
        New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
        New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
        ) |
        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   -ComponentSchema 'Pet' -array }) -PassThru |
        Add-PodeOAResponse -Default  -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAExternalRoute {
    [CmdletBinding(DefaultParameterSetName = 'Pipeline')]
    [OutputType([hashtable[]], ParameterSetName = 'Pipeline')]
    [OutputType([hashtable], ParameterSetName = 'builtin')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Route,

        [Parameter(Mandatory = $true , ParameterSetName = 'BuiltIn')]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [ValidateScript({ $_.Count -gt 0 })]
        [hashtable[]]
        $Servers,

        [Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [switch]
        $PassThru,

        [Parameter( ParameterSetName = 'BuiltIn')]
        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'builtin' {

            # ensure the route has appropriate slashes
            $Path = Update-PodeRouteSlashes -Path $Path
            $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
            $Path = Resolve-PodePlaceholders -Path $Path
            $extRoute = @{
                Method  = $Method.ToLower()
                Path    = $Path
                Local   = $false
                OpenApi = @{
                    Path           = $OpenApiPath
                    Responses      = $null
                    Parameters     = $null
                    RequestBody    = $null
                    callbacks      = [ordered]@{}
                    Authentication = @()
                    Servers        = $Servers
                    DefinitionTag  = $DefinitionTag
                }
            }
            foreach ($tag in $DefinitionTag) {
                #add the default OpenApi responses
                if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) {
                    $extRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
                }
                if (! (Test-PodeOAComponentExternalPath -DefinitionTag $tag -Name $Path)) {
                    $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath[$Path] = @{}
                }

                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.$Path[$Method] = $extRoute
            }

            if ($PassThru) {
                return $extRoute
            }
        }

        'pipeline' {
            if ($null -eq $Route) { throw 'Add-PodeOAExternalRoute - The parameter -Route cannot be NULL.' }
            foreach ($r in @($Route)) {
                $r.OpenApi.Servers = $Servers
            }
            if ($PassThru) {
                return $Route
            }
        }
    }
}



<#
.SYNOPSIS
Creates an OpenAPI Server Object.

.DESCRIPTION
Creates an OpenAPI Server Object to use with Add-PodeOAExternalRoute

.PARAMETER ServerEndpointList
Used for piping

.PARAMETER Url
A URL to the target host.  This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served.
Variable substitutions will be made when a variable is named in `{`brackets`}`.

.PARAMETER Description
An optional string describing the host designated by the URL. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation.


.EXAMPLE
New-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test server'

.EXAMPLE
New-PodeOAServerEndpoint -Url '/api' -Description 'My local server'


}
#>
function New-PodeOAServerEndpoint {
    param (
        [Parameter(ValueFromPipeline = $true , DontShow = $true )]
        [hashtable[]]
        $ServerEndpointList,

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^(https?://|/).+')]
        [string]
        $Url,

        [string]
        $Description
    )
    begin {
        $lUrl = [ordered]@{url = $Url }
        if ($Description) {
            $lUrl.description = $Description
        }
        $collectedInput = [System.Collections.Generic.List[hashtable]]::new()
    }
    process {
        if ($ServerEndpointList) {
            $collectedInput.AddRange($ServerEndpointList)
        }
    }
    end {
        if ($ServerEndpointList) {
            return $collectedInput + $lUrl
        }
        else {
            return $lUrl
        }
    }
}



<#
.SYNOPSIS
Sets metadate for the supplied route.

.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.

.PARAMETER Name
    Alias for 'Name'. A unique identifier for the webhook.
    It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).

.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.

.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.

.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeOAWebhook -PassThru -Method Get    |
        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   -ComponentSchema 'Pet' -array }) -PassThru |
        Add-PodeOAResponse -Default  -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAWebhook {
    param(

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
        [string]
        $Name,

        [Parameter(Mandatory = $true )]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [switch]
        $PassThru,

        [string[]]
        $DefinitionTag
    )

    $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag

    $refRoute = @{
        Method      = $Method.ToLower()
        NotPrepared = $true
        OpenApi     = @{
            Responses      = @{}
            Parameters     = $null
            RequestBody    = $null
            callbacks      = [ordered]@{}
            Authentication = @()
        }
    }
    foreach ($tag in $DefinitionTag) {
        if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag ) {
            throw 'The feature reusable component webhook is not available in OpenAPI v3.0.x'
        }
        $PodeContext.Server.OpenAPI.Definitions[$tag].webhooks[$Name] = $refRoute
    }

    if ($PassThru) {
        return $refRoute
    }
}


<#
.SYNOPSIS
Select a group of OpenAPI Definions for modification.

.DESCRIPTION
Select a group of OpenAPI Definions for modification.

.PARAMETER Tag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
If Tag is empty or null the default Definition is selected

.PARAMETER ScriptBlock
The ScriptBlock that will modified the group.

.EXAMPLE
Select-PodeOADefinition -Tag 'v3', 'v3.1'  -Script {
        New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -Required |
            New-PodeOAIntProperty -Name 'petId' -Format Int64 -Example 198772 -Required |
            New-PodeOAIntProperty -Name 'quantity' -Format Int32 -Example 7 -Required |
            New-PodeOAStringProperty -Name 'shipDate' -Format Date-Time |
            New-PodeOAStringProperty -Name 'status' -Description 'Order Status' -Required -Example 'approved' -Enum @('placed', 'approved', 'delivered') |
            New-PodeOABoolProperty -Name 'complete' |
            New-PodeOAObjectProperty -XmlName 'order' |
            Add-PodeOAComponentSchema -Name 'Order'

New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/xml' -Content 'Pet' |
    Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store'

}
#>
function Select-PodeOADefinition {
    [CmdletBinding()]
    param(
        [string[]]
        $Tag,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Scriptblock


    )

    if (Test-PodeIsEmpty $Scriptblock) {
        throw 'No scriptblock for -Scriptblock passed'
    }
    if (Test-PodeIsEmpty -Value $Tag) {
        $Tag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag
    }
    else {
        $Tag = Test-PodeOADefinitionTag -Tag $Tag
    }
    # check for scoped vars
    $Scriptblock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Scriptblock -PSSession $PSCmdlet.SessionState
    $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Push($PodeContext.Server.OpenAPI.SelectedDefinitionTag)

    $PodeContext.Server.OpenAPI.SelectedDefinitionTag = $Tag

    $null = Invoke-PodeScriptBlock -ScriptBlock $Scriptblock   -UsingVariables $usingVars -Splat
    $PodeContext.Server.OpenAPI.SelectedDefinitionTag = $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Pop()

}








<#
.SYNOPSIS
Check if a Definition exist

.DESCRIPTION
Check if a Definition exist. If the parameter Tag is empty or Null $PodeContext.Server.OpenAPI.SelectedDefinitionTag is returned

.PARAMETER Tag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Test-PodeOADefinitionTag -Tag 'v3', 'v3.1'
#>
function Test-PodeOADefinitionTag {
    param (
        [Parameter(Mandatory = $false)]
        [string[]]
        $Tag
    )

    if ($Tag -and $Tag.Count -gt 0) {
        foreach ($t in $Tag) {
            if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) {
                throw "DefinitionTag $t is not defined"
            }
        }
        return $Tag
    }
    else {
        return $PodeContext.Server.OpenAPI.SelectedDefinitionTag
    }
}



<#
.SYNOPSIS
Validate the OpenAPI definition if all Reference are satisfied

.DESCRIPTION
Validate the OpenAPI definition if all Reference are satisfied



.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
    if ((Test-PodeOADefinition -DefinitionTag 'v3').count -eq 0){
        Write-PodeHost "The OpenAPI definition is valid"
    }
#>
function Test-PodeOADefinition {
    param (
        [string[]]
        $DefinitionTag
    )
    if (! ($DefinitionTag -and $DefinitionTag.Count -gt 0)) {
        $DefinitionTag = $PodeContext.Server.OpenAPI.Definitions.keys
    }

    $result = @{
        valid  = $true
        issues = @{
        }
    }

    foreach ($tag in $DefinitionTag) {
        if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.enabled) {
            if ([string]::IsNullOrWhiteSpace(  $PodeContext.Server.OpenAPI.Definitions[$tag].info.title) -or [string]::IsNullOrWhiteSpace(  $PodeContext.Server.OpenAPI.Definitions[$tag].info.version)) {
                $result.valid = $false
            }
            $result.issues[$tag] = @{
                title      = [string]::IsNullOrWhiteSpace(  $PodeContext.Server.OpenAPI.Definitions[$tag].info.title)
                version    = [string]::IsNullOrWhiteSpace(  $PodeContext.Server.OpenAPI.Definitions[$tag].info.version)
                components = @{}
                definition = ''
            }
            foreach ($field in $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation.keys) {
                foreach ($name in $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field].keys) {
                    if (! (Test-PodeOAComponentInternal -DefinitionTag $tag -Field $field -Name $name)) {
                        $result.issues[$tag].components["#/components/$field/$name"] = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name]
                        $result.valid = $false
                    }
                }
            }
            try {
                Get-PodeOADefinition -DefinitionTag $tag | Out-Null
            }
            catch {
                $result.issues[$tag].definition = $_.Exception.Message
            }
        }
    }
    return  $result
}
src\Public\Responses.ps1
<#
.SYNOPSIS
Attaches a file onto the Response for downloading.

.DESCRIPTION
Attaches a file from the "/public", and static Routes, onto the Response for downloading.
If the supplied path is not in the Static Routes but is a literal/relative path, then this file is used instead.

.PARAMETER Path
The Path to a static file relative to the "/public" directory, or a static Route.
If the supplied Path doesn't match any custom static Route, then Pode will look in the "/public" directory.
Failing this, if the file path exists as a literal/relative file, then this file is used as a fall back.

.PARAMETER ContentType
Manually specify the content type of the response rather than infering it from the attachment's file extension.
The supplied value must match the valid ContentType format, e.g. application/json

.PARAMETER EndpointName
Optional EndpointName that the static route was creating under.

.PARAMETER FileBrowser
If the path is a folder, instead of returning 404, will return A browsable content of the directory.

.EXAMPLE
Set-PodeResponseAttachment -Path 'downloads/installer.exe'

.EXAMPLE
Set-PodeResponseAttachment -Path './image.png'

.EXAMPLE
Set-PodeResponseAttachment -Path 'c:/content/accounts.xlsx'

.EXAMPLE
Set-PodeResponseAttachment -Path './data.txt' -ContentType 'application/json'

.EXAMPLE
Set-PodeResponseAttachment -Path '/assets/data.txt' -EndpointName 'Example'
#>

function Set-PodeResponseAttachment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Path,

        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $FileBrowser

    )

    # already sent? skip
    if ($WebEvent.Response.Sent) {
        return
    }

    # only attach files from public/static-route directories when path is relative
    $route = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName)
    if ($route) {
        $_path = $route.Content.Source

    }
    else {
        $_path = Get-PodeRelativePath -Path $Path -JoinRoot
    }
    #call internal Attachment function
    Write-PodeAttachmentResponseInternal -Path $_path -ContentType $ContentType -FileBrowser:$fileBrowser
}


<#
.SYNOPSIS
Writes a String or a Byte[] to the Response.

.DESCRIPTION
Writes a String or a Byte[] to the Response, as some specified content type. This value can also be cached.

.PARAMETER Value
A String value to write.

.PARAMETER Bytes
An array of Bytes to write.

.PARAMETER ContentType
The content type of the data being written.

.PARAMETER MaxAge
The maximum age to cache the value on the browser, in seconds.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER Cache
Should the value be cached by browsers, or not?

.EXAMPLE
Write-PodeTextResponse -Value 'Leeeeeerrrooooy Jeeeenkiiins!'

.EXAMPLE
Write-PodeTextResponse -Value '{"name": "Rick"}' -ContentType 'application/json'

.EXAMPLE
Write-PodeTextResponse -Bytes (Get-Content -Path ./some/image.png -Raw -AsByteStream) -Cache -MaxAge 1800

.EXAMPLE
Write-PodeTextResponse -Value 'Untitled Text Response' -StatusCode 418
#>
function Write-PodeTextResponse {
    [CmdletBinding(DefaultParameterSetName = 'String')]
    param (
        [Parameter(ParameterSetName = 'String', ValueFromPipeline = $true, Position = 0)]
        [string]
        $Value,

        [Parameter(ParameterSetName = 'Bytes')]
        [byte[]]
        $Bytes,

        [Parameter()]
        [string]
        $ContentType = 'text/plain',

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache
    )

    $isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string')
    $isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes')

    # set the status code of the response, but only if it's not 200 (to prevent overriding)
    if ($StatusCode -ne 200) {
        Set-PodeResponseStatus -Code $StatusCode -NoErrorPage
    }

    # if there's nothing to write, return
    if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) {
        return
    }

    if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) {
        return
    }

    # if the response stream isn't writable or already sent, return
    $res = $WebEvent.Response
    if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite -or $res.Sent))) {
        return
    }

    # set a cache value
    if ($Cache) {
        Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate"
        Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString('r', [CultureInfo]::InvariantCulture))
    }

    # specify the content-type if supplied (adding utf-8 if missing)
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        $charset = 'charset=utf-8'
        if ($ContentType -inotcontains $charset) {
            $ContentType = "$($ContentType); $($charset)"
        }

        $res.ContentType = $ContentType
    }

    # if we're serverless, set the string as the body
    if (!$WebEvent.Streamed) {
        if ($isStringValue) {
            $res.Body = $Value
        }
        else {
            $res.Body = $Bytes
        }
    }

    else {
        # convert string to bytes
        if ($isStringValue) {
            $Bytes = ConvertFrom-PodeValueToBytes -Value $Value
        }

        # check if we only need a range of the bytes
        if (($null -ne $WebEvent.Ranges) -and ($WebEvent.Response.StatusCode -eq 200) -and ($StatusCode -eq 200)) {
            $lengths = @()
            $size = $Bytes.Length

            $Bytes = @(foreach ($range in $WebEvent.Ranges) {
                    # ensure range not invalid
                    if (([int]$range.Start -lt 0) -or ([int]$range.Start -ge $size) -or ([int]$range.End -lt 0)) {
                        Set-PodeResponseStatus -Code 416 -NoErrorPage
                        return
                    }

                    # skip start bytes only
                    if ([string]::IsNullOrWhiteSpace($range.End)) {
                        $Bytes[$range.Start..($size - 1)]
                        $lengths += "$($range.Start)-$($size - 1)/$($size)"
                    }

                    # end bytes only
                    elseif ([string]::IsNullOrWhiteSpace($range.Start)) {
                        if ([int]$range.End -gt $size) {
                            $range.End = $size
                        }

                        if ([int]$range.End -gt 0) {
                            $Bytes[$($size - $range.End)..($size - 1)]
                            $lengths += "$($size - $range.End)-$($size - 1)/$($size)"
                        }
                        else {
                            $lengths += "0-0/$($size)"
                        }
                    }

                    # normal range
                    else {
                        if ([int]$range.End -ge $size) {
                            Set-PodeResponseStatus -Code 416 -NoErrorPage
                            return
                        }

                        $Bytes[$range.Start..$range.End]
                        $lengths += "$($range.Start)-$($range.End)/$($size)"
                    }
                })

            Set-PodeHeader -Name 'Content-Range' -Value "bytes $($lengths -join ', ')"
            if ($StatusCode -eq 200) {
                Set-PodeResponseStatus -Code 206 -NoErrorPage
            }
        }

        # check if we need to compress the response
        if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) {
            try {
                $ms = New-Object -TypeName System.IO.MemoryStream
                $stream = New-Object "System.IO.Compression.$($WebEvent.AcceptEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Compress, $true)
                $stream.Write($Bytes, 0, $Bytes.Length)
                $stream.Close()
                $ms.Position = 0
                $Bytes = $ms.ToArray()
            }
            finally {
                if ($null -ne $stream) {
                    $stream.Close()
                }

                if ($null -ne $ms) {
                    $ms.Close()
                }
            }

            # set content encoding header
            Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding
        }

        # write the content to the response stream
        $res.ContentLength64 = $Bytes.Length

        try {
            $ms = New-Object -TypeName System.IO.MemoryStream
            $ms.Write($Bytes, 0, $Bytes.Length)
            $ms.WriteTo($res.OutputStream)
        }
        catch {
            if ((Test-PodeValidNetworkFailure $_.Exception)) {
                return
            }

            $_ | Write-PodeErrorLog
            throw
        }
        finally {
            if ($null -ne $ms) {
                $ms.Close()
            }
        }
    }
}

<#
.SYNOPSIS
Renders the content of a static, or dynamic, file on the Response.

.DESCRIPTION
Renders the content of a static, or dynamic, file on the Response.
You can set browser's to cache the content, and also override the file's content type.

.PARAMETER Path
The path to a file.

.PARAMETER Data
A HashTable of dynamic data to supply to a dynamic file.

.PARAMETER ContentType
The content type of the file's contents - this overrides the file's extension.

.PARAMETER MaxAge
The maximum age to cache the file's content on the browser, in seconds.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER Cache
Should the file's content be cached by browsers, or not?

.PARAMETER FileBrowser
If the path is a folder, instead of returning 404, will return A browsable content of the directory.

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt'

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -Cache -MaxAge 1800

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -ContentType 'application/json'

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Views/Index.pode' -Data @{ Counter = 2 }

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -StatusCode 201

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/' -FileBrowser
#>
function Write-PodeFileResponse {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $ContentType = $null,

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache,

        [switch]
        $FileBrowser
    )

    # resolve for relative path
    $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot

    Write-PodeFileResponseInternal -Path $RelativePath -Data $Data -ContentType $ContentType -MaxAge $MaxAge `
        -StatusCode $StatusCode -Cache:$Cache -FileBrowser:$FileBrowser
}

<#
.SYNOPSIS
Serves a directory listing as a web page.

.DESCRIPTION
The Write-PodeDirectoryResponse function generates an HTML response that lists the contents of a specified directory,
allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the
display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it
serves the file directly.

.PARAMETER Path
The path to the directory that should be displayed. This path is resolved and used to generate a list of contents.

.EXAMPLE
Write-PodeDirectoryResponse -Path './static'

Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories.
#>
function Write-PodeDirectoryResponse {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [string]
        $Path
    )

    # resolve for relative path
    $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot

    if (Test-Path -Path $RelativePath -PathType Container) {
        Write-PodeDirectoryResponseInternal -Path $RelativePath
    }
    else {
        Set-PodeResponseStatus -Code 404
    }
}
<#
.SYNOPSIS
Writes CSV data to the Response.

.DESCRIPTION
Writes CSV data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a CSV file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeCsvResponse -Value "Name`nRick"

.EXAMPLE
Write-PodeCsvResponse -Value @{ Name = 'Rick' }

.EXAMPLE
Write-PodeCsvResponse -Path 'E:/Files/Names.csv'
#>
function Write-PodeCsvResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                        New-Object psobject -Property $v
                    })

                if (Test-PodeIsPSCore) {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false)
                }
                else {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation)
                }

                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode
}


<#
.SYNOPSIS
Writes HTML data to the Response.

.DESCRIPTION
Writes HTML data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a HTML file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeHtmlResponse -Value "Raw HTML can be placed here"

.EXAMPLE
Write-PodeHtmlResponse -Value @{ Message = 'Hello, all!' }

.EXAMPLE
Write-PodeHtmlResponse -Path 'E:/Site/About.html'
#>
function Write-PodeHtmlResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = ($Value | ConvertTo-Html)
                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode
}


<#
.SYNOPSIS
Writes Markdown data to the Response.

.DESCRIPTION
Writes Markdown data to the Response, with the option to render it as HTML.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a Markdown file.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER AsHtml
If supplied, the Markdown will be converted to HTML. (This is only supported in PS7+)

.EXAMPLE
Write-PodeMarkdownResponse -Value '# Hello, world!' -AsHtml

.EXAMPLE
Write-PodeMarkdownResponse -Path 'E:/Site/About.md'
#>
function Write-PodeMarkdownResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $AsHtml
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    $mimeType = 'text/markdown'

    if ($AsHtml) {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $mimeType = 'text/html'
            $Value = ($Value | ConvertFrom-Markdown).Html
        }
    }

    Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes JSON data to the Response.

.DESCRIPTION
Writes JSON data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.

.PARAMETER Path
The path to a JSON file.

.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER NoCompress
The JSON document is not compressed (Human readable form)

.EXAMPLE
Write-PodeJsonResponse -Value '{"name": "Rick"}'

.EXAMPLE
Write-PodeJsonResponse -Value @{ Name = 'Rick' } -StatusCode 201

.EXAMPLE
Write-PodeJsonResponse -Path 'E:/Files/Names.json'
#>
function Write-PodeJsonResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter(ParameterSetName = 'Value')]
        [ValidateRange(0, 100)]
        [int]
        $Depth = 10,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [Parameter(ParameterSetName = 'Value')]
        [switch]
        $NoCompress

    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
            if ([string]::IsNullOrWhiteSpace($Value)) {
                $Value = '{}'
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                if ($Depth -le 0) {
                    $Value = (ConvertTo-Json -InputObject $Value -Compress:(!$NoCompress))
                }
                else {
                    $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress:(!$NoCompress))
                }
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = '{}'
    }

    Write-PodeTextResponse -Value $Value -ContentType 'application/json' -StatusCode $StatusCode
}


<#
.SYNOPSIS
Writes XML data to the Response.

.DESCRIPTION
Writes XML data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to an XML file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeXmlResponse -Value '<root><name>Rick</name></root>'

.EXAMPLE
Write-PodeXmlResponse -Value @{ Name = 'Rick' } -StatusCode 201

.EXAMPLE
Write-PodeXmlResponse -Path 'E:/Files/Names.xml'
#>
function Write-PodeXmlResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                        New-Object psobject -Property $v
                    })

                $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/xml' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes YAML data to the Response.

.DESCRIPTION
Writes YAML data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to YAML.

.PARAMETER Path
The path to a YAML file.

.PARAMETER ContentType
Because JSON content has not yet an official content type. one custom can be specified here (Default: 'application/x-yaml' )

.PARAMETER Depth
The Depth to generate the YAML document - the larger this value the worse performance gets.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeYamlResponse -Value '{"name": "Rick"}'

.EXAMPLE
Write-PodeYamlResponse -Value @{ Name = 'Rick' } -StatusCode 201

.EXAMPLE
Write-PodeYamlResponse -Path 'E:/Files/Names.json'
#>
function Write-PodeYamlResponse {
    [CmdletBinding(DefaultParameterSetName = 'Value')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $Path,

        [Parameter()]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [ValidateNotNullOrEmpty()]
        [string]
        $ContentType = 'application/x-yaml',


        [Parameter(ParameterSetName = 'Value')]
        [ValidateRange(0, 100)]
        [int]
        $Depth = 10,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                if ( $Depth -gt 0) {
                    $Value = ConvertTo-PodeYaml -InputObject $Value -Depth $Depth
                }
                else {
                    $Value = ConvertTo-PodeYaml -InputObject $Value
                }
            }
        }
    }
    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = '[]'
    }

    Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode

}



<#
.SYNOPSIS
Renders a dynamic, or static, View on the Response.

.DESCRIPTION
Renders a dynamic, or static, View on the Response; allowing for dynamic data to be supplied.

.PARAMETER Path
The path to a View, relative to the "/views" directory. (Extension is optional).

.PARAMETER Data
Any dynamic data to supply to a dynamic View.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER Folder
If supplied, a custom views folder will be used.

.PARAMETER FlashMessages
Automatically supply all Flash messages in the current session to the View.

.EXAMPLE
Write-PodeViewResponse -Path 'index'

.EXAMPLE
Write-PodeViewResponse -Path 'accounts/profile_page' -Data @{ Username = 'Morty' }

.EXAMPLE
Write-PodeViewResponse -Path 'login' -FlashMessages
#>
function Write-PodeViewResponse {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data = @{},

        [Parameter()]
        [int]
        $StatusCode = 200,

        [Parameter()]
        [string]
        $Folder,

        [switch]
        $FlashMessages
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }

    # add path to data as "pagename" - unless key already exists
    if (!$Data.ContainsKey('pagename')) {
        $Data['pagename'] = $Path
    }

    # load all flash messages if needed
    if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) {
        $Data['flash'] = @{}

        foreach ($name in (Get-PodeFlashMessageNames)) {
            $Data.flash[$name] = (Get-PodeFlashMessage -Name $name)
        }
    }
    elseif ($null -eq $Data['flash']) {
        $Data['flash'] = @{}
    }

    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directories
    $viewFolder = $PodeContext.Server.InbuiltDrives['views']
    if (![string]::IsNullOrWhiteSpace($Folder)) {
        $viewFolder = $PodeContext.Server.Views[$Folder]
    }

    $Path = [System.IO.Path]::Combine($viewFolder, $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path)) {
        return
    }

    # run any engine logic and render it
    $engine = (Get-PodeViewEngineType -Path $Path)
    $value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)

    switch ($engine.ToLowerInvariant()) {
        'md' {
            Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml
        }

        default {
            Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode
        }
    }
}


<#
.SYNOPSIS
Sets the Status Code of the Response, and controls rendering error pages.

.DESCRIPTION
Sets the Status Code of the Response, and controls rendering error pages.

.PARAMETER Code
The Status Code to set on the Response.

.PARAMETER Description
An optional Status Description.

.PARAMETER Exception
An exception to use when detailing error information on error pages.

.PARAMETER ContentType
The content type of the error page to use.

.PARAMETER NoErrorPage
Don't render an error page when the Status Code is 400+.

.EXAMPLE
Set-PodeResponseStatus -Code 404

.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception

.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception -ContentType 'application/json'
#>
function Set-PodeResponseStatus {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [int]
        $Code,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        $Exception,

        [Parameter()]
        [string]
        $ContentType = $null,

        [switch]
        $NoErrorPage
    )

    # already sent? skip
    if ($WebEvent.Response.Sent) {
        return
    }

    # set the code
    $WebEvent.Response.StatusCode = $Code

    # set an appropriate description (mapping if supplied is blank)
    if ([string]::IsNullOrWhiteSpace($Description)) {
        $Description = (Get-PodeStatusDescription -StatusCode $Code)
    }

    if (!$PodeContext.Server.IsServerless -and ![string]::IsNullOrWhiteSpace($Description)) {
        $WebEvent.Response.StatusDescription = $Description
    }

    # if the status code is >=400 then attempt to load error page
    if (!$NoErrorPage -and ($Code -ge 400)) {
        Show-PodeErrorPage -Code $Code -Description $Description -Exception $Exception -ContentType $ContentType
    }
}

<#
.SYNOPSIS
Redirecting a user to a new URL.

.DESCRIPTION
Redirecting a user to a new URL, or the same URL as the Request but a different Protocol - or other components.

.PARAMETER Url
Redirect the user to a new URL, or a relative path.

.PARAMETER EndpointName
The Name of an Endpoint to redirect to.

.PARAMETER Port
Change the port of the current Request before redirecting.

.PARAMETER Protocol
Change the protocol of the current Request before redirecting.

.PARAMETER Address
Change the domain address of the current Request before redirecting.

.PARAMETER Moved
Set the Status Code as "301 Moved", rather than "302 Redirect".

.EXAMPLE
Move-PodeResponseUrl -Url 'https://google.com'

.EXAMPLE
Move-PodeResponseUrl -Url '/about'

.EXAMPLE
Move-PodeResponseUrl -Protocol HTTPS

.EXAMPLE
Move-PodeResponseUrl -Port 9000 -Moved
#>
function Move-PodeResponseUrl {
    [CmdletBinding(DefaultParameterSetName = 'Url')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Url')]
        [string]
        $Url,

        [Parameter(ParameterSetName = 'Endpoint')]
        [string]
        $EndpointName,

        [Parameter(ParameterSetName = 'Components')]
        [int]
        $Port = 0,

        [Parameter(ParameterSetName = 'Components')]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter(ParameterSetName = 'Components')]
        [string]
        $Address,

        [switch]
        $Moved
    )

    # build the url
    if ($PSCmdlet.ParameterSetName -ieq 'components') {
        $uri = $WebEvent.Request.Url

        # set the protocol
        $Protocol = $Protocol.ToLowerInvariant()
        if ([string]::IsNullOrWhiteSpace($Protocol)) {
            $Protocol = $uri.Scheme
        }

        # set the domain
        if ([string]::IsNullOrWhiteSpace($Address)) {
            $Address = $uri.Host
        }

        # set the port
        if ($Port -le 0) {
            $Port = $uri.Port
        }

        $PortStr = [string]::Empty
        if (@(80, 443) -notcontains $Port) {
            $PortStr = ":$($Port)"
        }

        # combine to form the url
        $Url = "$($Protocol)://$($Address)$($PortStr)$($uri.PathAndQuery)"
    }

    # build the url from an endpoint
    elseif ($PSCmdlet.ParameterSetName -ieq 'endpoint') {
        $endpoint = Get-PodeEndpointByName -Name $EndpointName -ThrowError

        # set the port
        $PortStr = [string]::Empty
        if (@(80, 443) -notcontains $endpoint.Port) {
            $PortStr = ":$($endpoint.Port)"
        }

        $Url = "$($endpoint.Protocol)://$($endpoint.FriendlyName)$($PortStr)$($WebEvent.Request.Url.PathAndQuery)"
    }

    Set-PodeHeader -Name 'Location' -Value $Url

    if ($Moved) {
        Set-PodeResponseStatus -Code 301 -Description 'Moved'
    }
    else {
        Set-PodeResponseStatus -Code 302 -Description 'Redirect'
    }
}

<#
.SYNOPSIS
Writes data to a TCP socket stream.

.DESCRIPTION
Writes data to a TCP socket stream.

.PARAMETER Message
The message to write

.EXAMPLE
Write-PodeTcpClient -Message '250 OK'
#>
function Write-PodeTcpClient {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string]
        $Message
    )

    $TcpEvent.Response.WriteLine($Message, $true)
}

<#
.SYNOPSIS
Reads data from a TCP socket stream.

.DESCRIPTION
Reads data from a TCP socket stream.

.PARAMETER Timeout
An optional Timeout in milliseconds.

.PARAMETER CheckBytes
An optional array of bytes to check at the end of a receievd data stream, to determine if the data is complete.

.PARAMETER CRLFMessageEnd
If supplied, the CheckBytes will be set to 13 and 10 to make sure a message ends with CR and LF.

.EXAMPLE
$data = Read-PodeTcpClient

.EXAMPLE
$data = Read-PodeTcpClient -CRLFMessageEnd
#>
function Read-PodeTcpClient {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    [OutputType([string])]
    param(
        [Parameter()]
        [int]
        $Timeout = 0,

        [Parameter(ParameterSetName = 'CheckBytes')]
        [byte[]]
        $CheckBytes = $null,

        [Parameter(ParameterSetName = 'CRLF')]
        [switch]
        $CRLFMessageEnd
    )

    $cBytes = $CheckBytes
    if ($CRLFMessageEnd) {
        $cBytes = [byte[]]@(13, 10)
    }

    return (Wait-PodeTask -Task $TcpEvent.Request.Read($cBytes, $PodeContext.Tokens.Cancellation.Token) -Timeout $Timeout)
}

<#
.SYNOPSIS
Close an open TCP client connection

.DESCRIPTION
Close an open TCP client connection

.EXAMPLE
Close-PodeTcpClient
#>
function Close-PodeTcpClient {
    [CmdletBinding()]
    param()

    $TcpEvent.Request.Close()
}

<#
.SYNOPSIS
Saves any uploaded files on the Request to the File System.

.DESCRIPTION
Saves any uploaded files on the Request to the File System.

.PARAMETER Key
The name of the key within the $WebEvent's Data HashTable that stores the file names.

.PARAMETER Path
The path to save files. If this is a directory then the file name of the uploaded file will be used, but if this is a file path then that name is used instead.
If the Request has multiple files in, and you specify a file path, then all files will be saved to that one file path - overwriting each other.

.PARAMETER FileName
An optional FileName to save a specific files if multiple files were supplied in the Request. By default, every file is saved.

.EXAMPLE
Save-PodeRequestFile -Key 'avatar'

.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images'

.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images' -FileName 'icon.png'
#>
function Save-PodeRequestFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Path = '.',

        [Parameter()]
        [string[]]
        $FileName
    )

    # if path is '.', replace with server root
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # ensure the parameter name exists in data
    if (!(Test-PodeRequestFile -Key $Key)) {
        throw "A parameter called '$($Key)' was not supplied in the request, or has no data available"
    }

    # get the file names
    $files = @($WebEvent.Data[$Key])
    if (($null -ne $FileName) -and ($FileName.Length -gt 0)) {
        $files = @(foreach ($file in $files) {
                if ($FileName -icontains $file) {
                    $file
                }
            })
    }

    # ensure the file data exists
    foreach ($file in $files) {
        if (!$WebEvent.Files.ContainsKey($file)) {
            throw "No data for file '$($file)' was uploaded in the request"
        }
    }

    # save the files
    foreach ($file in $files) {
        # if the path is a directory, add the filename
        $filePath = $Path
        if (Test-Path -Path $filePath -PathType Container) {
            $filePath = [System.IO.Path]::Combine($filePath, $file)
        }

        # save the file
        $WebEvent.Files[$file].Save($filePath)
    }
}

<#
.SYNOPSIS
Test to see if the Request contains the key for any uploaded files.

.DESCRIPTION
Test to see if the Request contains the key for any uploaded files.

.PARAMETER Key
The name of the key within the $WebEvent's Data HashTable that stores the file names.

.PARAMETER FileName
An optional FileName to test for a specific file within the list of uploaded files.

.EXAMPLE
Test-PodeRequestFile -Key 'avatar'

.EXAMPLE
Test-PodeRequestFile -Key 'avatar' -FileName 'icon.png'
#>
function Test-PodeRequestFile {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $FileName
    )

    # ensure the parameter name exists in data
    if (!$WebEvent.Data.ContainsKey($Key)) {
        return $false
    }

    # ensure it has filenames
    if ([string]::IsNullOrEmpty($WebEvent.Data[$Key])) {
        return $false
    }

    # do we have any specific files?
    if (![string]::IsNullOrEmpty($FileName)) {
        return (@($WebEvent.Data[$Key]) -icontains $FileName)
    }

    # we have files
    return $true
}

<#
.SYNOPSIS
Short description

.DESCRIPTION
Long description

.PARAMETER Type
The type name of the view engine (inbuilt types are: Pode and HTML).

.PARAMETER ScriptBlock
A ScriptBlock for specifying custom view engine rendering rules.

.PARAMETER Extension
A custom extension for the engine's files.

.EXAMPLE
Set-PodeViewEngine -Type HTML

.EXAMPLE
Set-PodeViewEngine -Type Markdown

.EXAMPLE
Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { param($path, $data) /* logic */ }
#>
function Set-PodeViewEngine {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Type,

        [Parameter()]
        [scriptblock]
        $ScriptBlock = $null,

        [Parameter()]
        [string]
        $Extension
    )

    # truncate markdown
    if ($Type -ieq 'Markdown') {
        $Type = 'md'
    }

    # override extension with type
    if ([string]::IsNullOrWhiteSpace($Extension)) {
        $Extension = $Type
    }

    # check if the scriptblock has any using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    }

    # setup view engine config
    $PodeContext.Server.ViewEngine.Type = $Type.ToLowerInvariant()
    $PodeContext.Server.ViewEngine.Extension = $Extension.ToLowerInvariant()
    $PodeContext.Server.ViewEngine.ScriptBlock = $ScriptBlock
    $PodeContext.Server.ViewEngine.UsingVariables = $usingVars
    $PodeContext.Server.ViewEngine.IsDynamic = (@('html', 'md') -inotcontains $Type)
}

<#
.SYNOPSIS
Includes the contents of a partial View into another dynamic View.

.DESCRIPTION
Includes the contents of a partial View into another dynamic View. The partial View can be static or dynamic.

.PARAMETER Path
The path to a partial View, relative to the "/views" directory. (Extension is optional).

.PARAMETER Data
Any dynamic data to supply to a dynamic partial View.

.PARAMETER Folder
If supplied, a custom views folder will be used.

.EXAMPLE
Use-PodePartialView -Path 'shared/footer'
#>
function Use-PodePartialView {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $Folder
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }
    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $viewFolder = $PodeContext.Server.InbuiltDrives['views']
    if (![string]::IsNullOrWhiteSpace($Folder)) {
        $viewFolder = $PodeContext.Server.Views[$Folder]
    }

    $Path = [System.IO.Path]::Combine($viewFolder, $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "File not found at path: $($Path)"
    }

    # run any engine logic
    return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
}

<#
.SYNOPSIS
Broadcasts a message to connected WebSocket clients.

.DESCRIPTION
Broadcasts a message to all, or some, connected WebSocket clients. You can specify a path to send messages to, or a specific ClientId.

.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.

.PARAMETER Path
The Path of connected clients to send the message.

.PARAMETER ClientId
A specific ClientId of a connected client to send a message. Not currently used.

.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.

.PARAMETER Mode
The Mode to broadcast a message: Auto, Broadcast, Direct. (Default: Auto)

.PARAMETER IgnoreEvent
If supplied, if a SignalEvent is available it's data, such as path/clientId, will be ignored.

.EXAMPLE
Send-PodeSignal -Value @{ Message = 'Hello, world!' }

.EXAMPLE
Send-PodeSignal -Value @{ Data = @(123, 100, 101) } -Path '/response-charts'
#>
function Send-PodeSignal {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        $Value,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $ClientId,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [ValidateSet('Auto', 'Broadcast', 'Direct')]
        [string]
        $Mode = 'Auto',

        [switch]
        $IgnoreEvent
    )
    # error if not configured
    if (!$PodeContext.Server.Signals.Enabled) {
        throw 'WebSockets have not been configured to send signal messages'
    }

    # do nothing if no value
    if (($null -eq $Value) -or ([string]::IsNullOrEmpty($Value))) {
        return
    }

    # jsonify the value
    if ($Value -isnot [string]) {
        if ($Depth -le 0) {
            $Value = (ConvertTo-Json -InputObject $Value -Compress)
        }
        else {
            $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress)
        }
    }

    # check signal event
    if (!$IgnoreEvent -and ($null -ne $SignalEvent)) {
        if ([string]::IsNullOrWhiteSpace($Path)) {
            $Path = $SignalEvent.Data.Path
        }

        if ([string]::IsNullOrWhiteSpace($ClientId)) {
            $ClientId = $SignalEvent.Data.ClientId
        }

        if (($Mode -ieq 'Auto') -and ($SignalEvent.Data.Direct -or ($SignalEvent.ClientId -ieq $SignalEvent.Data.ClientId))) {
            $Mode = 'Direct'
        }
    }

    # broadcast or direct?
    if ($Mode -iin @('Auto', 'Broadcast')) {
        $PodeContext.Server.Signals.Listener.AddServerSignal($Value, $Path, $ClientId)
    }
    else {
        $SignalEvent.Response.Write($Value)
    }
}

<#
.SYNOPSIS
Add a custom path that contains additional views.

.DESCRIPTION
Add a custom path that contains additional views.

.PARAMETER Name
The Name of the views folder.

.PARAMETER Source
The literal, or relative, path to the directory that contains views.

.EXAMPLE
Add-PodeViewFolder -Name 'assets' -Source './assets'
#>
function Add-PodeViewFolder {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Source
    )

    # ensure the folder doesn't already exist
    if ($PodeContext.Server.Views.ContainsKey($Name)) {
        throw "The Views folder name already exists: $($Name)"
    }

    # ensure the path exists at server root
    $Source = Get-PodeRelativePath -Path $Source -JoinRoot
    if (!(Test-PodePath -Path $Source -NoStatus)) {
        throw "The Views path does not exist: $($Source)"
    }

    # setup a temp drive for the path
    $Source = New-PodePSDrive -Path $Source

    # add the route(s)
    Write-Verbose "Adding View Folder: [$($Name)] $($Source)"
    $PodeContext.Server.Views[$Name] = $Source
}

<#
.SYNOPSIS
Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing.

.DESCRIPTION
Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing.

.EXAMPLE
Send-PodeResponse
#>
function Send-PodeResponse {
    [CmdletBinding()]
    param()

    if ($null -ne $WebEvent.Response) {
        $WebEvent.Response.Send()
    }
}
src\Public\Routes.ps1
<#
.SYNOPSIS
Adds a Route for a specific HTTP Method(s).

.DESCRIPTION
Adds a Route for a specific HTTP Method(s), with path, that when called with invoke any logic and/or Middleware.

.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.

.PARAMETER Path
The URI path for the Route.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.

.PARAMETER ScriptBlock
A ScriptBlock for the Route's main logic.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Route should be bound against.

.PARAMETER ContentType
The content type the Route should use when parsing any payloads.

.PARAMETER TransferEncoding
The transfer encoding the Route should use when parsing any payloads.

.PARAMETER ErrorContentType
The content type of any error pages that may get returned.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Route's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Route's ScriptBlock.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.

.PARAMETER Login
If supplied, the Route will be flagged to Authentication as being a Route that handles user logins.

.PARAMETER Logout
If supplied, the Route will be flagged to Authentication as being a Route that handles users logging out.

.PARAMETER PassThru
If supplied, the route created will be returned so it can be passed through a pipe.

.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER OAResponses
An alternative way to associate OpenApi responses unsing New-PodeOAResponse instead of piping multiple Add-PodeOAResponse

.PARAMETER OAReference
A reference to OpenAPI reusable pathItem component created with Add-PodeOAComponentPathItem

.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -TransferEncoding gzip -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -Role 'Developer', 'QA' -ScriptBlock { /* logic */ }

.EXAMPLE
$Responses = New-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' |
            New-PodeOAResponse -StatusCode 404 -Description 'User not found' |
            New-PodeOAResponse -StatusCode 405 -Description 'Invalid Input'

Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -OAResponses $Responses -ScriptBlock {
            #code is going here
        }
#>
function Add-PodeRoute {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string[]]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter( )]
        [AllowNull()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $Login,

        [switch]
        $Logout,

        [hashtable]
        $OAResponses,

        [string]
        $OAReference,

        [switch]
        $PassThru,

        [string[]]
        $OADefinitionTag
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }

        if ($null -ne $RouteGroup.OADefinitionTag ) {
            $OADefinitionTag = $RouteGroup.OADefinitionTag
        }
    }

    # var for new routes created
    $newRoutes = @()

    # store the original path
    $origPath = $Path

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw 'No Path supplied for Route'
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path
    $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
    $Path = Resolve-PodePlaceholders -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoints -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # if middleware, scriptblock and file path are all null/empty, error
    if ((Test-PodeIsEmpty $Middleware) -and (Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and (Test-PodeIsEmpty $Authentication)) {
        throw "No logic passed for Route: $($Path)"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # convert any middleware into valid hashtables
    $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)

    # if an access name was supplied, setup access as middleware first to it's after auth middleware
    if (![string]::IsNullOrWhiteSpace($Access)) {
        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            throw 'Access requires Authentication to be supplied on Routes'
        }

        if (!(Test-PodeAccessExists -Name $Access)) {
            throw "Access method does not exist: $($Access)"
        }

        $options = @{
            Name = $Access
        }

        $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # if an auth name was supplied, setup the auth as the first middleware
    if (![string]::IsNullOrWhiteSpace($Authentication)) {
        if (!(Test-PodeAuthExists -Name $Authentication)) {
            throw "Authentication method does not exist: $($Authentication)"
        }

        $options = @{
            Name   = $Authentication
            Login  = $Login
            Logout = $Logout
            Anon   = $AllowAnon
        }

        $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # custom access
    if ($null -eq $CustomAccess) {
        $CustomAccess = @{}
    }

    # workout a default content type for the route
    $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType

    # workout a default transfer encoding for the route
    $TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding

    # loop through each method
    foreach ($_method in $Method) {
        # ensure the route doesn't already exist for each endpoint
        $endpoints = @(foreach ($_endpoint in $endpoints) {
                $found = Test-PodeRouteInternal -Method $_method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

                if ($found) {
                    if ($IfExists -ieq 'Overwrite') {
                        Remove-PodeRoute -Method $_method -Path $origPath -EndpointName $_endpoint.Name
                    }

                    if ($IfExists -ieq 'Skip') {
                        continue
                    }
                }

                $_endpoint
            })

        if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
            continue
        }

        #add security header method if autoMethods is enabled
        if (  $PodeContext.Server.Security.autoMethods ) {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value $_method.ToUpper() -Append
        }

        $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag

        #add the default OpenApi responses
        if ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses) {
            $DefaultResponse = @{}
            foreach ($tag in $DefinitionTag) {
                $DefaultResponse[$tag] = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
            }
        }

        # add the route(s)
        Write-Verbose "Adding Route: [$($_method)] $($Path)"
        $methodRoutes = @(foreach ($_endpoint in $endpoints) {
                @{
                    Logic            = $ScriptBlock
                    UsingVariables   = $usingVars
                    Middleware       = $Middleware
                    Authentication   = $Authentication
                    Access           = $Access
                    AccessMeta       = @{
                        Role   = $Role
                        Group  = $Group
                        Scope  = $Scope
                        User   = $User
                        Custom = $CustomAccess
                    }
                    Endpoint         = @{
                        Protocol = $_endpoint.Protocol
                        Address  = $_endpoint.Address.Trim()
                        Name     = $_endpoint.Name
                    }
                    ContentType      = $ContentType
                    TransferEncoding = $TransferEncoding
                    ErrorType        = $ErrorContentType
                    Arguments        = $ArgumentList
                    Method           = $_method
                    Path             = $Path
                    OpenApi          = @{
                        Path           = $OpenApiPath
                        Responses      = $DefaultResponse
                        Parameters     = $null
                        RequestBody    = $null
                        CallBacks      = @{}
                        Authentication = @()
                        Servers        = @()
                        DefinitionTag  = $DefinitionTag
                    }
                    IsStatic         = $false
                    Metrics          = @{
                        Requests = @{
                            Total       = 0
                            StatusCodes = @{}
                        }
                    }
                }
            })

        if (![string]::IsNullOrWhiteSpace($Authentication)) {
            Set-PodeOAAuth -Route $methodRoutes -Name $Authentication -AllowAnon:$AllowAnon
        }

        $PodeContext.Server.Routes[$_method][$Path] += @($methodRoutes)
        if ($PassThru) {
            $newRoutes += $methodRoutes
        }
    }
    if ($OAReference) {
        Test-PodeOAComponentInternal -Field pathItems -DefinitionTag $DefinitionTag -Name $OAReference -PostValidation
        foreach ($r in @($newRoutes)) {
            $r.OpenApi = @{
                '$ref'        = "#/components/paths/$OAReference"
                DefinitionTag = $DefinitionTag
                Path          = $OpenApiPath
            }
        }
    }
    elseif ($OAResponses) {
        foreach ($r in @($newRoutes)) {
            $r.OpenApi.Responses = $OAResponses
        }
    }

    # return the routes?
    if ($PassThru) {
        return $newRoutes
    }
}

<#
.SYNOPSIS
Add a static Route for rendering static content.

.DESCRIPTION
Add a static Route for rendering static content. You can also define default pages to display.

.PARAMETER Path
The URI path for the static Route.

.PARAMETER Source
The literal, or relative, path to the directory that contains the static content.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.

.PARAMETER ContentType
The content type the static Route should use when parsing any payloads.

.PARAMETER TransferEncoding
The transfer encoding the static Route should use when parsing any payloads.

.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.

.PARAMETER ErrorContentType
The content type of any error pages that may get returned.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER AllowAnon
If supplied, the static route will allow anonymous access for non-authenticated users.

.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.

.PARAMETER PassThru
If supplied, the static route created will be returned so it can be passed through a pipe.

.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER FileBrowser
If supplied, when the path is a folder, instead of returning 404, will return A browsable content of the directory.

.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.

.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets'

.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html')

.EXAMPLE
Add-PodeStaticRoute -Path '/installers' -Source './exes' -DownloadOnly

.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -RedirectToDefault
#>
function Add-PodeStaticRoute {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [string]
        $Source,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string[]]
        $Defaults,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $DownloadOnly,

        [switch]
        $FileBrowser,

        [switch]
        $PassThru,

        [switch]
        $RedirectToDefault
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
            $Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if (Test-PodeIsEmpty $Defaults) {
            $Defaults = $RouteGroup.Defaults
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.DownloadOnly) {
            $DownloadOnly = $RouteGroup.DownloadOnly
        }

        if ($RouteGroup.FileBrowser) {
            $FileBrowser = $RouteGroup.FileBrowser
        }

        if ($RouteGroup.RedirectToDefault) {
            $RedirectToDefault = $RouteGroup.RedirectToDefault
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }
    }

    # store the route method
    $Method = 'Static'

    # store the original path
    $origPath = $Path

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "[$($Method)]: No Path supplied for Static Route"
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path -Static
    $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
    $Path = Resolve-PodePlaceholders -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoints -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # ensure the route doesn't already exist for each endpoint
    $endpoints = @(foreach ($_endpoint in $endpoints) {
            $found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

            if ($found) {
                if ($IfExists -ieq 'Overwrite') {
                    Remove-PodeStaticRoute -Path $origPath -EndpointName $_endpoint.Name
                }

                if ($IfExists -ieq 'Skip') {
                    continue
                }
            }

            $_endpoint
        })

    if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
        return
    }

    # if static, ensure the path exists at server root
    $Source = Get-PodeRelativePath -Path $Source -JoinRoot
    if (!(Test-PodePath -Path $Source -NoStatus)) {
        throw "[$($Method))] $($Path): The Source path supplied for Static Route does not exist: $($Source)"
    }

    # setup a temp drive for the path
    $Source = New-PodePSDrive -Path $Source

    # setup default static files
    if ($null -eq $Defaults) {
        $Defaults = Get-PodeStaticRouteDefault
    }

    if (!$RedirectToDefault) {
        $RedirectToDefault = $PodeContext.Server.Web.Static.RedirectToDefault
    }

    # convert any middleware into valid hashtables
    $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)

    # if an access name was supplied, setup access as middleware first to it's after auth middleware
    if (![string]::IsNullOrWhiteSpace($Access)) {
        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            throw 'Access requires Authentication to be supplied on Static Routes'
        }

        if (!(Test-PodeAccessExists -Name $Access)) {
            throw "Access method does not exist: $($Access)"
        }

        $options = @{
            Name = $Access
        }

        $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # if an auth name was supplied, setup the auth as the first middleware
    if (![string]::IsNullOrWhiteSpace($Authentication)) {
        if (!(Test-PodeAuthExists -Name $Authentication)) {
            throw "Authentication method does not exist: $($Authentication)"
        }

        $options = @{
            Name = $Authentication
            Anon = $AllowAnon
        }

        $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
    }

    # workout a default content type for the route
    $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType

    # workout a default transfer encoding for the route
    $TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding

    #The path use KleeneStar(Asterisk)
    $KleeneStar = $OrigPath.Contains('*')

    # add the route(s)
    Write-Verbose "Adding Route: [$($Method)] $($Path)"
    $newRoutes = @(foreach ($_endpoint in $endpoints) {
            @{
                Source            = $Source
                Path              = $Path
                KleeneStar        = $KleeneStar
                Method            = $Method
                Defaults          = $Defaults
                RedirectToDefault = $RedirectToDefault
                Middleware        = $Middleware
                Authentication    = $Authentication
                Access            = $Access
                AccessMeta        = @{
                    Role   = $Role
                    Group  = $Group
                    Scope  = $Scope
                    User   = $User
                    Custom = $CustomAccess
                }
                Endpoint          = @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.Address.Trim()
                    Name     = $_endpoint.Name
                }
                ContentType       = $ContentType
                TransferEncoding  = $TransferEncoding
                ErrorType         = $ErrorContentType
                Download          = $DownloadOnly
                IsStatic          = $true
                FileBrowser       = $FileBrowser.isPresent
                OpenApi           = @{
                    Path           = $OpenApiPath
                    Responses      = @{}
                    Parameters     = $null
                    RequestBody    = $null
                    CallBacks      = @{}
                    Authentication = @()
                    Servers        = @()
                    DefinitionTag  = $DefinitionTag
                }
                Metrics           = @{
                    Requests = @{
                        Total       = 0
                        StatusCodes = @{}
                    }
                }
            }
        })

    $PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)

    # return the routes?
    if ($PassThru) {
        return $newRoutes
    }
}

<#
.SYNOPSIS
Adds a Signal Route for WebSockets.

.DESCRIPTION
Adds a Signal Route, with path, that when called with invoke any logic.

.PARAMETER Path
The URI path for the Signal Route.

.PARAMETER ScriptBlock
A ScriptBlock for the Signal Route's main logic.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Signal Route should be bound against.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Signal Route's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Signal Route's ScriptBlock.

.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)

.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeSignalRoute {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    # check if we have any route group info defined
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }
    }

    $Method = 'Signal'

    # store the original path
    $origPath = $Path

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path

    # get endpoints from name
    $endpoints = Find-PodeEndpoints -EndpointName $EndpointName

    # get default route IfExists state
    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    # ensure the route doesn't already exist for each endpoint
    $endpoints = @(foreach ($_endpoint in $endpoints) {
            $found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')

            if ($found) {
                if ($IfExists -ieq 'Overwrite') {
                    Remove-PodeSignalRoute -Path $origPath -EndpointName $_endpoint.Name
                }

                if ($IfExists -ieq 'Skip') {
                    continue
                }
            }

            $_endpoint
        })

    if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
        return
    }

    # if scriptblock and file path are all null/empty, error
    if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath)) {
        throw "[$($Method)] $($Path): No logic passed"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the route(s)
    Write-Verbose "Adding Route: [$($Method)] $($Path)"
    $newRoutes = @(foreach ($_endpoint in $endpoints) {
            @{
                Logic          = $ScriptBlock
                UsingVariables = $usingVars
                Endpoint       = @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.Address.Trim()
                    Name     = $_endpoint.Name
                }
                Arguments      = $ArgumentList
                Method         = $Method
                Path           = $Path
                IsStatic       = $false
                Metrics        = @{
                    Requests = @{
                        Total = 0
                    }
                }
            }
        })

    $PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)
}

<#
.SYNOPSIS
Add a Route Group for multiple Routes.

.DESCRIPTION
Add a Route Group for sharing values between multiple Routes.

.PARAMETER Path
The URI path to use as a base for the Routes, that should be prepended.

.PARAMETER Routes
A ScriptBlock for adding Routes.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Route.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Routes.

.PARAMETER ContentType
The content type to use for the Routes, when parsing any payloads.

.PARAMETER TransferEncoding
The transfer encoding to use for the Routes, when parsing any payloads.

.PARAMETER ErrorContentType
The content type of any error pages that may get returned.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Routes.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER AllowAnon
If supplied, the Routes will allow anonymous access for non-authenticated users.

.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.

.EXAMPLE
Add-PodeRouteGroup -Path '/api' -Routes { Add-PodeRoute -Path '/route1' -Etc }
#>
function Add-PodeRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [string[]]
        $OADefinitionTag
    )

    if (Test-PodeIsEmpty $Routes) {
        throw 'No scriptblock for -Routes passed'
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }

        if ($null -ne $RouteGroup.OADefinitionTag ) {
            $OADefinitionTag = $RouteGroup.OADefinitionTag
        }

    }

    $RouteGroup = @{
        Path             = $Path
        Middleware       = $Middleware
        EndpointName     = $EndpointName
        ContentType      = $ContentType
        TransferEncoding = $TransferEncoding
        ErrorContentType = $ErrorContentType
        Authentication   = $Authentication
        Access           = $Access
        AllowAnon        = $AllowAnon
        IfExists         = $IfExists
        OADefinitionTag  = $OADefinitionTag
        AccessMeta       = @{
            Role   = $Role
            Group  = $Group
            Scope  = $Scope
            User   = $User
            Custom = $CustomAccess
        }
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}

<#
.SYNOPSIS
Add a Static Route Group for multiple Static Routes.

.DESCRIPTION
Add a Static Route Group for sharing values between multiple Static Routes.

.PARAMETER Path
The URI path to use as a base for the Static Routes.

.PARAMETER Source
A literal, or relative, base path to the directory that contains the static content, that should be prepended.

.PARAMETER Routes
A ScriptBlock for adding Static Routes.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Static Route.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Static Routes.

.PARAMETER ContentType
The content type to use for the Static Routes, when parsing any payloads.

.PARAMETER TransferEncoding
The transfer encoding to use for the Static Routes, when parsing any payloads.

.PARAMETER Defaults
An array of default pages to display, such as 'index.html', for each Static Route.

.PARAMETER ErrorContentType
The content type of any error pages that may get returned.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Static Routes.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)

.PARAMETER AllowAnon
If supplied, the Static Routes will allow anonymous access for non-authenticated users.

.PARAMETER FileBrowser
When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory.

.PARAMETER DownloadOnly
When supplied, all static content on the Routes will be attached as downloads - rather than rendered.

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.

.EXAMPLE
Add-PodeStaticRouteGroup -Path '/static' -Routes { Add-PodeStaticRoute -Path '/images' -Etc }
#>
function Add-PodeStaticRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Source,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [ValidateSet('', 'gzip', 'deflate')]
        [string]
        $TransferEncoding,

        [Parameter()]
        [string[]]
        $Defaults,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default',

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $FileBrowser,

        [switch]
        $DownloadOnly,

        [switch]
        $RedirectToDefault
    )

    if (Test-PodeIsEmpty $Routes) {
        throw 'No scriptblock for -Routes passed'
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
            $Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
        }

        if ($null -ne $RouteGroup.Middleware) {
            $Middleware = $RouteGroup.Middleware + $Middleware
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ([string]::IsNullOrWhiteSpace($ContentType)) {
            $ContentType = $RouteGroup.ContentType
        }

        if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
            $TransferEncoding = $RouteGroup.TransferEncoding
        }

        if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
            $ErrorContentType = $RouteGroup.ErrorContentType
        }

        if ([string]::IsNullOrWhiteSpace($Authentication)) {
            $Authentication = $RouteGroup.Authentication
        }

        if ([string]::IsNullOrWhiteSpace($Access)) {
            $Access = $RouteGroup.Access
        }

        if (Test-PodeIsEmpty $Defaults) {
            $Defaults = $RouteGroup.Defaults
        }

        if ($RouteGroup.AllowAnon) {
            $AllowAnon = $RouteGroup.AllowAnon
        }

        if ($RouteGroup.DownloadOnly) {
            $DownloadOnly = $RouteGroup.DownloadOnly
        }

        if ($RouteGroup.FileBrowser) {
            $FileBrowser = $RouteGroup.FileBrowser
        }

        if ($RouteGroup.RedirectToDefault) {
            $RedirectToDefault = $RouteGroup.RedirectToDefault
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }

        if ($null -ne $RouteGroup.AccessMeta.Role) {
            $Role = $RouteGroup.AccessMeta.Role + $Role
        }

        if ($null -ne $RouteGroup.AccessMeta.Group) {
            $Group = $RouteGroup.AccessMeta.Group + $Group
        }

        if ($null -ne $RouteGroup.AccessMeta.Scope) {
            $Scope = $RouteGroup.AccessMeta.Scope + $Scope
        }

        if ($null -ne $RouteGroup.AccessMeta.User) {
            $User = $RouteGroup.AccessMeta.User + $User
        }

        if ($null -ne $RouteGroup.AccessMeta.Custom) {
            $CustomAccess = $RouteGroup.AccessMeta.Custom
        }
    }

    $RouteGroup = @{
        Path              = $Path
        Source            = $Source
        Middleware        = $Middleware
        EndpointName      = $EndpointName
        ContentType       = $ContentType
        TransferEncoding  = $TransferEncoding
        Defaults          = $Defaults
        RedirectToDefault = $RedirectToDefault
        ErrorContentType  = $ErrorContentType
        Authentication    = $Authentication
        Access            = $Access
        AllowAnon         = $AllowAnon
        DownloadOnly      = $DownloadOnly
        FileBrowser       = $FileBrowser
        IfExists          = $IfExists
        AccessMeta        = @{
            Role   = $Role
            Group  = $Group
            Scope  = $Scope
            User   = $User
            Custom = $CustomAccess
        }
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}


<#
.SYNOPSIS
Adds a Signal Route Group for multiple WebSockets.

.DESCRIPTION
Adds a Signal Route Group for sharing values between multiple WebSockets.

.PARAMETER Path
The URI path to use as a base for the Signal Routes, that should be prepended.

.PARAMETER Routes
A ScriptBlock for adding Signal Routes.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Signal Routes.

.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)

.EXAMPLE
Add-PodeSignalRouteGroup -Path '/signals' -Routes { Add-PodeSignalRoute -Path '/signal1' -Etc }
#>
function Add-PodeSignalRouteGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Routes,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    if (Test-PodeIsEmpty $Routes) {
        throw 'No scriptblock for -Routes passed'
    }

    if ($Path -eq '/') {
        $Path = $null
    }

    # check for scoped vars
    $Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState

    # group details
    if ($null -ne $RouteGroup) {
        if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
            $Path = "$($RouteGroup.Path)$($Path)"
        }

        if ([string]::IsNullOrWhiteSpace($EndpointName)) {
            $EndpointName = $RouteGroup.EndpointName
        }

        if ($RouteGroup.IfExists -ine 'default') {
            $IfExists = $RouteGroup.IfExists
        }
    }

    $RouteGroup = @{
        Path         = $Path
        EndpointName = $EndpointName
        IfExists     = $IfExists
    }

    # add routes
    $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}

<#
.SYNOPSIS
Remove a specific Route.

.DESCRIPTION
Remove a specific Route.

.PARAMETER Method
The method of the Route to remove.

.PARAMETER Path
The path of the Route to remove.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Route to be removed.

.EXAMPLE
Remove-PodeRoute -Method Get -Route '/about'

.EXAMPLE
Remove-PodeRoute -Method Post -Route '/users/:userId' -EndpointName User
#>
function Remove-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "[$($Method)]: No Route path supplied for removing a Route"
    }

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Resolve-PodePlaceholders -Path $Path

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # remove the operationId from the openapi operationId list
    if ($PodeContext.Server.Routes[$Method][$Path].OpenAPI) {
        foreach ( $tag  in  $PodeContext.Server.Routes[$Method][$Path].OpenAPI.DefinitionTag) {
            if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId)) {
                $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId }
            }
        }
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Remove a specific static Route.

.DESCRIPTION
Remove a specific static Route.

.PARAMETER Path
The path of the static Route to remove.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the static Route to be removed.

.EXAMPLE
Remove-PodeStaticRoute -Path '/assets'
#>
function Remove-PodeStaticRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Static'

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlashes -Path $Path -Static

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Remove a specific Signal Route.

.DESCRIPTION
Remove a specific Signal Route.

.PARAMETER Path
The path of the Signal Route to remove.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Signal Route to be removed.

.EXAMPLE
Remove-PodeSignalRoute -Route '/message'
#>
function Remove-PodeSignalRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Signal'

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlashes -Path $Path

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $null = $PodeContext.Server.Routes[$Method].Remove($Path)
    }
}

<#
.SYNOPSIS
Removes all added Routes, or Routes for a specific Method.

.DESCRIPTION
Removes all added Routes, or Routes for a specific Method.

.PARAMETER Method
The Method to from which to remove all Routes.

.EXAMPLE
Clear-PodeRoutes

.EXAMPLE
Clear-PodeRoutes -Method Get
#>
function Clear-PodeRoutes {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method
    )

    if (![string]::IsNullOrWhiteSpace($Method)) {
        $PodeContext.Server.Routes[$Method].Clear()
    }
    else {
        $PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Routes[$_].Clear()
        }
    }
}

<#
.SYNOPSIS
Removes all added static Routes.

.DESCRIPTION
Removes all added static Routes.

.EXAMPLE
Clear-PodeStaticRoutes
#>
function Clear-PodeStaticRoutes {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Routes['Static'].Clear()
}

<#
.SYNOPSIS
Removes all added Signal Routes.

.DESCRIPTION
Removes all added Signal Routes.

.EXAMPLE
Clear-PodeSignalRoutes
#>
function Clear-PodeSignalRoutes {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Routes['Signal'].Clear()
}

<#
.SYNOPSIS
Takes an array of Commands, or a Module, and converts them into Routes.

.DESCRIPTION
Takes an array of Commands (Functions/Aliases), or a Module, and generates appropriate Routes for the commands.

.PARAMETER Commands
An array of Commands to convert - if a Module is supplied, these Commands must be present within that Module.

.PARAMETER Module
A Module whose exported commands will be converted.

.PARAMETER Method
An override HTTP method to use when generating the Routes. If not supplied, Pode will make a best guess based on the Command's Verb.

.PARAMETER Path
An optional Path for the Route, to prepend before the Command Name and Module.

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.

.PARAMETER NoVerb
If supplied, the Command's Verb will not be included in the Route's path.

.PARAMETER NoOpenApi
If supplied, no OpenAPI definitions will be generated for the routes created.

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Middleware { ... }

.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Authentication AuthName

.EXAMPLE
ConvertTo-PodeRoute -Module Pester -Path '/api'

.EXAMPLE
ConvertTo-PodeRoute -Commands @('Invoke-Pester') -Module Pester
#>
function ConvertTo-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $Commands,

        [Parameter()]
        [string]
        $Module,

        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string]
        $Method,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [switch]
        $NoVerb,

        [switch]
        $NoOpenApi
    )

    # if a module was supplied, import it - then validate the commands
    if (![string]::IsNullOrWhiteSpace($Module)) {
        Import-PodeModule -Name $Module

        Write-Verbose 'Getting exported commands from module'
        $ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys

        # if commands were supplied validate them - otherwise use all exported ones
        if (Test-PodeIsEmpty $Commands) {
            Write-Verbose "Using all commands in $($Module) for converting to routes"
            $Commands = $ModuleCommands
        }
        else {
            Write-Verbose "Validating supplied commands against module's exported commands"
            foreach ($cmd in $Commands) {
                if ($ModuleCommands -inotcontains $cmd) {
                    throw "Module $($Module) does not contain function $($cmd) to convert to a Route"
                }
            }
        }
    }

    # if there are no commands, fail
    if (Test-PodeIsEmpty $Commands) {
        throw 'No commands supplied to convert to Routes'
    }

    # trim end trailing slashes from the path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = $Path.TrimEnd('/')

    # create the routes for each of the commands
    foreach ($cmd in $Commands) {
        # get module verb/noun and comvert verb to HTTP method
        $split = ($cmd -split '\-')

        if ($split.Length -ge 2) {
            $verb = $split[0]
            $noun = $split[1..($split.Length - 1)] -join ([string]::Empty)
        }
        else {
            $verb = [string]::Empty
            $noun = $split[0]
        }

        # determine the http method, or use the one passed
        $_method = $Method
        if ([string]::IsNullOrWhiteSpace($_method)) {
            $_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb
        }

        # use the full function name, or remove the verb
        $name = $cmd
        if ($NoVerb) {
            $name = $noun
        }

        # build the route's path
        $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/')

        # create the route
        $params = @{
            Method         = $_method
            Path           = $_path
            Middleware     = $Middleware
            Authentication = $Authentication
            Access         = $Access
            Role           = $Role
            Group          = $Group
            Scope          = $Scope
            User           = $User
            AllowAnon      = $AllowAnon
            ArgumentList   = $cmd
            PassThru       = $true
        }

        $route = Add-PodeRoute @params -ScriptBlock {
            param($cmd)

            # either get params from the QueryString or Payload
            if ($WebEvent.Method -ieq 'get') {
                $parameters = $WebEvent.Query
            }
            else {
                $parameters = $WebEvent.Data
            }

            # invoke the function
            $result = (. $cmd @parameters)

            # if we have a result, convert it to json
            if (!(Test-PodeIsEmpty $result)) {
                Write-PodeJsonResponse -Value $result -Depth 1
            }
        }

        # set the openapi metadata of the function, unless told to skip
        if ($NoOpenApi) {
            continue
        }

        $help = Get-Help -Name $cmd
        $route = ($route | Set-PodeOARouteInfo -Summary $help.Synopsis -Tags $Module -PassThru)

        # set the routes parameters (get = query, everything else = payload)
        $params = (Get-Command -Name $cmd).Parameters
        if (($null -eq $params) -or ($params.Count -eq 0)) {
            continue
        }

        $props = @(foreach ($key in $params.Keys) {
                $params[$key] | ConvertTo-PodeOAPropertyFromCmdletParameter
            })

        if ($_method -ieq 'get') {
            $route | Set-PodeOARequest -Parameters @(foreach ($prop in $props) { $prop | ConvertTo-PodeOAParameter -In Query })
        }

        else {
            $route | Set-PodeOARequest -RequestBody (
                New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAObjectProperty -Array -Properties $props) }
            )
        }
    }
}

<#
.SYNOPSIS
Helper function to generate simple GET routes.

.DESCRIPTION
Helper function to generate simple GET routes from ScritpBlocks, Files, and Views.
The output is always rendered as HTML.

.PARAMETER Name
A unique name for the page, that will be used in the Path for the route.

.PARAMETER ScriptBlock
A ScriptBlock to invoke, where any results will be converted to HTML.

.PARAMETER FilePath
A FilePath, literal or relative, to a valid HTML file.

.PARAMETER View
The name of a View to render, this can be HTML or Dynamic.

.PARAMETER Data
A hashtable of Data to supply to a Dynamic File/View, or to be splatted as arguments for the ScriptBlock.

.PARAMETER Path
An optional Path for the Route, to prepend before the Name.

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.

.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.

.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.

.PARAMETER AllowAnon
If supplied, the Page will allow anonymous access for non-authenticated users.

.PARAMETER FlashMessages
If supplied, Views will have any flash messages supplied to them for rendering.

.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.

.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.

.EXAMPLE
Add-PodePage -Name Services -ScriptBlock { Get-Service }

.EXAMPLE
Add-PodePage -Name Index -View 'index'

.EXAMPLE
Add-PodePage -Name About -FilePath '.\views\about.pode' -Data @{ Date = [DateTime]::UtcNow }
#>
function Add-PodePage {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'View')]
        [string]
        $View,

        [Parameter()]
        [hashtable]
        $Data,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [Alias('Auth')]
        [string]
        $Authentication,

        [Parameter()]
        [string]
        $Access,

        [Parameter()]
        [string[]]
        $Role,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $User,

        [switch]
        $AllowAnon,

        [Parameter(ParameterSetName = 'View')]
        [switch]
        $FlashMessages
    )

    $logic = $null
    $arg = $null

    # ensure the name is a valid alphanumeric
    if ($Name -inotmatch '^[a-z0-9\-_]+$') {
        throw "The Page name should be a valid AlphaNumeric value: $($Name)"
    }

    # trim end trailing slashes from the path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = $Path.TrimEnd('/')

    # define the appropriate logic
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'scriptblock' {
            if (Test-PodeIsEmpty $ScriptBlock) {
                throw 'A non-empty ScriptBlock is required to created a Page Route'
            }

            $arg = @($ScriptBlock, $Data)
            $logic = {
                param($script, $data)

                # invoke the function (optional splat data)
                if (Test-PodeIsEmpty $data) {
                    $result = Invoke-PodeScriptBlock -ScriptBlock $script -Return
                }
                else {
                    $result = Invoke-PodeScriptBlock -ScriptBlock $script -Arguments $data -Return
                }

                # if we have a result, convert it to html
                if (!(Test-PodeIsEmpty $result)) {
                    Write-PodeHtmlResponse -Value $result
                }
            }
        }

        'file' {
            $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -TestPath
            $arg = @($FilePath, $Data)
            $logic = {
                param($file, $data)
                Write-PodeFileResponse -Path $file -ContentType 'text/html' -Data $data
            }
        }

        'view' {
            $arg = @($View, $Data, $FlashMessages)
            $logic = {
                param($view, $data, [bool]$flash)
                Write-PodeViewResponse -Path $view -Data $data -FlashMessages:$flash
            }
        }
    }

    # build the route's path
    $_path = ("$($Path)/$($Name)" -replace '[/]+', '/')

    # create the route
    $params = @{
        Method         = 'Get'
        Path           = $_path
        Middleware     = $Middleware
        Authentication = $Authentication
        Access         = $Access
        Role           = $Role
        Group          = $Group
        Scope          = $Scope
        User           = $User
        AllowAnon      = $AllowAnon
        ArgumentList   = $arg
        ScriptBlock    = $logic
    }

    Add-PodeRoute @params
}

<#
.SYNOPSIS
Get a Route(s).

.DESCRIPTION
Get a Route(s).

.PARAMETER Method
A Method to filter the routes.

.PARAMETER Path
A Path to filter the routes.

.PARAMETER EndpointName
The name of an endpoint to filter routes.

.EXAMPLE
Get-PodeRoute -Method Get -Path '/about'

.EXAMPLE
Get-PodeRoute -Method Post -Path '/users/:userId' -EndpointName User
#>
function Get-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes.Values.Values) {
        $routes += $route
    }

    # if we have a method, filter
    if (![string]::IsNullOrWhiteSpace($Method)) {
        $routes = @(foreach ($route in $routes) {
                if ($route.Method -ine $Method) {
                    continue
                }

                $route
            })
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Split-PodeRouteQuery -Path $Path
        $Path = Update-PodeRouteSlashes -Path $Path
        $Path = Resolve-PodePlaceholders -Path $Path

        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Get a static Route(s).

.DESCRIPTION
Get a static Route(s).

.PARAMETER Path
A Path to filter the static routes.

.PARAMETER EndpointName
The name of an endpoint to filter static routes.

.EXAMPLE
Get-PodeStaticRoute -Path '/assets'

.EXAMPLE
Get-PodeStaticRoute -Path '/assets' -EndpointName User
#>
function Get-PodeStaticRoute {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes['Static'].Values) {
        $routes += $route
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Update-PodeRouteSlashes -Path $Path -Static
        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Get a Signal Route(s).

.DESCRIPTION
Get a Signal Route(s).

.PARAMETER Path
A Path to filter the signal routes.

.PARAMETER EndpointName
The name of an endpoint to filter signal routes.

.EXAMPLE
Get-PodeSignalRoute -Path '/message'
#>
function Get-PodeSignalRoute {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every route
    $routes = @()
    foreach ($route in $PodeContext.Server.Routes['Signal'].Values) {
        $routes += $route
    }

    # if we have a path, filter
    if (![string]::IsNullOrWhiteSpace($Path)) {
        $Path = Update-PodeRouteSlashes -Path $Path
        $routes = @(foreach ($route in $routes) {
                if ($route.Path -ine $Path) {
                    continue
                }

                $route
            })
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $routes = @(foreach ($name in $EndpointName) {
                foreach ($route in $routes) {
                    if ($route.Endpoint.Name -ine $name) {
                        continue
                    }

                    $route
                }
            })
    }

    # return
    return $routes
}

<#
.SYNOPSIS
Automatically loads route ps1 files

.DESCRIPTION
Automatically loads route ps1 files from either a /routes folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)

.EXAMPLE
Use-PodeRoutes

.EXAMPLE
Use-PodeRoutes -Path './my-routes' -IfExists Skip
#>
function Use-PodeRoutes {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $IfExists = 'Default'
    )

    if ($IfExists -ieq 'Default') {
        $IfExists = Get-PodeRouteIfExistsPreference
    }

    Use-PodeFolder -Path $Path -DefaultPath 'routes'
}

<#
.SYNOPSIS
Set the default IfExists preference for Routes.

.DESCRIPTION
Set the default IfExists preference for Routes.

.PARAMETER Value
Specifies what action to take when a Route already exists. (Default: Default)

.EXAMPLE
Set-PodeRouteIfExistsPreference -Value Overwrite
#>
function Set-PodeRouteIfExistsPreference {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        [string]
        $Value = 'Default'
    )

    $PodeContext.Server.Preferences.Routes.IfExists = $Value
}

<#
.SYNOPSIS
Test if a Route already exists.

.DESCRIPTION
Test if a Route already exists for a given Method and Path.

.PARAMETER Method
The HTTP Method of the Route.

.PARAMETER Path
The URI path of the Route.

.PARAMETER EndpointName
The EndpointName of an Endpoint the Route is bound against.

.PARAMETER CheckWildcard
If supplied, Pode will check for the Route on the Method first, and then check for the Route on the '*' Method.

.EXAMPLE
Test-PodeRoute -Method Post -Path '/example'

.EXAMPLE
Test-PodeRoute -Method Post -Path '/example' -CheckWildcard

.EXAMPLE
Test-PodeRoute -Method Get -Path '/example/:exampleId' -CheckWildcard
#>
function Test-PodeRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $CheckWildcard
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw 'No Path supplied for testing Route'
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Resolve-PodePlaceholders -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]

    # check for routes
    $found = (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
    if (!$found -and $CheckWildcard) {
        $found = (Test-PodeRouteInternal -Method '*' -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
    }

    return $found
}

<#
.SYNOPSIS
Test if a Static Route already exists.

.DESCRIPTION
Test if a Static Route already exists for a given Path.

.PARAMETER Path
The URI path of the Static Route.

.PARAMETER EndpointName
The EndpointName of an Endpoint the Static Route is bound against.

.EXAMPLE
Test-PodeStaticRoute -Path '/assets'
#>
function Test-PodeStaticRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    # store the route method
    $Method = 'Static'

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw 'No Path supplied for testing Static Route'
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path -Static
    $Path = Resolve-PodePlaceholders -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]

    # check for routes
    return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}

<#
.SYNOPSIS
Test if a Signal Route already exists.

.DESCRIPTION
Test if a Signal Route already exists for a given Path.

.PARAMETER Path
The URI path of the Signal Route.

.PARAMETER EndpointName
The EndpointName of an Endpoint the Signal Route is bound against.

.EXAMPLE
Test-PodeSignalRoute -Path '/message'
#>
function Test-PodeSignalRoute {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $EndpointName
    )

    $Method = 'Signal'

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path

    # get endpoint from name
    $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]

    # check for routes
    return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}
src\Public\Schedules.ps1
<#
.SYNOPSIS
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.

.DESCRIPTION
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.

.PARAMETER Name
The Name of the Schedule.

.PARAMETER Cron
One, or an Array, of Cron Expressions to define when the Schedule should trigger.

.PARAMETER ScriptBlock
The script defining the Schedule's logic.

.PARAMETER Limit
The number of times the Schedule should trigger before being removed.

.PARAMETER StartTime
A DateTime for when the Schedule should start triggering.

.PARAMETER EndTime
A DateTime for when the Schedule should stop triggering, and be removed.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Schedule's logic.

.PARAMETER OnStart
If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time.

.EXAMPLE
Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' }
#>
function Add-PodeSchedule {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string[]]
        $Cron,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Limit = 0,

        [Parameter()]
        [DateTime]
        $StartTime,

        [Parameter()]
        [DateTime]
        $EndTime,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [hashtable]
        $ArgumentList,

        [switch]
        $OnStart
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeSchedule' -ThrowError

    # ensure the schedule doesn't already exist
    if ($PodeContext.Schedules.Items.ContainsKey($Name)) {
        throw "[Schedule] $($Name): Schedule already defined"
    }

    # ensure the limit is valid
    if ($Limit -lt 0) {
        throw "[Schedule] $($Name): Cannot have a negative limit"
    }

    # ensure the start/end dates are valid
    if (($null -ne $EndTime) -and ($EndTime -lt [DateTime]::Now)) {
        throw "[Schedule] $($Name): The EndTime value must be in the future"
    }

    if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -le $StartTime)) {
        throw "[Schedule] $($Name): Cannot have a StartTime after the EndTime"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the schedule
    $parsedCrons = ConvertFrom-PodeCronExpressions -Expressions @($Cron)
    $nextTrigger = Get-PodeCronNextEarliestTrigger -Expressions $parsedCrons -StartTime $StartTime -EndTime $EndTime

    $PodeContext.Schedules.Enabled = $true
    $PodeContext.Schedules.Items[$Name] = @{
        Name            = $Name
        StartTime       = $StartTime
        EndTime         = $EndTime
        Crons           = $parsedCrons
        CronsRaw        = @($Cron)
        Limit           = $Limit
        Count           = 0
        NextTriggerTime = $nextTrigger
        LastTriggerTime = $null
        Script          = $ScriptBlock
        UsingVariables  = $usingVars
        Arguments       = (Protect-PodeValue -Value $ArgumentList -Default @{})
        OnStart         = $OnStart
        Completed       = ($null -eq $nextTrigger)
    }
}

<#
.SYNOPSIS
Set the maximum number of concurrent schedules.

.DESCRIPTION
Set the maximum number of concurrent schedules.

.PARAMETER Maximum
The Maximum number of schedules to run.

.EXAMPLE
Set-PodeScheduleConcurrency -Maximum 25
#>
function Set-PodeScheduleConcurrency {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [int]
        $Maximum
    )

    # error if <=0
    if ($Maximum -le 0) {
        throw "Maximum concurrent schedules must be >=1 but got: $($Maximum)"
    }

    # ensure max > min
    $_min = 1
    if ($null -ne $PodeContext.RunspacePools.Schedules) {
        $_min = $PodeContext.RunspacePools.Schedules.Pool.GetMinRunspaces()
    }

    if ($_min -gt $Maximum) {
        throw "Maximum concurrent schedules cannot be less than the minimum of $($_min) but got: $($Maximum)"
    }

    # set the max schedules
    $PodeContext.Threads.Schedules = $Maximum
    if ($null -ne $PodeContext.RunspacePools.Schedules) {
        $PodeContext.RunspacePools.Schedules.Pool.SetMaxRunspaces($Maximum)
    }
}

<#
.SYNOPSIS
Adhoc invoke a Schedule's logic.

.DESCRIPTION
Adhoc invoke a Schedule's logic outside of its defined cron-expression. This invocation doesn't count towards the Schedule's limit.

.PARAMETER Name
The Name of the Schedule.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.

.EXAMPLE
Invoke-PodeSchedule -Name 'schedule-name'
#>
function Invoke-PodeSchedule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $ArgumentList = $null
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    # run schedule logic
    Invoke-PodeInternalScheduleLogic -Schedule $PodeContext.Schedules.Items[$Name] -ArgumentList $ArgumentList
}

<#
.SYNOPSIS
Removes a specific Schedule.

.DESCRIPTION
Removes a specific Schedule.

.PARAMETER Name
The Name of the Schedule to be removed.

.EXAMPLE
Remove-PodeSchedule -Name 'RenewToken'
#>
function Remove-PodeSchedule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Schedules.Items.Remove($Name)
}

<#
.SYNOPSIS
Removes all Schedules.

.DESCRIPTION
Removes all Schedules.

.EXAMPLE
Clear-PodeSchedules
#>
function Clear-PodeSchedules {
    [CmdletBinding()]
    param()

    $PodeContext.Schedules.Items.Clear()
}

<#
.SYNOPSIS
Edits an existing Schedule.

.DESCRIPTION
Edits an existing Schedule's properties, such an cron expressions or scriptblock.

.PARAMETER Name
The Name of the Schedule.

.PARAMETER Cron
Any new Cron Expressions for the Schedule.

.PARAMETER ScriptBlock
The new ScriptBlock for the Schedule.

.PARAMETER ArgumentList
Any new Arguments for the Schedule.

.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron '@minutely'

.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron @('@hourly', '0 0 * * TUE')
#>
function Edit-PodeSchedule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Cron,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [hashtable]
        $ArgumentList
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    $_schedule = $PodeContext.Schedules.Items[$Name]

    # edit cron if supplied
    if (!(Test-PodeIsEmpty $Cron)) {
        $_schedule.Crons = (ConvertFrom-PodeCronExpressions -Expressions @($Cron))
        $_schedule.CronsRaw = $Cron
        $_schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $_schedule.StartTime -EndTime $_schedule.EndTime
    }

    # edit scriptblock if supplied
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $_schedule.Script = $ScriptBlock
        $_schedule.UsingVariables = $usingVars
    }

    # edit arguments if supplied
    if (!(Test-PodeIsEmpty $ArgumentList)) {
        $_schedule.Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Returns any defined schedules.

.DESCRIPTION
Returns any defined schedules, with support for filtering.

.PARAMETER Name
Any schedule Names to filter the schedules.

.PARAMETER StartTime
An optional StartTime to only return Schedules that will trigger after this date.

.PARAMETER EndTime
An optional EndTime to only return Schedules that will trigger before this date.

.EXAMPLE
Get-PodeSchedule

.EXAMPLE
Get-PodeSchedule -Name Name1, Name2

.EXAMPLE
Get-PodeSchedule -Name Name1, Name2 -StartTime [datetime]::new(2020, 3, 1) -EndTime [datetime]::new(2020, 3, 31)
#>
function Get-PodeSchedule {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name,

        [Parameter()]
        $StartTime = $null,

        [Parameter()]
        $EndTime = $null
    )

    $schedules = $PodeContext.Schedules.Items.Values

    # further filter by schedule names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $schedules = @(foreach ($_name in $Name) {
                foreach ($schedule in $schedules) {
                    if ($schedule.Name -ine $_name) {
                        continue
                    }

                    $schedule
                }
            })
    }

    # filter by some start time
    if ($null -ne $StartTime) {
        $schedules = @(foreach ($schedule in $schedules) {
                if (($null -ne $schedule.StartTime) -and ($StartTime -lt $schedule.StartTime)) {
                    continue
                }

                $_end = $EndTime
                if ($null -eq $_end) {
                    $_end = $schedule.EndTime
                }

                if (($null -ne $schedule.EndTime) -and
                (($StartTime -gt $schedule.EndTime) -or
                    ((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $StartTime) -gt $_end))) {
                    continue
                }

                $schedule
            })
    }

    # filter by some end time
    if ($null -ne $EndTime) {
        $schedules = @(foreach ($schedule in $schedules) {
                if (($null -ne $schedule.EndTime) -and ($EndTime -gt $schedule.EndTime)) {
                    continue
                }

                $_start = $StartTime
                if ($null -eq $_start) {
                    $_start = $schedule.StartTime
                }

                if (($null -ne $schedule.StartTime) -and
                (($EndTime -lt $schedule.StartTime) -or
                    ((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $_start) -gt $EndTime))) {
                    continue
                }

                $schedule
            })
    }

    # return
    return $schedules
}

<#
.SYNOPSIS
Tests whether the passed Schedule exists.

.DESCRIPTION
Tests whether the passed Schedule exists by its name.

.PARAMETER Name
The Name of the Schedule.

.EXAMPLE
if (Test-PodeSchedule -Name ScheduleName) { }
#>
function Test-PodeSchedule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (($null -ne $PodeContext.Schedules.Items) -and $PodeContext.Schedules.Items.ContainsKey($Name))
}

<#
.SYNOPSIS
Get the next trigger time for a Schedule.

.DESCRIPTION
Get the next trigger time for a Schedule, either from the Schedule's StartTime or from a defined DateTime.

.PARAMETER Name
The Name of the Schedule.

.PARAMETER DateTime
An optional specific DateTime to get the next trigger time after. This DateTime must be between the Schedule's StartTime and EndTime.

.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1

.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1 -DateTime [datetime]::new(2020, 3, 10)
#>
function Get-PodeScheduleNextTrigger {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        $DateTime = $null
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    $_schedule = $PodeContext.Schedules.Items[$Name]

    # ensure date is after start/before end
    if (($null -ne $DateTime) -and ($null -ne $_schedule.StartTime) -and ($DateTime -lt $_schedule.StartTime)) {
        throw "Supplied date is before the start time of the schedule at $($_schedule.StartTime)"
    }

    if (($null -ne $DateTime) -and ($null -ne $_schedule.EndTime) -and ($DateTime -gt $_schedule.EndTime)) {
        throw "Supplied date is after the end time of the schedule at $($_schedule.EndTime)"
    }

    # get the next trigger
    if ($null -eq $DateTime) {
        $DateTime = $_schedule.StartTime
    }

    return (Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $DateTime -EndTime $_schedule.EndTime)
}

<#
.SYNOPSIS
Automatically loads schedule ps1 files

.DESCRIPTION
Automatically loads schedule ps1 files from either a /schedules folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeSchedules

.EXAMPLE
Use-PodeSchedules -Path './my-schedules'
#>
function Use-PodeSchedules {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'schedules'
}
src\Public\ScopedVariables.ps1
<#
.SYNOPSIS
Converts Scoped Variables within a given ScriptBlock.

.DESCRIPTION
Converts Scoped Variables within a given ScriptBlock, and returns the updated ScriptBlock back, including any
using-variable values that will need to be supplied as parameters to the ScriptBlock first.

.PARAMETER ScriptBlock
The ScriptBlock to be converted.

.PARAMETER PSSession
An optional SessionState object, used to retrieve using-variable values.
If not supplied, using-variable values will not be converted.

.PARAMETER Exclude
An optional array of one or more Scoped Variable Names to Exclude from converting. (ie: Session, Using, or a Name from Add-PodeScopedVariable)

.EXAMPLE
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

.EXAMPLE
$ScriptBlock = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -Exclude Session, Using
#>
function Convert-PodeScopedVariables {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [System.Management.Automation.SessionState]
        $PSSession,

        [Parameter()]
        [string[]]
        $Exclude
    )

    # do nothing if no scriptblock
    if ($null -eq $ScriptBlock) {
        return $ScriptBlock
    }

    # using vars
    $usingVars = $null

    # loop through each defined scoped variable and convert, unless excluded
    foreach ($key in $PodeContext.Server.ScopedVariables.Keys) {
        # excluded?
        if ($Exclude -icontains $key) {
            continue
        }

        # convert scoped var
        $ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name $key -ScriptBlock $ScriptBlock -PSSession $PSSession

        # using vars?
        if (($null -ne $otherResults) -and ($key -ieq 'using')) {
            $usingVars = $otherResults
        }
    }

    # return just the scriptblock, or include using vars as well
    if ($null -ne $usingVars) {
        return $ScriptBlock, $usingVars
    }

    return $ScriptBlock
}

<#
.SYNOPSIS
Converts a Scoped Variable within a given ScriptBlock.

.DESCRIPTION
Converts a Scoped Variable within a given ScriptBlock, and returns the updated ScriptBlock back, including any
other values that will need to be supplied as parameters to the ScriptBlock first.

.PARAMETER Name
The Name of the Scoped Variable to convert. (ie: Session, Using, or a Name from Add-PodeScopedVariable)

.PARAMETER ScriptBlock
The ScriptBlock to be converted.

.PARAMETER PSSession
An optional SessionState object, used to retrieve using-variable values or other values where scope is required.

.EXAMPLE
$ScriptBlock = Convert-PodeScopedVariable -Name State -ScriptBlock $ScriptBlock

.EXAMPLE
$ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name Using -ScriptBlock $ScriptBlock
#>
function Convert-PodeScopedVariable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [System.Management.Automation.SessionState]
        $PSSession
    )

    # do nothing if no scriptblock
    if ($null -eq $ScriptBlock) {
        return $ScriptBlock
    }

    # check if scoped var defined
    if (!(Test-PodeScopedVariable -Name $Name)) {
        throw "Scoped Variable not found: $($Name)"
    }

    # get the scoped var metadata
    $scopedVar = $PodeContext.Server.ScopedVariables[$Name]

    # scriptblock or replace?
    if ($null -ne $scopedVar.ScriptBlock) {
        return Invoke-PodeScriptBlock `
            -ScriptBlock $scopedVar.ScriptBlock `
            -Arguments $ScriptBlock, $PSSession, $scopedVar.Get.Pattern, $scopedVar.Set.Pattern `
            -Splat `
            -Return `
            -NoNewClosure
    }

    # replace style
    else {
        # convert scriptblock to string
        $strScriptBlock = "$($ScriptBlock)"

        # see if the script contains any form of the scoped variable, and if not just return
        $found = $strScriptBlock -imatch "\`$$($Name)\:"
        if (!$found) {
            return $ScriptBlock
        }

        # loop and replace "set" syntax if replace template supplied
        if (![string]::IsNullOrEmpty($scopedVar.Set.Replace)) {
            while ($strScriptBlock -imatch $scopedVar.Set.Pattern) {
                $setReplace = $scopedVar.Set.Replace.Replace('{{name}}', $Matches['name'])
                $strScriptBlock = $strScriptBlock.Replace($Matches['full'], $setReplace)
            }
        }

        # loop and replace "get" syntax
        while ($strScriptBlock -imatch $scopedVar.Get.Pattern) {
            $getReplace = $scopedVar.Get.Replace.Replace('{{name}}', $Matches['name'])
            $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))")
        }

        # convert update scriptblock back
        return [scriptblock]::Create($strScriptBlock)
    }
}

<#
.SYNOPSIS
Adds a new Scoped Variable.

.DESCRIPTION
Adds a new Scoped Variable, to make calling certain functions simpler.
For example "$state:Name" instead of "Get-PodeState" and "Set-PodeState".

.PARAMETER Name
The Name of the Scoped Variable.

.PARAMETER GetReplace
A template to be used when converting "$var = $SV:<name>" to a "Get-SVValue -Name <name>" syntax.
You can use the "{{name}}" placeholder to show where the <name> would be placed in the conversion. The result will also be automatically wrapped in brackets.
For example, "$var = $state:<name>" to "Get-PodeState -Name <name>" would need a GetReplace value of "Get-PodeState -Name '{{name}}'".

.PARAMETER SetReplace
An optional template to be used when converting "$SV:<name> = <value>" to a "Set-SVValue -Name <name> -Value <value>" syntax.
You can use the "{{name}}" placeholder to show where the <name> would be placed in the conversion. The <value> will automatically be appended to the end.
For example, "$state:<name> = <value>" to "Set-PodeState -Name <name> -Value <value>" would need a SetReplace value of "Set-PodeState -Name '{{name}}' -Value ".

.PARAMETER ScriptBlock
For more advanced conversions, that aren't as simple as a simple find/replace, you can supply a ScriptBlock instead.
This ScriptBlock will be supplied ScriptBlock to convert, followed by a SessionState object, and the Get/Set regex patterns, as parameters.
The ScriptBlock should returned a converted ScriptBlock that works, plus an optional array of values that should be supplied to the ScriptBlock when invoked.

.EXAMPLE
Add-PodeScopedVariable -Name 'cache' -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " -GetReplace "Get-PodeCache -Key '{{name}}'"

.EXAMPLE
Add-PodeScopedVariable -Name 'config' -ScriptBlock {
    param($ScriptBlock, $SessionState, $GetPattern, $SetPattern)
    $strScriptBlock = "$($ScriptBlock)"
    $template = "(Get-PodeConfig).'{{name}}'"

    # allows "$port = $config:port" instead of "$port = (Get-PodeConfig).port"
    while ($strScriptBlock -imatch $GetPattern) {
        $getReplace = $template.Replace('{{name}}', $Matches['name'])
        $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))")
    }

    return [scriptblock]::Create($strScriptBlock)
}
#>
function Add-PodeScopedVariable {
    [CmdletBinding(DefaultParameterSetName = 'Replace')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Replace')]
        [string]
        $GetReplace,

        [Parameter(ParameterSetName = 'Replace')]
        [string]
        $SetReplace = $null,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock
    )

    # check if var already defined
    if (Test-PodeScopedVariable -Name $Name) {
        throw "Scoped Variable already defined: $($Name)"
    }

    # add scoped var definition
    $PodeContext.Server.ScopedVariables[$Name] = @{
        $Name       = $Name
        ScriptBlock = $ScriptBlock
        Get         = @{
            Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+))"
            Replace = $GetReplace
        }
        Set         = @{
            Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+)\s*=)"
            Replace = $SetReplace
        }
    }
}

<#
.SYNOPSIS
Removes a Scoped Variable.

.DESCRIPTION
Removes a Scoped Variable.

.PARAMETER Name
The Name of a Scoped Variable to remove.

.EXAMPLE
Remove-PodeScopedVariable -Name State
#>
function Remove-PodeScopedVariable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Server.ScopedVariables.Remove($Name)
}

<#
.SYNOPSIS
Tests if a Scoped Variable exists.

.DESCRIPTION
Tests if a Scoped Variable exists.

.PARAMETER Name
The Name of the Scoped Variable to check.

.EXAMPLE
if (Test-PodeScopedVariable -Name $Name) { ... }
#>
function Test-PodeScopedVariable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.ScopedVariables.Contains($Name)
}

<#
.SYNOPSIS
Removes all Scoped Variables.

.DESCRIPTION
Removes all Scoped Variables.

.EXAMPLE
Clear-PodeScopedVariables
#>
function Clear-PodeScopedVariables {
    $null = $PodeContext.Server.ScopedVariables.Clear()
}

<#
.SYNOPSIS
Get a Scoped Variable(s).

.DESCRIPTION
Get a Scoped Variable(s).

.PARAMETER Name
The Name of the Scoped Variable(s) to retrieve.

.EXAMPLE
Get-PodeScopedVariable -Name State

.EXAMPLE
Get-PodeScopedVariable -Name State, Using
#>
function Get-PodeScopedVariable {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    # return all if no Name
    if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) {
        return $PodeContext.Server.ScopedVariables.Values
    }

    # return filtered
    return @(foreach ($n in $Name) {
            $PodeContext.Server.ScopedVariables[$n]
        })
}

<#
.SYNOPSIS
Automatically loads Scoped Variable ps1 files

.DESCRIPTION
Automatically loads Scoped Variable ps1 files from either a /scoped-vars folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeScopedVariables

.EXAMPLE
Use-PodeScopedVariables -Path './my-vars'
#>
function Use-PodeScopedVariables {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'scoped-vars'
}
src\Public\Secrets.ps1
<#
.SYNOPSIS
Register a Secret Vault.

.DESCRIPTION
Register a Secret Vault, which is defined by either custom logic or using the SecretManagement module.

.PARAMETER Name
The unique friendly Name of the Secret Vault within Pode.

.PARAMETER VaultParameters
A hashtable of extra parameters that should be supplied to either the SecretManagement module, or custom scriptblocks.

.PARAMETER UnlockSecret
An optional Secret to be used to unlock the Secret Vault if need.

.PARAMETER UnlockSecureSecret
An optional Secret, as a SecureString, to be used to unlock the Secret Vault if need.

.PARAMETER UnlockInterval
An optional number of minutes that Pode will periodically check/unlock the Secret Vault. (Default: 0)

.PARAMETER NoUnlock
If supplied, the Secret Vault will not be unlocked after registration. To unlock you'll need to call Unlock-PodeSecretVault.

.PARAMETER CacheTtl
An optional number of minutes that Secrets should be cached for. (Default: 0)

.PARAMETER InitScriptBlock
An optional scriptblock to run before the Secret Vault is registered, letting you initialise any connection, contexts, etc.

.PARAMETER VaultName
For SecretManagement module Secret Vaults, you can use thie parameter to specify the actual Vault name, and use the above Name parameter as a more friendly name if required.

.PARAMETER ModuleName
For SecretManagement module Secret Vaults, this is the name/path of the extension module to be used.

.PARAMETER ScriptBlock
For custom Secret Vaults, this is a scriptblock used to read the Secret from the Vault.

.PARAMETER UnlockScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to unlock the Secret Vault.

.PARAMETER RemoveScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to remove a Secret from the Vault.

.PARAMETER SetScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to create/update a Secret in the Vault.

.PARAMETER UnregisterScriptBlock
For custom Secret Vaults, this is an optional scriptblock used unregister the Secret Vault with any custom clean-up logic.

.EXAMPLE
Register-PodeSecretVault -Name 'VaultName' -ModuleName 'Az.KeyVault' -VaultParameters @{ AZKVaultName = $name; SubscriptionId = $subId }

.EXAMPLE
Register-PodeSecretVault -Name 'VaultName' -VaultParameters @{ Address = 'http://127.0.0.1:8200' } -ScriptBlock { ... }
#>
function Register-PodeSecretVault {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $VaultParameters,

        [Parameter()]
        [string]
        $UnlockSecret,

        [Parameter()]
        [securestring]
        $UnlockSecureSecret,

        [Parameter()]
        [int]
        $UnlockInterval = 0,

        [switch]
        $NoUnlock,

        [Parameter()]
        [int]
        $CacheTtl = 0, # in minutes

        [Parameter()]
        [scriptblock]
        $InitScriptBlock,

        [Parameter(ParameterSetName = 'SecretManagement')]
        [string]
        $VaultName,

        [Parameter(Mandatory = $true, ParameterSetName = 'SecretManagement')]
        [Alias('Module')]
        [string]
        $ModuleName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
        [scriptblock]
        $ScriptBlock, # Read a secret

        [Parameter(ParameterSetName = 'Custom')]
        [Alias('Unlock')]
        [scriptblock]
        $UnlockScriptBlock,

        [Parameter(ParameterSetName = 'Custom')]
        [Alias('Remove')]
        [scriptblock]
        $RemoveScriptBlock,

        [Parameter(ParameterSetName = 'Custom')]
        [Alias('Set')]
        [scriptblock]
        $SetScriptBlock,

        [Parameter(ParameterSetName = 'Custom')]
        [Alias('Unregister')]
        [scriptblock]
        $UnregisterScriptBlock
    )

    # has the vault already been registered?
    if (Test-PodeSecretVault -Name $Name) {
        $autoImported = [string]::Empty
        if ($PodeContext.Server.Secrets.Vaults[$Name].AutoImported) {
            $autoImported = ' from auto-importing'
        }

        throw "A Secret Vault with the name '$($Name)' has already been registered$($autoImported)"
    }

    # base vault config
    if (![string]::IsNullOrEmpty($UnlockSecret)) {
        $UnlockSecureSecret = $UnlockSecret | ConvertTo-SecureString -AsPlainText -Force
    }

    $vault = @{
        Name         = $Name
        Type         = $PSCmdlet.ParameterSetName.ToLowerInvariant()
        Parameters   = $VaultParameters
        AutoImported = $false
        Unlock       = @{
            Secret   = $UnlockSecureSecret
            Expiry   = $null
            Interval = $UnlockInterval
            Enabled  = (!(Test-PodeIsEmpty $UnlockSecureSecret))
        }
        Cache        = @{
            Ttl     = $CacheTtl
            Enabled = ($CacheTtl -gt 0)
        }
    }

    # initialise the secret vault
    if ($null -ne $InitScriptBlock) {
        $vault | Initialize-PodeSecretVault -ScriptBlock $InitScriptBlock
    }

    # set vault config depending on vault type
    switch ($vault.Type) {
        'custom' {
            $vault | Register-PodeSecretCustomVault `
                -ScriptBlock $ScriptBlock `
                -UnlockScriptBlock $UnlockScriptBlock `
                -RemoveScriptBlock $RemoveScriptBlock `
                -SetScriptBlock $SetScriptBlock `
                -UnregisterScriptBlock $UnregisterScriptBlock
        }

        'secretmanagement' {
            $vault | Register-PodeSecretManagementVault `
                -VaultName $VaultName `
                -ModuleName $ModuleName
        }
    }

    # create timer to clear cached secrets every minute
    Start-PodeSecretCacheHousekeeper

    # add vault config to context
    $PodeContext.Server.Secrets.Vaults[$Name] = $vault

    # unlock the vault?
    if (!$NoUnlock -and $vault.Unlock.Enabled) {
        Unlock-PodeSecretVault -Name $Name
    }
}

<#
.SYNOPSIS
Unregister a Secret Vault.

.DESCRIPTION
Unregister a Secret Vault. If the Vault was via the SecretManagement module it will also be unregistered there as well.

.PARAMETER Name
The Name of the Secret Vault in Pode to unregister.

.EXAMPLE
Unregister-PodeSecretVault -Name 'VaultName'
#>
function Unregister-PodeSecretVault {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # has the vault been registered?
    if (!(Test-PodeSecretVault -Name $Name)) {
        return
    }

    # get vault
    $vault = $PodeContext.Server.Secrets.Vaults[$Name]

    # unlock depending on vault type, and set expiry
    switch ($vault.Type) {
        'custom' {
            $vault | Unregister-PodeSecretCustomVault
        }

        'secretmanagement' {
            $vault | Unregister-PodeSecretManagementVault
        }
    }

    # unregister from Pode
    $null = $PodeContext.Server.Secrets.Vaults.Remove($Name)
}

<#
.SYNOPSIS
Unlock the Secret Vault.

.DESCRIPTION
Unlock the Secret Vault.

.PARAMETER Name
The Name of the Secret Vault in Pode to be unlocked.

.EXAMPLE
Unlock-PodeSecretVault -Name 'VaultName'
#>
function Unlock-PodeSecretVault {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # has the vault been registered?
    if (!(Test-PodeSecretVault -Name $Name)) {
        throw "No Secret Vault with the name '$($Name)' has been registered"
    }

    # get vault
    $vault = $PodeContext.Server.Secrets.Vaults[$Name]
    $expiry = $null

    # is unlocking even enabled?
    if (!$vault.Unlock.Enabled) {
        return
    }

    # unlock depending on vault type, and set expiry
    switch ($vault.Type) {
        'custom' {
            $expiry = $vault | Unlock-PodeSecretCustomVault
        }

        'secretmanagement' {
            $expiry = $vault | Unlock-PodeSecretManagementVault
        }
    }

    # if we have an expiry returned, set to UTC and configure unlock schedule
    if ($null -ne $expiry) {
        $expiry = ([datetime]$expiry).ToUniversalTime()
        if ($expiry -le [datetime]::UtcNow) {
            throw "Secret Vault unlock expiry date is in the past (UTC): $($expiry)"
        }

        $vault.Unlock.Expiry = $expiry
        Start-PodeSecretVaultUnlocker
    }
}

<#
.SYNOPSIS
Fetches and returns information of a Secret Vault.

.DESCRIPTION
Fetches and returns information of a Secret Vault.

.PARAMETER Name
The Name(s) of a Secret Vault to retrieve.

.EXAMPLE
$vault = Get-PodeSecretVault -Name 'VaultName'

.EXAMPLE
$vaults = Get-PodeSecretVault -Name 'VaultName1', 'VaultName2'
#>
function Get-PodeSecretVault {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name
    )

    $vaults = $PodeContext.Server.Secrets.Vaults.Values

    # further filter by vault names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $vaults = @(foreach ($_name in $Name) {
                foreach ($vault in $vaults) {
                    if ($vault.Name -ine $_name) {
                        continue
                    }

                    $vault
                }
            })
    }

    # return
    return $vaults
}

<#
.SYNOPSIS
Tests if a Secret Vault has been registered.

.DESCRIPTION
Tests if a Secret Vault has been registered.

.PARAMETER Name
The Name of the Secret Vault to test.

.EXAMPLE
if (Test-PodeSecretVault -Name 'VaultName') { ... }
#>
function Test-PodeSecretVault {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (($null -ne $PodeContext.Server.Secrets.Vaults) -and $PodeContext.Server.Secrets.Vaults.ContainsKey($Name))
}

<#
.SYNOPSIS
Mount a Secret from a Secret Vault.

.DESCRIPTION
Mount a Secret from a Secret Vault, so it can be more easily referenced and support caching.

.PARAMETER Name
A unique friendly Name for the Secret.

.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.

.PARAMETER Property
An optional array of Properties to be returned if the Secret contains multiple properties.

.PARAMETER ExpandProperty
An optional Property to be expanded from the Secret and return if it contains multiple properties.

.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.

.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.

.PARAMETER CacheTtl
An optional number of minutes to Cache the Secret's value for. You can use this parameter to override the Secret Vault's value. (Default: -1)
If the value is -1 it uses the Secret Vault's CacheTtl. A value of 0 is to disable caching for this Secret. A value >0 overrides the Secret Vault.

.EXAMPLE
Mount-PodeSecret -Name 'SecretName' -Vault 'VaultName' -Key 'path/to/secret' -ExpandProperty 'foo'

.EXAMPLE
Mount-PodeSecret -Name 'SecretName' -Vault 'VaultName' -Key 'key_of_secret' -CacheTtl 5
#>
function Mount-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter()]
        [string[]]
        $Property,

        [Parameter()]
        [string]
        $ExpandProperty,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [object[]]
        $ArgumentList,

        # in minutes (-1 means use the vault default, 0 is off, anything higher than 0 is an override)
        [Parameter()]
        [int]
        $CacheTtl = -1
    )

    # has the secret been mounted already?
    if (Test-PodeSecret -Name $Name) {
        throw "A Secret with the name '$($Name)' has already been mounted"
    }

    # does the vault exist?
    if (!(Test-PodeSecretVault -Name $Vault)) {
        throw "No Secret Vault with the name '$($Vault)' has been registered"
    }

    # check properties
    if (!(Test-PodeIsEmpty $Property) -and !(Test-PodeIsEmpty $ExpandProperty)) {
        throw 'You can only provide one of either Property or ExpandPropery, but not both'
    }

    # which cache value?
    if ($CacheTtl -lt 0) {
        $CacheTtl = [int]$PodeContext.Server.Secrets.Vaults[$Vault].Cache.Ttl
    }

    # mount secret reference
    $props = $Property
    if (![string]::IsNullOrWhiteSpace($ExpandProperty)) {
        $props = $ExpandProperty
    }

    $PodeContext.Server.Secrets.Keys[$Name] = @{
        Key        = $Key
        Properties = @{
            Fields  = $props
            Expand  = (![string]::IsNullOrWhiteSpace($ExpandProperty))
            Enabled = (!(Test-PodeIsEmpty $props))
        }
        Vault      = $Vault
        Arguments  = $ArgumentList
        Cache      = @{
            Ttl     = $CacheTtl
            Enabled = ($CacheTtl -gt 0)
        }
    }
}

<#
.SYNOPSIS
Dismount a previously mounted Secret.

.DESCRIPTION
Dismount a previously mounted Secret.

.PARAMETER Name
The friendly Name of the Secret.

.PARAMETER Remove
If supplied, the Secret will also be removed from the Secret Vault as well.

.EXAMPLE
Dismount-PodeSecret -Name 'SecretName'

.EXAMPLE
Dismount-PodeSecret -Name 'SecretName' -Remove
#>
function Dismount-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $Remove
    )

    # do nothing if the secret hasn't been mounted, unless Remove is specified
    if (!(Test-PodeSecret -Name $Name)) {
        if ($Remove) {
            throw "No Secret with the name '$($Name)' has been mounted to be removed from a Secret Vault"
        }

        return
    }

    # if "remove" switch passed, remove the secret from the vault as well
    if ($Remove) {
        $secret = $PodeContext.Server.Secrets.Keys[$Name]
        Remove-PodeSecret -Key $secret.Key -Vault $secret.Vault -ArgumentList $secret.Arguments
    }

    # remove reference
    $null = $PodeContext.Server.Secrets.Keys.Remove($Name)
}

<#
.SYNOPSIS
Retrieve the value of a mounted Secret.

.DESCRIPTION
Retrieve the value of a mounted Secret from a Secret Vault. You can also use "$value = $secret:<NAME>" syntax in certain places.

.PARAMETER Name
The friendly Name of a Secret.

.EXAMPLE
$value = Get-PodeSecret -Name 'SecretName'

.EXAMPLE
$value = $secret:SecretName
#>
function Get-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # has the secret been mounted?
    if (!(Test-PodeSecret -Name $Name)) {
        throw "No Secret with the name '$($Name)' has been mounted"
    }

    # get the secret and vault
    $secret = $PodeContext.Server.Secrets.Keys[$Name]

    # is the value cached?
    if ($secret.Cache.Enabled -and ($null -ne $secret.Cache.Expiry) -and ($secret.Cache.Expiry -gt [datetime]::UtcNow)) {
        return $secret.Cache.Value
    }

    # fetch the secret depending on vault type
    switch ($PodeContext.Server.Secrets.Vaults[$secret.Vault].Type) {
        'custom' {
            $value = Get-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -ArgumentList $secret.Arguments
        }

        'secretmanagement' {
            $value = Get-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key
        }
    }

    # filter the value by any properties
    if ($secret.Properties.Enabled) {
        if ($secret.Properties.Expand) {
            $value = Select-Object -InputObject $value -ExpandProperty $secret.Properties.Fields
        }
        else {
            $value = Select-Object -InputObject $value -Property $secret.Properties.Fields
        }
    }

    # cache the value if needed
    if ($secret.Cache.Enabled) {
        $secret.Cache.Value = $value
        $secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl)
    }

    # return value
    return $value
}

<#
.SYNOPSIS
Test if a Secret has been mounted.

.DESCRIPTION
Test if a Secret has been mounted.

.PARAMETER Name
The friendly Name of a Secret.

.EXAMPLE
if (Test-PodeSecret -Name 'SecretName') { ... }
#>
function Test-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (($null -ne $PodeContext.Server.Secrets.Keys) -and $PodeContext.Server.Secrets.Keys.ContainsKey($Name))
}

<#
.SYNOPSIS
Update the value of a mounted Secret.

.DESCRIPTION
Update the value of a mounted Secret in a Secret Vault. You can also use "$secret:<NAME> = $value" syntax in certain places.

.PARAMETER Name
The friendly Name of a Secret.

.PARAMETER InputObject
The value to use when updating the Secret.
Only the following object types are supported: byte[], string, securestring, pscredential, hashtable.

.PARAMETER Metadata
An optional Metadata hashtable.

.EXAMPLE
Update-PodeSecret -Name 'SecretName' -InputObject @{ key = value }

.EXAMPLE
Update-PodeSecret -Name 'SecretName' -InputObject 'value'

.EXAMPLE
$secret:SecretName = 'value'
#>
function Update-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        #> byte[], string, securestring, pscredential, hashtable
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [hashtable]
        $Metadata
    )

    # has the secret been mounted?
    if (!(Test-PodeSecret -Name $Name)) {
        throw "No Secret with the name '$($Name)' has been mounted"
    }

    # make sure the value type is correct
    $InputObject = Protect-PodeSecretValueType -Value $InputObject

    # get the secret and vault
    $secret = $PodeContext.Server.Secrets.Keys[$Name]

    # reset the cache if enabled
    if ($secret.Cache.Enabled) {
        $secret.Cache.Value = $InputObject
        $secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl)
    }

    # if we're expanding a property, convert this to a hashtable
    if ($secret.Properties.Enabled -and $secret.Properties.Expand) {
        $InputObject = @{
            "$($secret.Properties.Fields)" = $InputObject
        }
    }

    # set the secret depending on vault type
    switch ($PodeContext.Server.Secrets.Vaults[$secret.Vault].Type) {
        'custom' {
            Set-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata -ArgumentList $secret.Arguments
        }

        'secretmanagement' {
            Set-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata
        }
    }
}

<#
.SYNOPSIS
Remove a Secret from a Secret Vault.

.DESCRIPTION
Remove a Secret from a Secret Vault. To remove a mounted Secret, you can pass the Remove switch to Dismount-PodeSecret.

.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.

.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.

.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.

.EXAMPLE
Remove-PodeSecret -Key 'path/to/secret' -Vault 'VaultName'
#>
function Remove-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # has the vault been registered?
    if (!(Test-PodeSecretVault -Name $Vault)) {
        throw "No Secret Vault with the name '$($Vault)' has been registered"
    }

    # remove the secret depending on vault type
    switch ($PodeContext.Server.Secrets.Vaults[$Vault].Type) {
        'custom' {
            Remove-PodeSecretCustomKey -Vault $Vault -Key $Key -ArgumentList $ArgumentList
        }

        'secretmanagement' {
            Remove-PodeSecretManagementKey -Vault $Vault -Key $Key
        }
    }
}

<#
.SYNOPSIS
Read a Secret from a Secret Vault.

.DESCRIPTION
Read a Secret from a Secret Vault.

.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.

.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.

.PARAMETER Property
An optional array of Properties to be returned if the Secret contains multiple properties.

.PARAMETER ExpandProperty
An optional Property to be expanded from the Secret and return if it contains multiple properties.

.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.

.EXAMPLE
$value = Read-PodeSecret -Key 'path/to/secret' -Vault 'VaultName'

.EXAMPLE
$value = Read-PodeSecret -Key 'key_of_secret' -Vault 'VaultName' -Property prop1, prop2
#>
function Read-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter()]
        [string[]]
        $Property,

        [Parameter()]
        [string]
        $ExpandProperty,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # has the vault been registered?
    if (!(Test-PodeSecretVault -Name $Vault)) {
        throw "No Secret Vault with the name '$($Vault)' has been registered"
    }

    # fetch the secret depending on vault type
    switch ($PodeContext.Server.Secrets.Vaults[$Vault].Type) {
        'custom' {
            $value = Get-PodeSecretCustomKey -Vault $Vault -Key $Key -ArgumentList $ArgumentList
        }

        'secretmanagement' {
            $value = Get-PodeSecretManagementKey -Vault $Vault -Key $Key
        }
    }

    # filter the value by any properties
    if (![string]::IsNullOrWhiteSpace($ExpandProperty)) {
        $value = Select-Object -InputObject $value -ExpandProperty $ExpandProperty
    }
    elseif (![string]::IsNullOrEmpty($Property)) {
        $value = Select-Object -InputObject $value -Property $Property
    }

    # return value
    return $value
}

<#
.SYNOPSIS
Create/update a Secret in a Secret Vault.

.DESCRIPTION
Create/update a Secret in a Secret Vault.

.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.

.PARAMETER Vault
The friendly name of the Secret Vault this Secret should be created in.

.PARAMETER InputObject
The value to use when updating the Secret.
Only the following object types are supported: byte[], string, securestring, pscredential, hashtable.

.PARAMETER Metadata
An optional Metadata hashtable.

.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.

.EXAMPLE
Set-PodeSecret -Key 'path/to/secret' -Vault 'VaultName' -InputObject 'value'

.EXAMPLE
Set-PodeSecret -Key 'key_of_secret' -Vault 'VaultName' -InputObject @{ key = value }
#>
function Set-PodeSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        #> byte[], string, securestring, pscredential, hashtable
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [hashtable]
        $Metadata,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # has the vault been registered?
    if (!(Test-PodeSecretVault -Name $Vault)) {
        throw "No Secret Vault with the name '$($Vault)' has been registered"
    }

    # make sure the value type is correct
    $InputObject = Protect-PodeSecretValueType -Value $InputObject

    # set the secret depending on vault type
    switch ($PodeContext.Server.Secrets.Vaults[$Vault].Type) {
        'custom' {
            Set-PodeSecretCustomKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata -ArgumentList $ArgumentList
        }

        'secretmanagement' {
            Set-PodeSecretManagementKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata
        }
    }
}
src\Public\Security.ps1
<#
.SYNOPSIS
Sets inbuilt definitions for security headers.

.DESCRIPTION
Sets inbuilt definitions for security headers, in either Simple or Strict types.

.PARAMETER Type
The Type of security to use.

.PARAMETER UseHsts
If supplied, the Strict-Transport-Security header will be set.

.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)

.EXAMPLE
Set-PodeSecurity -Type Simple

.EXAMPLE
Set-PodeSecurity -Type Strict -UseHsts
#>
function Set-PodeSecurity {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Simple', 'Strict')]
        [string]
        $Type,

        [switch]
        $UseHsts,

        [switch]
        $XssBlock
    )

    # general headers
    Set-PodeSecurityContentTypeOptions

    Set-PodeSecurityPermissionsPolicy `
        -SyncXhr 'none' `
        -Fullscreen 'self' `
        -Camera 'none' `
        -Geolocation 'self' `
        -PictureInPicture 'self' `
        -Accelerometer 'none' `
        -Microphone 'none' `
        -Usb 'none' `
        -Autoplay 'self' `
        -Payment 'none' `
        -Magnetometer 'self' `
        -Gyroscope 'self' `
        -DisplayCapture 'self'

    Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin
    Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200
    Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock

    # only add hsts if specifiec
    if ($UseHsts) {
        Set-PodeSecurityStrictTransportSecurity -Duration 31536000 -IncludeSubDomains
    }

    # type specific headers
    switch ($Type.ToLowerInvariant()) {
        'simple' {
            Set-PodeSecurityFrameOptions -Type SameOrigin
            Set-PodeSecurityReferrerPolicy -Type Strict-Origin
        }

        'strict' {
            Set-PodeSecurityFrameOptions -Type Deny
            Set-PodeSecurityReferrerPolicy -Type No-Referrer
        }
    }

    # hide server info
    Hide-PodeSecurityServer
}

<#
.SYNOPSIS
Removes definitions for all security headers.

.DESCRIPTION
Removes definitions for all security headers.

.EXAMPLE
Remove-PodeSecurity
#>
function Remove-PodeSecurity {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Security.Headers.Clear()
    Show-PodeSecurityServer
}

<#
.SYNOPSIS
Add definition for specified security header.

.DESCRIPTION
Add definition for specified security header.

.PARAMETER Name
The Name of the security header.

.PARAMETER Value
The Value of the security header.

.PARAMETER Append
Append the value to the header instead of replacing it

.EXAMPLE
Add-PodeSecurityHeader -Name 'X-Header-Name' -Value 'SomeValue'
#>
function Add-PodeSecurityHeader {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [switch]
        $Append
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return
    }

    if ($Append -and $PodeContext.Server.Security.Headers.ContainsKey($Name)) {
        $Headers = @(($PodeContext.Server.Security.Headers[$Name].split(',')).trim())
        if ($Headers -inotcontains $Value) {
            $Headers += $Value
            $PodeContext.Server.Security.Headers[$Name] = (($Headers.trim() | Select-Object -Unique) -join ', ')
        }
        else {
            return
        }
    }
    else {
        $PodeContext.Server.Security.Headers[$Name] = $Value
    }
}

<#
.SYNOPSIS
Removes definition for specified security header.

.DESCRIPTION
Removes definition for specified security header.

.PARAMETER Name
The Name of the security header.

.EXAMPLE
Remove-PodeSecurityHeader -Name 'X-Header-Name'
#>
function Remove-PodeSecurityHeader {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $PodeContext.Server.Security.Headers.Remove($Name)
}

<#
.SYNOPSIS
Hide the Server HTTP Header from Responses

.DESCRIPTION
Hide the Server HTTP Header from Responses

.EXAMPLE
Hide-PodeSecurityServer
#>
function Hide-PodeSecurityServer {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Security.ServerDetails = $false
}

<#
.SYNOPSIS
Show the Server HTTP Header on Responses

.DESCRIPTION
Show the Server HTTP Header on Responses

.EXAMPLE
Show-PodeSecurityServer
#>
function Show-PodeSecurityServer {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Security.ServerDetails = $true
}

<#
.SYNOPSIS
Set a value for the X-Frame-Options header.

.DESCRIPTION
Set a value for the X-Frame-Options header.

.PARAMETER Type
The Type to use.

.EXAMPLE
Set-PodeSecurityFrameOptions -Type SameOrigin
#>
function Set-PodeSecurityFrameOptions {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Deny', 'SameOrigin')]
        [string]
        $Type
    )

    Add-PodeSecurityHeader -Name 'X-Frame-Options' -Value $Type.ToUpperInvariant()
}

<#
.SYNOPSIS
Removes definition for the X-Frame-Options header.

.DESCRIPTION
Removes definition for the X-Frame-Options header.

.EXAMPLE
Remove-PodeSecurityFrameOptions
#>
function Remove-PodeSecurityFrameOptions {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'X-Frame-Options'
}

<#
.SYNOPSIS
Set the value to use for the Content-Security-Policy and X-XSS-Protection headers.

.DESCRIPTION
Set the value to use for the Content-Security-Policy and X-XSS-Protection headers.

.PARAMETER Default
The values to use for the Default portion of the header.

.PARAMETER Child
The values to use for the Child portion of the header.

.PARAMETER Connect
The values to use for the Connect portion of the header.

.PARAMETER Font
The values to use for the Font portion of the header.

.PARAMETER Frame
The values to use for the Frame portion of the header.

.PARAMETER Image
The values to use for the Image portion of the header.

.PARAMETER Manifest
The values to use for the Manifest portion of the header.

.PARAMETER Media
The values to use for the Media portion of the header.

.PARAMETER Object
The values to use for the Object portion of the header.

.PARAMETER Scripts
The values to use for the Scripts portion of the header.

.PARAMETER Style
The values to use for the Style portion of the header.

.PARAMETER BaseUri
The values to use for the BaseUri portion of the header.

.PARAMETER FormAction
The values to use for the FormAction portion of the header.

.PARAMETER FrameAncestor
The values to use for the FrameAncestor portion of the header.

.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.

.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.

.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)

.EXAMPLE
Set-PodeSecurityContentSecurityPolicy -Default 'self'
#>
function Set-PodeSecurityContentSecurityPolicy {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Default,

        [Parameter()]
        [string[]]
        $Child,

        [Parameter()]
        [string[]]
        $Connect,

        [Parameter()]
        [string[]]
        $Font,

        [Parameter()]
        [string[]]
        $Frame,

        [Parameter()]
        [string[]]
        $Image,

        [Parameter()]
        [string[]]
        $Manifest,

        [Parameter()]
        [string[]]
        $Media,

        [Parameter()]
        [string[]]
        $Object,

        [Parameter()]
        [string[]]
        $Scripts,

        [Parameter()]
        [string[]]
        $Style,

        [Parameter()]
        [string[]]
        $BaseUri,

        [Parameter()]
        [string[]]
        $FormAction,

        [Parameter()]
        [string[]]
        $FrameAncestor,

        [Parameter()]
        [ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
            'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
            'Allow-Storage-Access-By-User-Activation', 'Allow-Top-Navigation', 'Allow-Top-Navigation-By-User-Activation', 'None')]
        [string]
        $Sandbox = 'None',

        [switch]
        $UpgradeInsecureRequests,

        [switch]
        $XssBlock
    )

    # build the header's value
    $values = @(
        Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default
        Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child
        Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect
        Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font
        Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame
        Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image
        Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest
        Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media
        Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object
        Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts
        Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style
        Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri
        Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction
        Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor
    )

    if ($Sandbox -ine 'None') {
        $values += "sandbox $($Sandbox.ToLowerInvariant())".Trim()
    }

    if ($UpgradeInsecureRequests) {
        $values += 'upgrade-insecure-requests'
    }

    $values = ($values -ne $null)
    $value = ($values -join '; ')

    # add the header
    Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value

    # this is done to explicitly disable XSS auditors in modern browsers
    # as having it enabled has now been found to cause more vulnerabilities
    if ($XssBlock) {
        Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '1; mode=block'
    }
    else {
        Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '0'
    }
}

<#
.SYNOPSIS
Adds additional values to already defined values for the Content-Security-Policy header.

.DESCRIPTION
Adds additional values to already defined values for the Content-Security-Policy header, instead of overriding them.

.PARAMETER Default
The values to add for the Default portion of the header.

.PARAMETER Child
The values to add for the Child portion of the header.

.PARAMETER Connect
The values to add for the Connect portion of the header.

.PARAMETER Font
The values to add for the Font portion of the header.

.PARAMETER Frame
The values to add for the Frame portion of the header.

.PARAMETER Image
The values to add for the Image portion of the header.

.PARAMETER Manifest
The values to add for the Manifest portion of the header.

.PARAMETER Media
The values to add for the Media portion of the header.

.PARAMETER Object
The values to add for the Object portion of the header.

.PARAMETER Scripts
The values to add for the Scripts portion of the header.

.PARAMETER Style
The values to add for the Style portion of the header.

.PARAMETER BaseUri
The values to add for the BaseUri portion of the header.

.PARAMETER FormAction
The values to add for the FormAction portion of the header.

.PARAMETER FrameAncestor
The values to add for the FrameAncestor portion of the header.

.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.

.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.

.EXAMPLE
Add-PodeSecurityContentSecurityPolicy -Default '*.twitter.com' -Image 'data'
#>
function Add-PodeSecurityContentSecurityPolicy {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Default,

        [Parameter()]
        [string[]]
        $Child,

        [Parameter()]
        [string[]]
        $Connect,

        [Parameter()]
        [string[]]
        $Font,

        [Parameter()]
        [string[]]
        $Frame,

        [Parameter()]
        [string[]]
        $Image,

        [Parameter()]
        [string[]]
        $Manifest,

        [Parameter()]
        [string[]]
        $Media,

        [Parameter()]
        [string[]]
        $Object,

        [Parameter()]
        [string[]]
        $Scripts,

        [Parameter()]
        [string[]]
        $Style,

        [Parameter()]
        [string[]]
        $BaseUri,

        [Parameter()]
        [string[]]
        $FormAction,

        [Parameter()]
        [string[]]
        $FrameAncestor,

        [Parameter()]
        [ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
            'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
            'Allow-Storage-Access-By-User-Activation', 'Allow-Top-Navigation', 'Allow-Top-Navigation-By-User-Activation', 'None')]
        [string]
        $Sandbox = 'None',

        [switch]
        $UpgradeInsecureRequests
    )

    # build the header's value
    $values = @(
        Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default -Append
        Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child -Append
        Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect -Append
        Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font -Append
        Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame -Append
        Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image -Append
        Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest -Append
        Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media -Append
        Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object -Append
        Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts -Append
        Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style -Append
        Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri -Append
        Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction -Append
        Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor -Append
    )

    if ($Sandbox -ine 'None') {
        $values += "sandbox $($Sandbox.ToLowerInvariant())".Trim()
    }

    if ($UpgradeInsecureRequests) {
        $values += 'upgrade-insecure-requests'
    }

    $values = ($values -ne $null)
    $value = ($values -join '; ')

    # add the header
    Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value
}

<#
.SYNOPSIS
Removes definition for the Content-Security-Policy and X-XSS-Protection headers.

.DESCRIPTION
Removes definition for the Content-Security-Policy and X-XSS-Protection headers.

.EXAMPLE
Remove-PodeSecurityContentSecurityPolicy
#>
function Remove-PodeSecurityContentSecurityPolicy {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Content-Security-Policy'
    Remove-PodeSecurityHeader -Name 'X-XSS-Protection'
}

<#
.SYNOPSIS
Set the value to use for the Permissions-Policy header.

.DESCRIPTION
Set the value to use for the Permissions-Policy header.

.PARAMETER Accelerometer
The values to use for the Accelerometer portion of the header.

.PARAMETER AmbientLightSensor
The values to use for the AmbientLightSensor portion of the header.

.PARAMETER Autoplay
The values to use for the Autoplay portion of the header.

.PARAMETER Battery
The values to use for the Battery portion of the header.

.PARAMETER Camera
The values to use for the Camera portion of the header.

.PARAMETER DisplayCapture
The values to use for the DisplayCapture portion of the header.

.PARAMETER DocumentDomain
The values to use for the DocumentDomain portion of the header.

.PARAMETER EncryptedMedia
The values to use for the EncryptedMedia portion of the header.

.PARAMETER Fullscreen
The values to use for the Fullscreen portion of the header.

.PARAMETER Gamepad
The values to use for the Gamepad portion of the header.

.PARAMETER Geolocation
The values to use for the Geolocation portion of the header.

.PARAMETER Gyroscope
The values to use for the Gyroscope portion of the header.

.PARAMETER InterestCohort
The values to use for the InterestCohort portal of the header.

.PARAMETER LayoutAnimations
The values to use for the LayoutAnimations portion of the header.

.PARAMETER LegacyImageFormats
The values to use for the LegacyImageFormats portion of the header.

.PARAMETER Magnetometer
The values to use for the Magnetometer portion of the header.

.PARAMETER Microphone
The values to use for the Microphone portion of the header.

.PARAMETER Midi
The values to use for the Midi portion of the header.

.PARAMETER OversizedImages
The values to use for the OversizedImages portion of the header.

.PARAMETER Payment
The values to use for the Payment portion of the header.

.PARAMETER PictureInPicture
The values to use for the PictureInPicture portion of the header.

.PARAMETER PublicKeyCredentials
The values to use for the PublicKeyCredentials portion of the header.

.PARAMETER Speakers
The values to use for the Speakers portion of the header.

.PARAMETER SyncXhr
The values to use for the SyncXhr portion of the header.

.PARAMETER UnoptimisedImages
The values to use for the UnoptimisedImages portion of the header.

.PARAMETER UnsizedMedia
The values to use for the UnsizedMedia portion of the header.

.PARAMETER Usb
The values to use for the Usb portion of the header.

.PARAMETER ScreenWakeLake
The values to use for the ScreenWakeLake portion of the header.

.PARAMETER WebShare
The values to use for the WebShare portion of the header.

.PARAMETER XrSpatialTracking
The values to use for the XrSpatialTracking portion of the header.

.EXAMPLE
Set-PodeSecurityPermissionsPolicy -LayoutAnimations 'none' -UnoptimisedImages 'none' -OversizedImages 'none' -SyncXhr 'none' -UnsizedMedia 'none'
#>
function Set-PodeSecurityPermissionsPolicy {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Accelerometer,

        [Parameter()]
        [string[]]
        $AmbientLightSensor,

        [Parameter()]
        [string[]]
        $Autoplay,

        [Parameter()]
        [string[]]
        $Battery,

        [Parameter()]
        [string[]]
        $Camera,

        [Parameter()]
        [string[]]
        $DisplayCapture,

        [Parameter()]
        [string[]]
        $DocumentDomain,

        [Parameter()]
        [string[]]
        $EncryptedMedia,

        [Parameter()]
        [string[]]
        $Fullscreen,

        [Parameter()]
        [string[]]
        $Gamepad,

        [Parameter()]
        [string[]]
        $Geolocation,

        [Parameter()]
        [string[]]
        $Gyroscope,

        [Parameter()]
        [string[]]
        $InterestCohort,

        [Parameter()]
        [string[]]
        $LayoutAnimations,

        [Parameter()]
        [string[]]
        $LegacyImageFormats,

        [Parameter()]
        [string[]]
        $Magnetometer,

        [Parameter()]
        [string[]]
        $Microphone,

        [Parameter()]
        [string[]]
        $Midi,

        [Parameter()]
        [string[]]
        $OversizedImages,

        [Parameter()]
        [string[]]
        $Payment,

        [Parameter()]
        [string[]]
        $PictureInPicture,

        [Parameter()]
        [string[]]
        $PublicKeyCredentials,

        [Parameter()]
        [string[]]
        $Speakers,

        [Parameter()]
        [string[]]
        $SyncXhr,

        [Parameter()]
        [string[]]
        $UnoptimisedImages,

        [Parameter()]
        [string[]]
        $UnsizedMedia,

        [Parameter()]
        [string[]]
        $Usb,

        [Parameter()]
        [string[]]
        $ScreenWakeLake,

        [Parameter()]
        [string[]]
        $WebShare,

        [Parameter()]
        [string[]]
        $XrSpatialTracking
    )

    # build the header's value
    $values = @(
        Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer
        Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor
        Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay
        Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery
        Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera
        Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture
        Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain
        Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia
        Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen
        Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad
        Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation
        Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope
        Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort
        Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations
        Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats
        Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer
        Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone
        Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi
        Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages
        Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment
        Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture
        Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials
        Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers
        Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr
        Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages
        Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia
        Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb
        Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake
        Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare
        Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking
    )

    $values = ($values -ne $null)
    $value = ($values -join ', ')

    # add the header
    Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value
}

<#
.SYNOPSIS
Adds additional values to already defined values for the Permissions-Policy header.

.DESCRIPTION
Adds additional values to already defined values for the Permissions-Policy header, instead of overriding them.

.PARAMETER Accelerometer
The values to add for the Accelerometer portion of the header.

.PARAMETER AmbientLightSensor
The values to add for the AmbientLightSensor portion of the header.

.PARAMETER Autoplay
The values to add for the Autoplay portion of the header.

.PARAMETER Battery
The values to add for the Battery portion of the header.

.PARAMETER Camera
The values to add for the Camera portion of the header.

.PARAMETER DisplayCapture
The values to add for the DisplayCapture portion of the header.

.PARAMETER DocumentDomain
The values to add for the DocumentDomain portion of the header.

.PARAMETER EncryptedMedia
The values to add for the EncryptedMedia portion of the header.

.PARAMETER Fullscreen
The values to add for the Fullscreen portion of the header.

.PARAMETER Gamepad
The values to add for the Gamepad portion of the header.

.PARAMETER Geolocation
The values to add for the Geolocation portion of the header.

.PARAMETER Gyroscope
The values to add for the Gyroscope portion of the header.

.PARAMETER InterestCohort
The values to use for the InterestCohort portal of the header.

.PARAMETER LayoutAnimations
The values to add for the LayoutAnimations portion of the header.

.PARAMETER LegacyImageFormats
The values to add for the LegacyImageFormats portion of the header.

.PARAMETER Magnetometer
The values to add for the Magnetometer portion of the header.

.PARAMETER Microphone
The values to add for the Microphone portion of the header.

.PARAMETER Midi
The values to add for the Midi portion of the header.

.PARAMETER OversizedImages
The values to add for the OversizedImages portion of the header.

.PARAMETER Payment
The values to add for the Payment portion of the header.

.PARAMETER PictureInPicture
The values to add for the PictureInPicture portion of the header.

.PARAMETER PublicKeyCredentials
The values to add for the PublicKeyCredentials portion of the header.

.PARAMETER Speakers
The values to add for the Speakers portion of the header.

.PARAMETER SyncXhr
The values to add for the SyncXhr portion of the header.

.PARAMETER UnoptimisedImages
The values to add for the UnoptimisedImages portion of the header.

.PARAMETER UnsizedMedia
The values to add for the UnsizedMedia portion of the header.

.PARAMETER Usb
The values to add for the Usb portion of the header.

.PARAMETER ScreenWakeLake
The values to add for the ScreenWakeLake portion of the header.

.PARAMETER WebShare
The values to add for the WebShare portion of the header.

.PARAMETER XrSpatialTracking
The values to add for the XrSpatialTracking portion of the header.

.EXAMPLE
Add-PodeSecurityPermissionsPolicy -AmbientLightSensor 'none'
#>
function Add-PodeSecurityPermissionsPolicy {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Accelerometer,

        [Parameter()]
        [string[]]
        $AmbientLightSensor,

        [Parameter()]
        [string[]]
        $Autoplay,

        [Parameter()]
        [string[]]
        $Battery,

        [Parameter()]
        [string[]]
        $Camera,

        [Parameter()]
        [string[]]
        $DisplayCapture,

        [Parameter()]
        [string[]]
        $DocumentDomain,

        [Parameter()]
        [string[]]
        $EncryptedMedia,

        [Parameter()]
        [string[]]
        $Fullscreen,

        [Parameter()]
        [string[]]
        $Gamepad,

        [Parameter()]
        [string[]]
        $Geolocation,

        [Parameter()]
        [string[]]
        $Gyroscope,

        [Parameter()]
        [string[]]
        $InterestCohort,

        [Parameter()]
        [string[]]
        $LayoutAnimations,

        [Parameter()]
        [string[]]
        $LegacyImageFormats,

        [Parameter()]
        [string[]]
        $Magnetometer,

        [Parameter()]
        [string[]]
        $Microphone,

        [Parameter()]
        [string[]]
        $Midi,

        [Parameter()]
        [string[]]
        $OversizedImages,

        [Parameter()]
        [string[]]
        $Payment,

        [Parameter()]
        [string[]]
        $PictureInPicture,

        [Parameter()]
        [string[]]
        $PublicKeyCredentials,

        [Parameter()]
        [string[]]
        $Speakers,

        [Parameter()]
        [string[]]
        $SyncXhr,

        [Parameter()]
        [string[]]
        $UnoptimisedImages,

        [Parameter()]
        [string[]]
        $UnsizedMedia,

        [Parameter()]
        [string[]]
        $Usb,

        [Parameter()]
        [string[]]
        $ScreenWakeLake,

        [Parameter()]
        [string[]]
        $WebShare,

        [Parameter()]
        [string[]]
        $XrSpatialTracking
    )

    # build the header's value
    $values = @(
        Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer -Append
        Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor -Append
        Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay -Append
        Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery -Append
        Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera -Append
        Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture -Append
        Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain -Append
        Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia -Append
        Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen -Append
        Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad -Append
        Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation -Append
        Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope -Append
        Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort -Append
        Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations -Append
        Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats -Append
        Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer -Append
        Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone -Append
        Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi -Append
        Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages -Append
        Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment -Append
        Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture -Append
        Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials -Append
        Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers -Append
        Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr -Append
        Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages -Append
        Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia -Append
        Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb -Append
        Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake -Append
        Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare -Append
        Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking -Append
    )

    $values = ($values -ne $null)
    $value = ($values -join ', ')

    # add the header
    Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value
}

<#
.SYNOPSIS
Removes definition for the Permissions-Policy header.

.DESCRIPTION
Removes definitions for the Permissions-Policy header.

.EXAMPLE
Remove-PodeSecurityPermissionsPolicy
#>
function Remove-PodeSecurityPermissionsPolicy {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Permissions-Policy'
}

<#
.SYNOPSIS
Set a value for the Referrer-Policy header.

.DESCRIPTION
Set a value for the Referrer-Policy header.

.PARAMETER Type
The Type to use.

.EXAMPLE
Set-PodeSecurityReferrerPolicy -Type No-Referrer
#>
function Set-PodeSecurityReferrerPolicy {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('No-Referrer', 'No-Referrer-When-Downgrade', 'Same-Origin', 'Origin', 'Strict-Origin',
            'Origin-When-Cross-Origin', 'Strict-Origin-When-Cross-Origin', 'Unsafe-Url')]
        [string]
        $Type
    )

    Add-PodeSecurityHeader -Name 'Referrer-Policy' -Value $Type.ToLowerInvariant()
}

<#
.SYNOPSIS
Removes definition for the Referrer-Policy header.

.DESCRIPTION
Removes definitions for the Referrer-Policy header.

.EXAMPLE
Remove-PodeSecurityReferrerPolicy
#>
function Remove-PodeSecurityReferrerPolicy {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Referrer-Policy'
}

<#
.SYNOPSIS
Set a value for the X-Content-Type-Options header.

.DESCRIPTION
Set a value for the X-Content-Type-Options header to "nosniff".

.EXAMPLE
Set-PodeSecurityContentTypeOptions
#>
function Set-PodeSecurityContentTypeOptions {
    [CmdletBinding()]
    param()

    Add-PodeSecurityHeader -Name 'X-Content-Type-Options' -Value 'nosniff'
}

<#
.SYNOPSIS
Removes definition for the X-Content-Type-Options header.

.DESCRIPTION
Removes definitions for the X-Content-Type-Options header.

.EXAMPLE
Remove-PodeSecurityContentTypeOptions
#>
function Remove-PodeSecurityContentTypeOptions {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'X-Content-Type-Options'
}

<#
.SYNOPSIS
Set a value for the Strict-Transport-Security header.

.DESCRIPTION
Set a value for the Strict-Transport-Security header.

.PARAMETER Duration
The Duration the browser to respect the header in seconds. (Default: 1 year)

.PARAMETER IncludeSubDomains
If supplied, the header will have includeSubDomains.

.EXAMPLE
Set-PodeSecurityStrictTransportSecurity -Duration 86400 -IncludeSubDomains
#>
function Set-PodeSecurityStrictTransportSecurity {
    [CmdletBinding()]
    param(
        [Parameter()]
        [int]
        $Duration = 31536000,

        [switch]
        $IncludeSubDomains
    )

    if ($Duration -le 0) {
        throw "Invalid Strict-Transport-Security duration supplied: $($Duration). Should be greater than 0"
    }

    $value = "max-age=$($Duration)"

    if ($IncludeSubDomains) {
        $value += '; includeSubDomains'
    }

    Add-PodeSecurityHeader -Name 'Strict-Transport-Security' -Value $value
}

<#
.SYNOPSIS
Removes definition for the Strict-Transport-Security header.

.DESCRIPTION
Removes definitions for the Strict-Transport-Security header.

.EXAMPLE
Remove-PodeSecurityStrictTransportSecurity
#>
function Remove-PodeSecurityStrictTransportSecurity {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Strict-Transport-Security'
}

<#
.SYNOPSIS
Removes definitions for the Cross-Origin headers.

.DESCRIPTION
Removes definitions for the Cross-Origin headers: Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy

.PARAMETER Embed
Specifies a value for Cross-Origin-Embedder-Policy.

.PARAMETER Open
Specifies a value for Cross-Origin-Opener-Policy.

.PARAMETER Resource
Specifies a value for Cross-Origin-Resource-Policy.

.EXAMPLE
Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin
#>
function Set-PodeSecurityCrossOrigin {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('', 'Unsafe-None', 'Require-Corp')]
        [string]
        $Embed = '',

        [Parameter()]
        [ValidateSet('', 'Unsafe-None', 'Same-Origin-Allow-Popups', 'Same-Origin')]
        [string]
        $Open = '',

        [Parameter()]
        [ValidateSet('', 'Same-Site', 'Same-Origin', 'Cross-Origin')]
        [string]
        $Resource = ''
    )

    Add-PodeSecurityHeader -Name 'Cross-Origin-Embedder-Policy' -Value $Embed.ToLowerInvariant()
    Add-PodeSecurityHeader -Name 'Cross-Origin-Opener-Policy' -Value $Open.ToLowerInvariant()
    Add-PodeSecurityHeader -Name 'Cross-Origin-Resource-Policy' -Value $Resource.ToLowerInvariant()
}

<#
.SYNOPSIS
Removes definitions for the Cross-Origin headers.

.DESCRIPTION
Removes definitions for the Cross-Origin headers: Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy

.EXAMPLE
Remove-PodeSecurityCrossOrigin
#>
function Remove-PodeSecurityCrossOrigin {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Cross-Origin-Embedder-Policy'
    Remove-PodeSecurityHeader -Name 'Cross-Origin-Opener-Policy'
    Remove-PodeSecurityHeader -Name 'Cross-Origin-Resource-Policy'
}

<#
.SYNOPSIS
Set definitions for Access-Control headers.

.DESCRIPTION
Removes definitions for the Access-Control headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age, Access-Control-Allow-Credentials

.PARAMETER Origin
Specifies a value for Access-Control-Allow-Origin.

.PARAMETER Methods
Specifies a value for Access-Control-Allow-Methods.

.PARAMETER Headers
Specifies a value for Access-Control-Allow-Headers.

.PARAMETER Duration
Specifies a value for Access-Control-Max-Age in seconds. (Default: 7200)
Use a value of one for debugging any CORS related issues

.PARAMETER Credentials
Specifies a value for Access-Control-Allow-Credentials

.PARAMETER WithOptions
If supplied, a global Options Route will be created.

.PARAMETER AuthorizationHeader
Add 'Authorization' to the headers list

.PARAMETER AutoHeaders
Automatically populate the list of allowed Headers based on the OpenApi definition.
This parameter can works in conjuntion with CrossDomainXhrRequests,AuthorizationHeader and Headers (Headers cannot be '*').
By default add  'content-type' to the headers

.PARAMETER AutoMethods
Automatically populate the list of allowed Methods based on the defined Routes.
This parameter can works in conjuntion with the parameter Methods, if Methods is not including '*'

.PARAMETER CrossDomainXhrRequests
Add 'x-requested-with' to the list of allowed headers
More info available here:
https://fetch.spec.whatwg.org/
https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-7.0#credentials-in-cross-origin-requests
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

.EXAMPLE
Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200
#>
function Set-PodeSecurityAccessControl {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Origin,

        [Parameter()]
        [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string[]]
        $Methods = '',

        [Parameter()]
        [string[]]
        $Headers,

        [Parameter()]
        [int]
        $Duration = 7200,

        [switch]
        $Credentials,

        [switch]
        $WithOptions,

        [switch]
        $AuthorizationHeader,

        [switch]
        $AutoHeaders,

        [switch]
        $AutoMethods,

        [switch]
        $CrossDomainXhrRequests
    )

    # origin
    Add-PodeSecurityHeader -Name 'Access-Control-Allow-Origin' -Value $Origin

    # methods
    if (![string]::IsNullOrWhiteSpace($Methods)) {
        if ($Methods -icontains '*') {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value '*'
        }
        else {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value ($Methods -join ', ')
        }
    }

    # headers
    if (![string]::IsNullOrWhiteSpace($Headers) -or $AuthorizationHeader -or $CrossDomainXhrRequests) {
        if ($Headers -icontains '*') {
            if ($Credentials) {
                throw 'The * wildcard for Headers, when Credentials is passed, will be taken as a literal string and not a wildcard'
            }

            $Headers = @('*')
        }

        if ($AuthorizationHeader) {
            if ([string]::IsNullOrWhiteSpace($Headers)) {
                $Headers = @()
            }

            $Headers += 'Authorization'
        }

        if ($CrossDomainXhrRequests) {
            if ([string]::IsNullOrWhiteSpace($Headers)) {
                $Headers = @()
            }
            $Headers += 'x-requested-with'
        }
        Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value (($Headers | Select-Object -Unique) -join ', ')
    }

    if ($AutoHeaders) {
        if ($Headers -icontains '*') {
            throw 'The * wildcard for Headers, is not comptatibile with the AutoHeaders switch'
        }

        Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value 'content-type' -Append
        $PodeContext.Server.Security.autoHeaders = $true
    }

    if ($AutoMethods) {
        if ($Methods -icontains '*') {
            throw 'The * wildcard for Methods, is not comptatibile with the AutoMethods switch'
        }
        if ($WithOptions) {
            Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value 'Options' -Append
        }
        $PodeContext.Server.Security.autoMethods = $true
    }

    # duration
    if ($Duration -le 0) {
        throw "Invalid Access-Control-Max-Age duration supplied: $($Duration). Should be greater than 0"
    }

    Add-PodeSecurityHeader -Name 'Access-Control-Max-Age' -Value $Duration

    # creds
    if ($Credentials) {
        Add-PodeSecurityHeader -Name 'Access-Control-Allow-Credentials' -Value 'true'
    }

    # opts route
    if ($WithOptions) {
        Add-PodeRoute -Method Options -Path * -ScriptBlock {
            Set-PodeResponseStatus -Code 200
        }
    }
}

<#
.SYNOPSIS
Removes definitions for the Access-Control headers.

.DESCRIPTION
Removes definitions for the Access-Control headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age, Access-Control-Allow-Credentials

.EXAMPLE
Remove-PodeSecurityAccessControl
#>
function Remove-PodeSecurityAccessControl {
    [CmdletBinding()]
    param()

    Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Origin'
    Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Methods'
    Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Headers'
    Remove-PodeSecurityHeader -Name 'Access-Control-Max-Age'
    Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Credentials'
}
src\Public\Sessions.ps1
<#
.SYNOPSIS
Enables Middleware for creating, retrieving and using Sessions within Pode.

.DESCRIPTION
Enables Middleware for creating, retrieving and using Sessions within Pode; with support for defining Session duration, and custom Storage.
If you're storing sessions outside of Pode, you must supply a Secret value so sessions aren't corrupted.

.PARAMETER Secret
An optional Secret to use when signing Sessions (Default: random GUID).

.PARAMETER Name
The name of the cookie/header used for the Session.

.PARAMETER Duration
The duration a Session should last for, before being expired.

.PARAMETER Generator
A custom ScriptBlock to generate a random unique SessionId. The value returned must be a String.

.PARAMETER Storage
A custom PSObject that defines methods for Delete, Get, and Set. This allow you to store Sessions in custom Storage such as Redis. A Secret is required.

.PARAMETER Scope
The Scope that the Session applies to, possible values are Browser and Tab (Default: Browser).
The Browser scope is the default logic, where authentication and general data for the sessions are shared across all tabs.
The Tab scope keep the authentication data shared across all tabs, but general data is separated across different tabs.
For the Tab scope, the "Tab ID" required will be sourced from the "X-PODE-SESSION-TAB-ID" header.

.PARAMETER Extend
If supplied, the Sessions will have their durations extended on each successful Request.

.PARAMETER HttpOnly
If supplied, the Session cookie will only be accessible to browsers.

.PARAMETER Secure
If supplied, the Session cookie will only be accessible over HTTPS Requests.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.PARAMETER UseHeaders
If supplied, Sessions will be sent back in a header on the Response with the Name supplied.

.EXAMPLE
Enable-PodeSessionMiddleware -Duration 120

.EXAMPLE
Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() }

.EXAMPLE
Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -UseHeaders -Strict
#>
function Enable-PodeSessionMiddleware {
    [CmdletBinding(DefaultParameterSetName = 'Cookies')]
    param(
        [Parameter()]
        [string]
        $Secret,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name = 'pode.sid',

        [Parameter()]
        [ValidateScript({
                if ($_ -lt 0) {
                    throw "Duration must be 0 or greater, but got: $($_)s"
                }

                return $true
            })]
        [int]
        $Duration = 0,

        [Parameter()]
        [scriptblock]
        $Generator,

        [Parameter()]
        [psobject]
        $Storage = $null,

        [Parameter()]
        [ValidateSet('Browser', 'Tab')]
        [string]
        $Scope = 'Browser',

        [switch]
        $Extend,

        [Parameter(ParameterSetName = 'Cookies')]
        [switch]
        $HttpOnly,

        [Parameter(ParameterSetName = 'Cookies')]
        [switch]
        $Secure,

        [switch]
        $Strict,

        [Parameter(ParameterSetName = 'Headers')]
        [switch]
        $UseHeaders
    )

    # check that session logic hasn't already been initialised
    if (Test-PodeSessionsEnabled) {
        throw 'Session Middleware has already been intialised'
    }

    # ensure the override store has the required methods
    if (!(Test-PodeIsEmpty $Storage)) {
        $members = @($Storage | Get-Member | Select-Object -ExpandProperty Name)
        @('delete', 'get', 'set') | ForEach-Object {
            if ($members -inotcontains $_) {
                throw "Custom session storage does not implement the required '$($_)()' method"
            }
        }
    }

    # verify the secret, set to guid if not supplied, or error if none and we have a storage
    if ([string]::IsNullOrEmpty($Secret)) {
        if (!(Test-PodeIsEmpty $Storage)) {
            throw 'A Secret is required when using custom session storage'
        }

        $Secret = Get-PodeServerDefaultSecret
    }

    # if no custom storage, use the inmem one
    if (Test-PodeIsEmpty $Storage) {
        $Storage = (Get-PodeSessionInMemStore)
        Set-PodeSessionInMemClearDown
    }

    # set options against server context
    $PodeContext.Server.Sessions = @{
        Name       = $Name
        Secret     = $Secret
        GenerateId = (Protect-PodeValue -Value $Generator -Default { return (New-PodeGuid) })
        Store      = $Storage
        Info       = @{
            Duration   = $Duration
            Extend     = $Extend.IsPresent
            Secure     = $Secure.IsPresent
            Strict     = $Strict.IsPresent
            HttpOnly   = $HttpOnly.IsPresent
            UseHeaders = $UseHeaders.IsPresent
            Scope      = @{
                Type      = $Scope.ToLowerInvariant()
                IsBrowser = ($Scope -ieq 'Browser')
            }
        }
    }

    # return scriptblock for the session middleware
    Get-PodeSessionMiddleware |
        New-PodeMiddleware |
        Add-PodeMiddleware -Name '__pode_mw_sessions__'
}

<#
.SYNOPSIS
Remove the current Session, logging it out.

.DESCRIPTION
Remove the current Session, logging it out. This will remove the session from Storage, and Cookies.

.EXAMPLE
Remove-PodeSession
#>
function Remove-PodeSession {
    [CmdletBinding()]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions have not been configured'
    }

    # do nothing if session is null
    if ($null -eq $WebEvent.Session) {
        return
    }

    # remove the session, and from auth and cookies
    Remove-PodeAuthSession
}

<#
.SYNOPSIS
Saves the current Session's data.

.DESCRIPTION
Saves the current Session's data.

.PARAMETER Force
If supplied, the data will be saved even if nothing has changed.

.EXAMPLE
Save-PodeSession -Force
#>
function Save-PodeSession {
    [CmdletBinding()]
    param(
        [switch]
        $Force
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions have not been configured'
    }

    # error if session is null
    if ($null -eq $WebEvent.Session) {
        throw 'There is no session available to save'
    }

    # if auth is in use, then assign to session store
    if (!(Test-PodeIsEmpty $WebEvent.Auth) -and $WebEvent.Auth.Store) {
        $WebEvent.Session.Data.Auth = $WebEvent.Auth
    }

    # save the session
    Save-PodeSessionInternal -Force:$Force
}

<#
.SYNOPSIS
Returns the currently authenticated SessionId.

.DESCRIPTION
Returns the currently authenticated SessionId. If there's no session, or it's not authenticated, then null is returned instead.
You can also have the SessionId returned as signed as well.

.PARAMETER Signed
If supplied, the returned SessionId will also be signed.

.PARAMETER Force
If supplied, the sessionId will be returned regardless of authentication.

.EXAMPLE
$sessionId = Get-PodeSessionId
#>
function Get-PodeSessionId {
    [CmdletBinding()]
    param(
        [switch]
        $Signed,

        [switch]
        $Force
    )

    $sessionId = $null

    # do nothing if not authenticated, or force passed
    if (!$Force -and ((Test-PodeIsEmpty $WebEvent.Session.Data.Auth.User) -or !$WebEvent.Session.Data.Auth.IsAuthenticated)) {
        return $sessionId
    }

    # get the sessionId
    $sessionId = $WebEvent.Session.FullId

    # do they want the session signed?
    if ($Signed) {
        $strict = $PodeContext.Server.Sessions.Info.Strict
        $secret = $PodeContext.Server.Sessions.Secret

        # sign the value if we have a secret
        $sessionId = (Invoke-PodeValueSign -Value $sessionId -Secret $secret -Strict:$strict)
    }

    # return the ID
    return $sessionId
}

function Get-PodeSessionTabId {
    [CmdletBinding()]
    param()

    if ($PodeContext.Server.Sessions.Info.Scope.IsBrowser) {
        return $null
    }

    return Get-PodeHeader -Name 'X-PODE-SESSION-TAB-ID'
}

<#
.SYNOPSIS
Resets the current Session's expiry date.

.DESCRIPTION
Resets the current Session's expiry date, to be from the current time plus the defined Session duration.

.EXAMPLE
Reset-PodeSessionExpiry
#>
function Reset-PodeSessionExpiry {
    [CmdletBinding()]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsEnabled)) {
        throw 'Sessions have not been configured'
    }

    # error if session is null
    if ($null -eq $WebEvent.Session) {
        throw 'There is no session available to save'
    }

    # temporarily set this session to auto-extend
    $WebEvent.Session.Extend = $true

    # reset on response
    Set-PodeSession
}

<#
.SYNOPSIS
Returns the defined Session duration.

.DESCRIPTION
Returns the defined Session duration that all Session are created using.

.EXAMPLE
$duration = Get-PodeSessionDuration
#>
function Get-PodeSessionDuration {
    [CmdletBinding()]
    param()

    return [int]$PodeContext.Server.Sessions.Info.Duration
}

<#
.SYNOPSIS
Returns the datetime on which the current Session's will expire.

.DESCRIPTION
Returns the datetime on which the current Session's will expire.

.EXAMPLE
$expiry = Get-PodeSessionExpiry
#>
function Get-PodeSessionExpiry {
    [CmdletBinding()]
    param()

    # error if session is null
    if ($null -eq $WebEvent.Session) {
        throw 'There is no session available to save'
    }

    # default min date
    if ($null -eq $WebEvent.Session.TimeStamp) {
        return [datetime]::MinValue
    }

    # use datetime.now or existing timestamp?
    $expiry = [DateTime]::UtcNow

    if (!$WebEvent.Session.Extend -and ($null -ne $WebEvent.Session.TimeStamp)) {
        $expiry = $WebEvent.Session.TimeStamp
    }

    # add session duration on
    $expiry = $expiry.AddSeconds($PodeContext.Server.Sessions.Info.Duration)

    # return expiry
    return $expiry
}

function Test-PodeSessionsEnabled {
    return (($null -ne $PodeContext.Server.Sessions) -and ($PodeContext.Server.Sessions.Count -gt 0))
}

function Get-PodeSessionInfo {
    return $PodeContext.Server.Sessions.Info
}

function Test-PodeSessionScopeIsBrowser {
    return [bool]$PodeContext.Server.Sessions.Info.Scope.IsBrowser
}
src\Public\SSE.ps1
<#
.SYNOPSIS
Converts the current HTTP request to a Route to be an SSE connection.

.DESCRIPTION
Converts the current HTTP request to a Route to be an SSE connection, by sending the required headers back to the client.
The connection can only be configured if the request's Accept header is "text/event-stream", unless Forced.

.PARAMETER Name
The Name of the SSE connection, which ClientIds will be stored under.

.PARAMETER Group
An optional Group for this SSE connection, to enable broadcasting events to all connections for an SSE connection name in a Group.

.PARAMETER Scope
The Scope of the SSE connection, either Default, Local or Global (Default: Default).
- If the Scope is Default, then it will be Global unless the default has been updated via Set-PodeSseDefaultScope.
- If the Scope is Local, then the SSE connection will only be opened for the duration of the request to a Route that configured it.
- If the Scope is Global, then the SSE connection will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc.

.PARAMETER RetryDuration
An optional RetryDuration, in milliseconds, for the period of time a browser should wait before reattempting a connection if lost (Default: 0).

.PARAMETER ClientId
An optional ClientId to use for the SSE connection, this value will be signed if signing is enabled (Default: GUID).

.PARAMETER AllowAllOrigins
If supplied, then Access-Control-Allow-Origin will be set to * on the response.

.PARAMETER Force
If supplied, the Accept header of the request will be ignored; attempting to configure an SSE connection even if the header isn't "text/event-stream".

.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions'

.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -Scope Local

.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -Group 'admins'

.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -AllowAllOrigins

.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -ClientId 'my-client-id'
#>
function ConvertTo-PodeSseConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Group,

        [Parameter()]
        [ValidateSet('Default', 'Local', 'Global')]
        [string]
        $Scope = 'Default',

        [Parameter()]
        [int]
        $RetryDuration = 0,

        [Parameter()]
        [string]
        $ClientId,

        [switch]
        $AllowAllOrigins,

        [switch]
        $Force
    )

    # check Accept header - unless forcing
    if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) {
        throw 'SSE can only be configured on requests with an Accept header value of text/event-stream'
    }

    # check for default scope, and set
    if ($Scope -ieq 'default') {
        $Scope = $PodeContext.Server.Sse.DefaultScope
    }

    # generate clientId
    $ClientId = New-PodeSseClientId -ClientId $ClientId

    # set and send SSE headers
    $ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent)

    # create SSE property on WebEvent
    $WebEvent.Sse = @{
        Name        = $Name
        Group       = $Group
        ClientId    = $ClientId
        LastEventId = Get-PodeHeader -Name 'Last-Event-ID'
        IsLocal     = ($Scope -ieq 'local')
    }
}

<#
.SYNOPSIS
Sets the default scope for new SSE connections.

.DESCRIPTION
Sets the default scope for new SSE connections.

.PARAMETER Scope
The default Scope for new SSE connections, either Local or Global.
- If the Scope is Local, then new SSE connections will only be opened for the duration of the request to a Route that configured it.
- If the Scope is Global, then new SSE connections will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc.

.EXAMPLE
Set-PodeSseDefaultScope -Scope Local

.EXAMPLE
Set-PodeSseDefaultScope -Scope Global
#>
function Set-PodeSseDefaultScope {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Local', 'Global')]
        [string]
        $Scope
    )

    $PodeContext.Server.Sse.DefaultScope = $Scope
}

<#
.SYNOPSIS
Retrieves the default SSE connection scope for new SSE connections.

.DESCRIPTION
Retrieves the default SSE connection scope for new SSE connections.

.EXAMPLE
$scope = Get-PodeSseDefaultScope
#>
function Get-PodeSseDefaultScope {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.Sse.DefaultScope
}

<#
.SYNOPSIS
Send an Event to one or more SSE connections.

.DESCRIPTION
Send an Event to one or more SSE connections. This can either be:
- Every client for an SSE connection Name
- Specific ClientIds for an SSE connection Name
- The current SSE connection being referenced within $WebEvent.Sse

.PARAMETER Name
An SSE connection Name.

.PARAMETER Group
An optional array of 1 or more SSE connection Groups to send Events to, for the specified SSE connection Name.

.PARAMETER ClientId
An optional array of 1 or more SSE connection ClientIds to send Events to, for the specified SSE connection Name.

.PARAMETER Id
An optional ID for the Event being sent.

.PARAMETER EventType
An optional EventType for the Event being sent.

.PARAMETER Data
The Data for the Event being sent, either as a String or a Hashtable/PSObject. If the latter, it will be converted into JSON.

.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.

.PARAMETER FromEvent
If supplied, the SSE connection Name and ClientId will atttempt to be retrived from $WebEvent.Sse.
These details will be set if ConvertTo-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-NAME are set on an HTTP request.

.EXAMPLE
Send-PodeSseEvent -FromEvent -Data 'This is an event'

.EXAMPLE
Send-PodeSseEvent -FromEvent -Data @{ Message = 'A message' }

.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' }

.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Group 'admins' -Data @{ Message = 'A message' }

.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action'
#>
function Send-PodeSseEvent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Name')]
        [string[]]
        $Group = $null,

        [Parameter(ParameterSetName = 'Name')]
        [string[]]
        $ClientId = $null,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $EventType,

        [Parameter(Mandatory = $true)]
        $Data,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter(ParameterSetName = 'WebEvent')]
        [switch]
        $FromEvent
    )

    # do nothing if no value
    if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) {
        return
    }

    # jsonify the value
    if ($Data -isnot [string]) {
        if ($Depth -le 0) {
            $Data = (ConvertTo-Json -InputObject $Data -Compress)
        }
        else {
            $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress)
        }
    }

    # send directly back to current connection
    if ($FromEvent -and $WebEvent.Sse.IsLocal) {
        $WebEvent.Response.SendSseEvent($EventType, $Data, $Id)
        return
    }

    # from event and global?
    if ($FromEvent) {
        $Name = $WebEvent.Sse.Name
        $Group = $WebEvent.Sse.Group
        $ClientId = $WebEvent.Sse.ClientId
    }

    # error if no name
    if ([string]::IsNullOrEmpty($Name)) {
        throw 'An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name'
    }

    # check if broadcast level
    if (!(Test-PodeSseBroadcastLevel -Name $Name -Group $Group -ClientId $ClientId)) {
        throw "SSE failed to broadcast due to defined SSE broadcast level for $($Name): $(Get-PodeSseBroadcastLevel -Name $Name)"
    }

    # send event
    $PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id)
}

<#
.SYNOPSIS
Close one or more SSE connections.

.DESCRIPTION
Close one or more SSE connections. Either all connections for an SSE connection Name, or specific ClientIds for a Name.

.PARAMETER Name
The Name of the SSE connection which has the ClientIds for the connections to close. If supplied on its own, all connections will be closed.

.PARAMETER Group
An optional array of 1 or more SSE connection Groups, that are for the SSE connection Name. If supplied without any ClientIds, then all connections for the Group(s) will be closed.

.PARAMETER ClientId
An optional array of 1 or more SSE connection ClientIds, that are for the SSE connection Name.
If not supplied, every SSE connection for the supplied Name will be closed.

.EXAMPLE
Close-PodeSseConnection -Name 'Actions'

.EXAMPLE
Close-PodeSseConnection -Name 'Actions' -Group 'admins'

.EXAMPLE
Close-PodeSseConnection -Name 'Actions' -ClientId @('my-client-id', 'my-other'id')
#>
function Close-PodeSseConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Group = $null,

        [Parameter()]
        [string[]]
        $ClientId = $null
    )

    $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $Group, $ClientId)
}

<#
.SYNOPSIS
Test if an SSE connection ClientId is validly signed.

.DESCRIPTION
Test if an SSE connection ClientId is validly signed.

.PARAMETER ClientId
An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent.

.EXAMPLE
if (Test-PodeSseClientIdValid) { ... }

.EXAMPLE
if (Test-PodeSseClientIdValid -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ=') { ... }
#>
function Test-PodeSseClientIdSigned {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $ClientId
    )

    # get clientId from WebEvent if not passed
    if ([string]::IsNullOrEmpty($ClientId)) {
        $ClientId = $WebEvent.Request.SseClientId
    }

    # test if clientId is validly signed
    return Test-PodeValueSigned -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict)
}

<#
.SYNOPSIS
Test if an SSE connection ClientId is valid.

.DESCRIPTION
Test if an SSE connection ClientId, passed or from $WebEvent, is valid. A ClientId is valid if it's not signed and we're not signing ClientIds,
or if we are signing ClientIds and the ClientId is validly signed.

.PARAMETER ClientId
An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent.

.EXAMPLE
if (Test-PodeSseClientIdValid) { ... }

.EXAMPLE
if (Test-PodeSseClientIdValid -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseClientIdValid {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $ClientId
    )

    # get clientId from WebEvent if not passed
    if ([string]::IsNullOrEmpty($ClientId)) {
        $ClientId = $WebEvent.Request.SseClientId
    }

    # if no clientId, then it's not valid
    if ([string]::IsNullOrEmpty($ClientId)) {
        return $false
    }

    # if we're not signing, then valid if not signed, but invalid if signed
    if (!$PodeContext.Server.Sse.Signed) {
        return !$ClientId.StartsWith('s:')
    }

    # test if clientId is validly signed
    return Test-PodeSseClientIdSigned -ClientId $ClientId
}

<#
.SYNOPSIS
Test if the name of an SSE connection exists or not.

.DESCRIPTION
Test if the name of an SSE connection exists or not.

.PARAMETER Name
The Name of an SSE connection to test.

.EXAMPLE
if (Test-PodeSseName -Name 'Example') { ... }
#>
function Test-PodeSseName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name)
}

<#
.SYNOPSIS
Test if an SSE connection ClientId exists or not.

.DESCRIPTION
Test if an SSE connection ClientId exists or not.

.PARAMETER Name
The Name of an SSE connection.

.PARAMETER ClientId
The SSE connection ClientId to test.

.EXAMPLE
if (Test-PodeSseClientId -Name 'Example' -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseClientId {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $ClientId
    )

    return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name, $ClientId)
}

<#
.SYNOPSIS
Generate a new SSE connection ClientId.

.DESCRIPTION
Generate a new SSE connection ClientId, which will be signed if signing enabled.

.PARAMETER ClientId
An optional SSE connection ClientId to use, if a custom ClientId is needed and required to be signed.

.EXAMPLE
$clientId = New-PodeSseClientId

.EXAMPLE
$clientId = New-PodeSseClientId -ClientId 'my-client-id'

.EXAMPLE
$clientId = New-PodeSseClientId -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ='
#>
function New-PodeSseClientId {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $ClientId
    )

    # if no clientId passed, generate a random guid
    if ([string]::IsNullOrEmpty($ClientId)) {
        $ClientId = New-PodeGuid -Secure
    }

    # if we're signing the clientId, and it's not already signed, then sign it
    if ($PodeContext.Server.Sse.Signed -and !$ClientId.StartsWith('s:')) {
        $ClientId = Invoke-PodeValueSign -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict)
    }

    # return the clientId
    return $ClientId
}

<#
.SYNOPSIS
Enable the signing of SSE connection ClientIds.

.DESCRIPTION
Enable the signing of SSE connection ClientIds.

.PARAMETER Secret
A Secret to sign ClientIds, Get-PodeServerDefaultSecret can be used.

.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.

.EXAMPLE
Enable-PodeSseSigning

.EXAMPLE
Enable-PodeSseSigning -Strict

.EXAMPLE
Enable-PodeSseSigning -Secret 'Sup3rS3cr37!' -Strict

.EXAMPLE
Enable-PodeSseSigning -Secret 'Sup3rS3cr37!'
#>
function Enable-PodeSseSigning {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Secret,

        [switch]
        $Strict
    )

    # flag that we're signing SSE connections
    $PodeContext.Server.Sse.Signed = $true
    $PodeContext.Server.Sse.Secret = $Secret
    $PodeContext.Server.Sse.Strict = $Strict.IsPresent
}

<#
.SYNOPSIS
Disable the signing of SSE connection ClientIds.

.DESCRIPTION
Disable the signing of SSE connection ClientIds.

.EXAMPLE
Disable-PodeSseSigning
#>
function Disable-PodeSseSigning {
    [CmdletBinding()]
    param()

    # flag that we're not signing SSE connections
    $PodeContext.Server.Sse.Signed = $false
    $PodeContext.Server.Sse.Secret = $null
    $PodeContext.Server.Sse.Strict = $false
}

<#
.SYNOPSIS
Set an allowed broadcast level for SSE connections.

.DESCRIPTION
Set an allowed broadcast level for SSE connections, either for all SSE connection names or specific ones.

.PARAMETER Name
An optional Name for an SSE connection (default: *).

.PARAMETER Type
The broadcast level Type for the SSE connection.
Name = Allow broadcasting at all levels, including broadcasting to all Groups and/or ClientIds for an SSE connection Name.
Group = Allow broadcasting to only Groups or specific ClientIds. If neither Groups nor ClientIds are supplied, sending an event will fail.
ClientId = Allow broadcasting to only ClientIds. If no ClientIds are supplied, sending an event will fail.

.EXAMPLE
Set-PodeSseBroadcastLevel -Type Name

.EXAMPLE
Set-PodeSseBroadcastLevel -Type Group

.EXAMPLE
Set-PodeSseBroadcastLevel -Name 'Actions' -Type ClientId
#>
function Set-PodeSseBroadcastLevel {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name = '*',

        [Parameter()]
        [ValidateSet('Name', 'Group', 'ClientId')]
        [string]
        $Type
    )

    $PodeContext.Server.Sse.BroadcastLevel[$Name] = $Type.ToLowerInvariant()
}

<#
.SYNOPSIS
Retrieve the broadcast level for an SSE connection Name.

.DESCRIPTION
Retrieve the broadcast level for an SSE connection Name. If one hasn't been set explicitly then the base level will be checked.
If no broadcasting level have been set at all, then the "Name" level will be returned.

.PARAMETER Name
The Name of an SSE connection.

.EXAMPLE
$level = Get-PodeSseBroadcastLevel -Name 'Actions'
#>
function Get-PodeSseBroadcastLevel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # if no levels, return null
    if ($PodeContext.Server.Sse.BroadcastLevel.Count -eq 0) {
        return 'name'
    }

    # get level or default level
    $level = $PodeContext.Server.Sse.BroadcastLevel[$Name]
    if ([string]::IsNullOrEmpty($level)) {
        $level = $PodeContext.Server.Sse.BroadcastLevel['*']
    }

    if ([string]::IsNullOrEmpty($level)) {
        $level = 'name'
    }

    # return level
    return $level
}

<#
.SYNOPSIS
Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds.

.DESCRIPTION
Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds.

.PARAMETER Name
The Name of the SSE connection.

.PARAMETER Group
An array of 1 or more Groups.

.PARAMETER ClientId
An array of 1 or more ClientIds.

.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions') { ... }

.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions' -Group 'admins') { ... }

.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions' -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseBroadcastLevel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Group,

        [Parameter()]
        [string[]]
        $ClientId
    )

    # get level, and if no level or level=name, return true
    $level = Get-PodeSseBroadcastLevel -Name $Name
    if ([string]::IsNullOrEmpty($level) -or ($level -ieq 'name')) {
        return $true
    }

    # if level=group, return false if no groups or clientIds
    # if level=clientId, return false if no clientIds
    switch ($level) {
        'group' {
            if ((($null -eq $Group) -or ($Group.Length -eq 0)) -and (($null -eq $ClientId) -or ($ClientId.Length -eq 0))) {
                return $false
            }
        }

        'clientid' {
            if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) {
                return $false
            }
        }
    }

    # valid, return true
    return $true
}
src\Public\State.ps1
<#
.SYNOPSIS
Sets an object within the shared state.

.DESCRIPTION
Sets an object within the shared state.

.PARAMETER Name
The name of the state object.

.PARAMETER Value
The value to set in the state.

.PARAMETER Scope
An optional Scope for the state object, used when saving the state.

.EXAMPLE
Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }

.EXAMPLE
Set-PodeState -Name 'Users' -Value @('user1', 'user2') -Scope General, Users
#>
function Set-PodeState {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ValueFromPipeline = $true)]
        [object]
        $Value,

        [Parameter()]
        [string[]]
        $Scope
    )

    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    if ($null -eq $Scope) {
        $Scope = @()
    }

    $PodeContext.Server.State[$Name] = @{
        Value = $Value
        Scope = $Scope
    }

    return $Value
}

<#
.SYNOPSIS
Retrieves some state object from the shared state.

.DESCRIPTION
Retrieves some state object from the shared state.

.PARAMETER Name
The name of the state object.

.PARAMETER WithScope
If supplied, the state's value and scope will be returned as a hashtable.

.EXAMPLE
Get-PodeState -Name 'Data'
#>
function Get-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $WithScope
    )

    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    if ($WithScope) {
        return $PodeContext.Server.State[$Name]
    }
    else {
        return $PodeContext.Server.State[$Name].Value
    }
}

<#
.SYNOPSIS
Returns the current names of state variables.

.DESCRIPTION
Returns the current names of state variables that have been set. You can filter the result using Scope or a Pattern.

.PARAMETER Pattern
An optional regex Pattern to filter the state names.

.PARAMETER Scope
An optional Scope to filter the state names.

.EXAMPLE
$names = Get-PodeStateNames -Scope '<scope>'

.EXAMPLE
$names = Get-PodeStateNames -Pattern '^\w+[0-9]{0,2}$'
#>
function Get-PodeStateNames {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [string[]]
        $Scope
    )

    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    if ($null -eq $Scope) {
        $Scope = @()
    }

    $tempState = $PodeContext.Server.State.Clone()
    $keys = $tempState.Keys

    if ($Scope.Length -gt 0) {
        $keys = @(foreach ($key in $keys) {
                if ($tempState[$key].Scope -iin $Scope) {
                    $key
                }
            })
    }

    if (![string]::IsNullOrWhiteSpace($Pattern)) {
        $keys = @(foreach ($key in $keys) {
                if ($key -imatch $Pattern) {
                    $key
                }
            })
    }

    return $keys
}

<#
.SYNOPSIS
Removes some state object from the shared state.

.DESCRIPTION
Removes some state object from the shared state. After removal, the original object being stored is returned.

.PARAMETER Name
The name of the state object.

.EXAMPLE
Remove-PodeState -Name 'Data'
#>
function Remove-PodeState {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    $value = $PodeContext.Server.State[$Name].Value
    $null = $PodeContext.Server.State.Remove($Name)
    return $value
}

<#
.SYNOPSIS
Saves the current shared state to a supplied 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.

.PARAMETER Path
The path to a JSON file which the current state will be saved to.

.PARAMETER Scope
An optional array of scopes for state objects that should be saved. (This has a lower precedence than Exclude/Include)

.PARAMETER Exclude
An optional array of state object names to exclude from being saved. (This has a higher precedence than Include)

.PARAMETER Include
An optional array of state object names to only include when being saved.

.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertTo-JSON's -Depth parameter. Default is 10.

.PARAMETER Compress
If supplied, the saved JSON will be compressed.

.EXAMPLE
Save-PodeState -Path './state.json'

.EXAMPLE
Save-PodeState -Path './state.json' -Exclude Name1, Name2

.EXAMPLE
Save-PodeState -Path './state.json' -Scope Users
#>
function Save-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $Exclude,

        [Parameter()]
        [string[]]
        $Include,

        [Parameter()]
        [int16]
        $Depth = 10,

        [switch]
        $Compress
    )

    # error if attempting to use outside of the pode server
    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    # get the full path to save the state
    $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
            }

            # check scopes (only remove if none match)
            $found = $false

            foreach ($_scope in $state[$_key].Scope) {
                if ($Scope -icontains $_scope) {
                    $found = $true
                    break
                }
            }

            if ($found) {
                continue
            }

            # none matched, remove
            $null = $state.Remove($_key)
        }
    }

    # include keys
    if (($null -ne $Include) -and ($Include.Length -gt 0)) {
        foreach ($_key in $state.Clone().Keys) {
            if ($Include -inotcontains $_key) {
                $null = $state.Remove($_key)
            }
        }
    }

    # exclude keys
    if (($null -ne $Exclude) -and ($Exclude.Length -gt 0)) {
        foreach ($_key in $state.Clone().Keys) {
            if ($Exclude -icontains $_key) {
                $null = $state.Remove($_key)
            }
        }
    }

    # save the state
    $null = ConvertTo-Json -InputObject $state -Depth $Depth -Compress:$Compress | Out-File -FilePath $Path -Force
}

<#
.SYNOPSIS
Restores the shared state from some JSON file.

.DESCRIPTION
Restores the shared state from some JSON file.

.PARAMETER Path
The path to a JSON file that contains the state information.

.PARAMETER Merge
If supplied, the state loaded from the JSON file will be merged with the current state, instead of overwriting it.

.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertFrom-JSON's -Depth parameter (Powershell >=6). Default is 10.

.EXAMPLE
Restore-PodeState -Path './state.json'
#>
function Restore-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [switch]
        $Merge,

        [int16]
        $Depth = 10
    )

    # error if attempting to use outside of the pode server
    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    # get the full path to the state
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot
    if (!(Test-Path $Path)) {
        return
    }

    # restore the state from file
    $state = @{}

    if (Test-PodeIsPSCore) {
        $state = (Get-Content $Path -Force | ConvertFrom-Json -AsHashtable -Depth $Depth)
    }
    else {
        $props = (Get-Content $Path -Force | ConvertFrom-Json).psobject.properties
        foreach ($prop in $props) {
            $state[$prop.Name] = $prop.Value
        }
    }

    # 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
        }
    }

    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]
        }
    }
    else {
        $PodeContext.Server.State = $state.Clone()
    }
}

<#
.SYNOPSIS
Tests if the shared state contains some state object.

.DESCRIPTION
Tests if the shared state contains some state object.

.PARAMETER Name
The name of the state object.

.EXAMPLE
Test-PodeState -Name 'Data'
#>
function Test-PodeState {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        throw 'Pode has not been initialised'
    }

    return $PodeContext.Server.State.ContainsKey($Name)
}
src\Public\Tasks.ps1
<#
.SYNOPSIS
Adds a new Task.

.DESCRIPTION
Adds a new Task, which can be asynchronously or synchronously invoked.

.PARAMETER Name
The Name of the Task.

.PARAMETER ScriptBlock
The script for the Task.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Task's logic.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the Task's ScriptBlock.

.EXAMPLE
Add-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeLogic }

.EXAMPLE
Add-PodeTask -Name 'Example1' -ScriptBlock { return Get-SomeObject }
#>
function Add-PodeTask {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [hashtable]
        $ArgumentList
    )
    # ensure the task doesn't already exist
    if ($PodeContext.Tasks.Items.ContainsKey($Name)) {
        throw "[Task] $($Name): Task already defined"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the task
    $PodeContext.Tasks.Enabled = $true
    $PodeContext.Tasks.Items[$Name] = @{
        Name           = $Name
        Script         = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = (Protect-PodeValue -Value $ArgumentList -Default @{})
    }
}

<#
.SYNOPSIS
Set the maximum number of concurrent Tasks.

.DESCRIPTION
Set the maximum number of concurrent Tasks.

.PARAMETER Maximum
The Maximum number of Tasks to run.

.EXAMPLE
Set-PodeTaskConcurrency -Maximum 10
#>
function Set-PodeTaskConcurrency {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [int]
        $Maximum
    )

    # error if <=0
    if ($Maximum -le 0) {
        throw "Maximum concurrent tasks must be >=1 but got: $($Maximum)"
    }

    # ensure max > min
    $_min = 1
    if ($null -ne $PodeContext.RunspacePools.Tasks) {
        $_min = $PodeContext.RunspacePools.Tasks.Pool.GetMinRunspaces()
    }

    if ($_min -gt $Maximum) {
        throw "Maximum concurrent tasks cannot be less than the minimum of $($_min) but got: $($Maximum)"
    }

    # set the max tasks
    $PodeContext.Threads.Tasks = $Maximum
    if ($null -ne $PodeContext.RunspacePools.Tasks) {
        $PodeContext.RunspacePools.Tasks.Pool.SetMaxRunspaces($Maximum)
    }
}

<#
.SYNOPSIS
Invoke a Task.

.DESCRIPTION
Invoke a Task either asynchronously or synchronously, with support for returning values.

.PARAMETER Name
The Name of the Task.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the Task's ScriptBlock.

.PARAMETER Timeout
A Timeout, in seconds, to abort running the task. (Default: -1 [never timeout])

.PARAMETER Wait
If supplied, Pode will wait until the Task has finished executing, and then return any values.

.EXAMPLE
Invoke-PodeTask -Name 'Example1' -Wait -Timeout 5

.EXAMPLE
$task = Invoke-PodeTask -Name 'Example1'

.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Wait-PodeTask -Timeout 3
#>
function Invoke-PodeTask {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $ArgumentList = $null,

        [Parameter()]
        [int]
        $Timeout = -1,

        [switch]
        $Wait
    )

    # ensure the task exists
    if (!$PodeContext.Tasks.Items.ContainsKey($Name)) {
        throw "Task '$($Name)' does not exist"
    }

    # run task logic
    $task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout

    # wait, and return result?
    if ($Wait) {
        return (Wait-PodeTask -Task $task -Timeout $Timeout)
    }

    # return task
    return $task
}

<#
.SYNOPSIS
Removes a specific Task.

.DESCRIPTION
Removes a specific Task.

.PARAMETER Name
The Name of Task to be removed.

.EXAMPLE
Remove-PodeTask -Name 'Example1'
#>
function Remove-PodeTask {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Tasks.Items.Remove($Name)
}

<#
.SYNOPSIS
Removes all Tasks.

.DESCRIPTION
Removes all Tasks.

.EXAMPLE
Clear-PodeTasks
#>
function Clear-PodeTasks {
    [CmdletBinding()]
    param()

    $PodeContext.Tasks.Items.Clear()
}

<#
.SYNOPSIS
Edits an existing Task.

.DESCRIPTION
Edits an existing Task's properties, such as scriptblock.

.PARAMETER Name
The Name of the Task.

.PARAMETER ScriptBlock
The new ScriptBlock for the Task.

.PARAMETER ArgumentList
Any new Arguments for the Task.

.EXAMPLE
Edit-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeNewLogic }
#>
function Edit-PodeTask {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [hashtable]
        $ArgumentList
    )

    # ensure the task exists
    if (!$PodeContext.Tasks.Items.ContainsKey($Name)) {
        throw "Task '$($Name)' does not exist"
    }

    $_task = $PodeContext.Tasks.Items[$Name]

    # edit scriptblock if supplied
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $_task.Script = $ScriptBlock
        $_task.UsingVariables = $usingVars
    }

    # edit arguments if supplied
    if (!(Test-PodeIsEmpty $ArgumentList)) {
        $_task.Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Returns any defined Tasks.

.DESCRIPTION
Returns any defined Tasks, with support for filtering.

.PARAMETER Name
Any Task Names to filter the Tasks.

.EXAMPLE
Get-PodeTask

.EXAMPLE
Get-PodeTask -Name Example1, Example2
#>
function Get-PodeTask {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    $tasks = $PodeContext.Tasks.Items.Values

    # further filter by task names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $tasks = @(foreach ($_name in $Name) {
                foreach ($task in $tasks) {
                    if ($task.Name -ine $_name) {
                        continue
                    }

                    $task
                }
            })
    }

    # return
    return $tasks
}

<#
.SYNOPSIS
Automatically loads task ps1 files

.DESCRIPTION
Automatically loads task ps1 files from either a /tasks folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeTasks

.EXAMPLE
Use-PodeTasks -Path './my-tasks'
#>
function Use-PodeTasks {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'tasks'
}

<#
.SYNOPSIS
Close and dispose of a Task.

.DESCRIPTION
Close and dispose of a Task, even if still running.

.PARAMETER Task
The Task to be closed.

.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Close-PodeTask
#>
function Close-PodeTask {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Task
    )

    Close-PodeTaskInternal -Result $Task
}

<#
.SYNOPSIS
Test if a running Task has completed.

.DESCRIPTION
Test if a running Task has completed.

.PARAMETER Task
The Task to be check.

.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Test-PodeTaskCompleted
#>
function Test-PodeTaskCompleted {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Task
    )

    return [bool]$Task.Runspace.Handler.IsCompleted
}

<#
.SYNOPSIS
Waits for a task to finish, and returns a result if there is one.

.DESCRIPTION
Waits for a task to finish, and returns a result if there is one.

.PARAMETER Task
The task to wait on.

.PARAMETER Timeout
An optional Timeout in milliseconds.

.EXAMPLE
$context = Wait-PodeTask -Task $listener.GetContextAsync()

.EXAMPLE
$result = Invoke-PodeTask -Name 'Example1' | Wait-PodeTask
#>
function Wait-PodeTask {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $Task,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    if ($Task -is [System.Threading.Tasks.Task]) {
        return (Wait-PodeNetTaskInternal -Task $Task -Timeout $Timeout)
    }

    if ($Task -is [hashtable]) {
        return (Wait-PodeTaskInternal -Task $Task -Timeout $Timeout)
    }

    throw 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]'
}
src\Public\Threading.ps1
<#
.SYNOPSIS
Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked.

.DESCRIPTION
Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked.

.PARAMETER Object
The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default.

.PARAMETER Name
The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite)

.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.

.PARAMETER CheckGlobal
If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object.

.EXAMPLE
Lock-PodeObject -ScriptBlock { /* logic */ }

.EXAMPLE
Lock-PodeObject -Object $SomeArray -ScriptBlock { /* logic */ }

.EXAMPLE
Lock-PodeObject -Name 'LockName' -Timeout 5000 -ScriptBlock { /* logic */ }

.EXAMPLE
$result = (Lock-PodeObject -Return -Object $SomeArray -ScriptBlock { /* logic */ })
#>
function Lock-PodeObject {
    [CmdletBinding(DefaultParameterSetName = 'Object')]
    [OutputType([object])]
    param(
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
        [object]
        $Object,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite,

        [switch]
        $Return,

        [switch]
        $CheckGlobal
    )

    try {
        if ([string]::IsNullOrEmpty($Name)) {
            Enter-PodeLockable -Object $Object -Timeout $Timeout -CheckGlobal:$CheckGlobal
        }
        else {
            Enter-PodeLockable -Name $Name -Timeout $Timeout -CheckGlobal:$CheckGlobal
        }

        if ($null -ne $ScriptBlock) {
            Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        if ([string]::IsNullOrEmpty($Name)) {
            Exit-PodeLockable -Object $Object
        }
        else {
            Exit-PodeLockable -Name $Name
        }
    }
}

<#
.SYNOPSIS
Creates a new custom Lockable object.

.DESCRIPTION
Creates a new custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable.

.PARAMETER Name
The Name of the Lockable object.

.EXAMPLE
New-PodeLockable -Name 'Lock1'
#>
function New-PodeLockable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if (Test-PodeLockable -Name $Name) {
        return
    }

    $PodeContext.Threading.Lockables.Custom[$Name] = [hashtable]::Synchronized(@{})
}

<#
.SYNOPSIS
Removes a custom Lockable object.

.DESCRIPTION
Removes a custom Lockable object.

.PARAMETER Name
The Name of the Lockable object to remove.

.EXAMPLE
Remove-PodeLockable -Name 'Lock1'
#>
function Remove-PodeLockable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if (Test-PodeLockable -Name $Name) {
        $PodeContext.Threading.Lockables.Custom.Remove($Name)
    }
}

<#
.SYNOPSIS
Get a custom Lockable object.

.DESCRIPTION
Get a custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable.

.PARAMETER Name
The Name of the Lockable object.

.EXAMPLE
Get-PodeLockable -Name 'Lock1' | Lock-PodeObject -ScriptBlock {}
#>
function Get-PodeLockable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Lockables.Custom[$Name]
}

<#
.SYNOPSIS
Test if a custom Lockable object exists.

.DESCRIPTION
Test if a custom Lockable object exists.

.PARAMETER Name
The Name of the Lockable object.

.EXAMPLE
Test-PodeLockable -Name 'Lock1'
#>
function Test-PodeLockable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Lockables.Custom.ContainsKey($Name)
}

<#
.SYNOPSIS
Place a lock on an object or Lockable.

.DESCRIPTION
Place a lock on an object or Lockable. This should eventually be followed by a call to Exit-PodeLockable.

.PARAMETER Object
The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default.

.PARAMETER Name
The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite)

.PARAMETER CheckGlobal
If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object.

.EXAMPLE
Enter-PodeLockable -Object $SomeArray

.EXAMPLE
Enter-PodeLockable -Name 'LockName' -Timeout 5000
#>
function Enter-PodeLockable {
    [CmdletBinding(DefaultParameterSetName = 'Object')]
    param(
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
        [object]
        $Object,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite,

        [switch]
        $CheckGlobal
    )

    # get object by name if set
    if (![string]::IsNullOrEmpty($Name)) {
        $Object = Get-PodeLockable -Name $Name
    }

    # if object is null, default to global
    if ($null -eq $Object) {
        $Object = $PodeContext.Threading.Lockables.Global
    }

    # check if value type and throw
    if ($Object -is [valuetype]) {
        throw 'Cannot lock value types'
    }

    # check if null and throw
    if ($null -eq $Object) {
        throw 'Cannot lock a null object'
    }

    # check if the global lockable is locked
    if ($CheckGlobal) {
        Lock-PodeObject -Object $PodeContext.Threading.Lockables.Global -ScriptBlock {} -Timeout $Timeout
    }

    # attempt to acquire lock
    $locked = $false
    [System.Threading.Monitor]::TryEnter($Object.SyncRoot, $Timeout, [ref]$locked)
    if (!$locked) {
        throw 'Failed to acquire lock on object'
    }
}

<#
.SYNOPSIS
Remove a lock from an object or Lockable.

.DESCRIPTION
Remove a lock from an object or Lockable, that was originally locked via Enter-PodeLockable.

.PARAMETER Object
The Object, or Lockable, to unlock. If no Object is supplied then the global lockable is used by default.

.PARAMETER Name
The Name of a Lockable object in Pode to unlock, if no Name is supplied then the global lockable is used by default.

.EXAMPLE
Exit-PodeLockable -Object $SomeArray

.EXAMPLE
Exit-PodeLockable -Name 'LockName'
#>
function Exit-PodeLockable {
    [CmdletBinding(DefaultParameterSetName = 'Object')]
    param(
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
        [object]
        $Object,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name
    )

    # get object by name if set
    if (![string]::IsNullOrEmpty($Name)) {
        $Object = Get-PodeLockable -Name $Name
    }

    # if object is null, default to global
    if ($null -eq $Object) {
        $Object = $PodeContext.Threading.Lockables.Global
    }

    # check if value type and throw
    if ($Object -is [valuetype]) {
        throw 'Cannot unlock value types'
    }

    # check if null and throw
    if ($null -eq $Object) {
        throw 'Cannot unlock a null object'
    }

    if ([System.Threading.Monitor]::IsEntered($Object.SyncRoot)) {
        [System.Threading.Monitor]::Pulse($Object.SyncRoot)
        [System.Threading.Monitor]::Exit($Object.SyncRoot)
    }
}

<#
.SYNOPSIS
Remove all Lockables.

.DESCRIPTION
Remove all Lockables.

.EXAMPLE
Clear-PodeLockables
#>
function Clear-PodeLockables {
    [CmdletBinding()]
    param()

    if (Test-PodeIsEmpty $PodeContext.Threading.Lockables.Custom) {
        return
    }

    foreach ($name in $PodeContext.Threading.Lockables.Custom.Keys.Clone()) {
        Remove-PodeLockable -Name $name
    }
}

<#
.SYNOPSIS
Create a new Mutex.

.DESCRIPTION
Create a new Mutex.

.PARAMETER Name
The Name of the Mutex.

.PARAMETER Scope
The Scope of the Mutex, can be either Self, Local, or Global. (Default: Self)
Self: The current process, or child processes.
Local: All processes for the current login session on Windows, or the the same as Self on Unix.
Global: All processes on the system, across every session.

.EXAMPLE
New-PodeMutex -Name 'SelfMutex'

.EXAMPLE
New-PodeMutex -Name 'LocalMutex' -Scope Local

.EXAMPLE
New-PodeMutex -Name 'GlobalMutex' -Scope Global
#>
function New-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('Self', 'Local', 'Global')]
        [string]
        $Scope = 'Self'
    )

    if (Test-PodeMutex -Name $Name) {
        throw "A mutex with the following name already exists: $($Name)"
    }

    $mutex = $null

    switch ($Scope.ToLowerInvariant()) {
        'self' {
            $mutex = [System.Threading.Mutex]::new($false)
        }

        'local' {
            $mutex = [System.Threading.Mutex]::new($false, "Local\$($Name)")
        }

        'global' {
            $mutex = [System.Threading.Mutex]::new($false, "Global\$($Name)")
        }
    }

    $PodeContext.Threading.Mutexes[$Name] = $mutex
}

<#
.SYNOPSIS
Test if a Mutex exists.

.DESCRIPTION
Test if a Mutex exists.

.PARAMETER Name
The Name of the Mutex.

.EXAMPLE
Test-PodeMutex -Name 'LocalMutex'
#>
function Test-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Mutexes.ContainsKey($Name)
}

<#
.SYNOPSIS
Get a Mutex.

.DESCRIPTION
Get a Mutex.

.PARAMETER Name
The Name of the Mutex.

.EXAMPLE
$mutex = Get-PodeMutex -Name 'SelfMutex'
#>
function Get-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Mutexes[$Name]
}

<#
.SYNOPSIS
Remove a Mutex.

.DESCRIPTION
Remove a Mutex.

.PARAMETER Name
The Name of the Mutex.

.EXAMPLE
Remove-PodeMutex -Name 'GlobalMutex'
#>
function Remove-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if (Test-PodeMutex -Name $Name) {
        $PodeContext.Threading.Mutexes[$Name].Dispose()
        $PodeContext.Threading.Mutexes.Remove($Name)
    }
}

<#
.SYNOPSIS
Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex.

.DESCRIPTION
Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex.

.PARAMETER Name
The Name of the Mutex.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite)

.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.

.EXAMPLE
Use-PodeMutex -Name 'SelfMutex' -Timeout 5000 -ScriptBlock {}

.EXAMPLE
$result = Use-PodeMutex -Name 'LocalMutex' -Return -ScriptBlock {}
#>
function Use-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite,

        [switch]
        $Return
    )

    try {
        $acquired = $false
        Enter-PodeMutex -Name $Name -Timeout $Timeout
        $acquired = $true
        Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        if ($acquired) {
            Exit-PodeMutex -Name $Name
        }
    }
}

<#
.SYNOPSIS
Acquires a hold on a Mutex.

.DESCRIPTION
Acquires a hold on a Mutex. This should eventually by followed by a call to Exit-PodeMutex.

.PARAMETER Name
The Name of the Mutex.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite)

.EXAMPLE
Enter-PodeMutex -Name 'SelfMutex' -Timeout 5000
#>
function Enter-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite
    )

    $mutex = Get-PodeMutex -Name $Name
    if ($null -eq $mutex) {
        throw "No mutex found called '$($Name)'"
    }

    if (!$mutex.WaitOne($Timeout)) {
        throw "Failed to acquire mutex ownership. Mutex name: $($Name)"
    }
}

<#
.SYNOPSIS
Release the hold on a Mutex.

.DESCRIPTION
Release the hold on a Mutex, that was originally acquired by Enter-PodeMutex.

.PARAMETER Name
The Name of the Mutex.

.EXAMPLE
Exit-PodeMutex -Name 'SelfMutex'
#>
function Exit-PodeMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $mutex = Get-PodeMutex -Name $Name
    if ($null -eq $mutex) {
        throw "No mutex found called '$($Name)'"
    }

    $mutex.ReleaseMutex()
}

<#
.SYNOPSIS
Removes all Mutexes.

.DESCRIPTION
Removes all Mutexes.

.EXAMPLE
Clear-PodeMutexes
#>
function Clear-PodeMutexes {
    [CmdletBinding()]
    param()

    if (Test-PodeIsEmpty $PodeContext.Threading.Mutexes) {
        return
    }

    foreach ($name in $PodeContext.Threading.Mutexes.Keys.Clone()) {
        Remove-PodeMutex -Name $name
    }
}

<#
.SYNOPSIS
Create a new Semaphore.

.DESCRIPTION
Create a new Semaphore.

.PARAMETER Name
The Name of the Semaphore.

.PARAMETER Count
The number of threads to allow a hold on the Semaphore. (Default: 1)

.PARAMETER Scope
The Scope of the Semaphore, can be either Self, Local, or Global. (Default: Self)
Self: The current process, or child processes.
Local: All processes for the current login session on Windows, or the the same as Self on Unix.
Global: All processes on the system, across every session.

.EXAMPLE
New-PodeSemaphore -Name 'SelfSemaphore'

.EXAMPLE
New-PodeSemaphore -Name 'LocalSemaphore' -Scope Local

.EXAMPLE
New-PodeSemaphore -Name 'GlobalSemaphore' -Count 3 -Scope Global
#>
function New-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Count = 1,

        [Parameter()]
        [ValidateSet('Self', 'Local', 'Global')]
        [string]
        $Scope = 'Self'
    )

    if (Test-PodeSemaphore -Name $Name) {
        throw "A semaphore with the following name already exists: $($Name)"
    }

    if ($Count -le 0) {
        $Count = 1
    }

    $semaphore = $null

    switch ($Scope.ToLowerInvariant()) {
        'self' {
            $semaphore = [System.Threading.Semaphore]::new($Count, $Count)
        }

        'local' {
            $semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Local\$($Name)")
        }

        'global' {
            $semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Global\$($Name)")
        }
    }

    $PodeContext.Threading.Semaphores[$Name] = $semaphore
}

<#
.SYNOPSIS
Test if a Semaphore exists.

.DESCRIPTION
Test if a Semaphore exists.

.PARAMETER Name
The Name of the Semaphore.

.EXAMPLE
Test-PodeSemaphore -Name 'LocalSemaphore'
#>
function Test-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Semaphores.ContainsKey($Name)
}

<#
.SYNOPSIS
Get a Semaphore.

.DESCRIPTION
Get a Semaphore.

.PARAMETER Name
The Name of the Semaphore.

.EXAMPLE
$semaphore = Get-PodeSemaphore -Name 'SelfSemaphore'
#>
function Get-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return $PodeContext.Threading.Semaphores[$Name]
}

<#
.SYNOPSIS
Remove a Semaphore.

.DESCRIPTION
Remove a Semaphore.

.PARAMETER Name
The Name of the Semaphore.

.EXAMPLE
Remove-PodeSemaphore -Name 'GlobalSemaphore'
#>
function Remove-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if (Test-PodeSemaphore -Name $Name) {
        $PodeContext.Threading.Semaphores[$Name].Dispose()
        $PodeContext.Threading.Semaphores.Remove($Name)
    }
}

<#
.SYNOPSIS
Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore.

.DESCRIPTION
Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore.

.PARAMETER Name
The Name of the Semaphore.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite)

.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.

.EXAMPLE
Use-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000 -ScriptBlock {}

.EXAMPLE
$result = Use-PodeSemaphore -Name 'LocalSemaphore' -Return -ScriptBlock {}
#>
function Use-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite,

        [switch]
        $Return
    )

    try {
        $acquired = $false
        Enter-PodeSemaphore -Name $Name -Timeout $Timeout
        $acquired = $true
        Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        if ($acquired) {
            Exit-PodeSemaphore -Name $Name
        }
    }
}

<#
.SYNOPSIS
Acquires a hold on a Semaphore.

.DESCRIPTION
Acquires a hold on a Semaphore. This should eventually by followed by a call to Exit-PodeSemaphore.

.PARAMETER Name
The Name of the Semaphore.

.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite)

.EXAMPLE
Enter-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000
#>
function Enter-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite
    )

    $semaphore = Get-PodeSemaphore -Name $Name
    if ($null -eq $semaphore) {
        throw "No semaphore found called '$($Name)'"
    }

    if (!$semaphore.WaitOne($Timeout)) {
        throw "Failed to acquire semaphore ownership. Semaphore name: $($Name)"
    }
}

<#
.SYNOPSIS
Release the hold on a Semaphore.

.DESCRIPTION
Release the hold on a Semaphore, that was originally acquired by Enter-PodeSemaphore.

.PARAMETER Name
The Name of the Semaphore.

.PARAMETER ReleaseCount
The number of releases to release in one go. (Default: 1)

.EXAMPLE
Exit-PodeSemaphore -Name 'SelfSemaphore'
#>
function Exit-PodeSemaphore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $ReleaseCount = 1
    )

    $semaphore = Get-PodeSemaphore -Name $Name
    if ($null -eq $semaphore) {
        throw "No semaphore found called '$($Name)'"
    }

    if ($ReleaseCount -lt 1) {
        $ReleaseCount = 1
    }

    $semaphore.Release($ReleaseCount)
}

<#
.SYNOPSIS
Removes all Semaphores.

.DESCRIPTION
Removes all Semaphores.

.EXAMPLE
Clear-PodeSemaphores
#>
function Clear-PodeSemaphores {
    [CmdletBinding()]
    param()

    if (Test-PodeIsEmpty $PodeContext.Threading.Semaphores) {
        return
    }

    foreach ($name in $PodeContext.Threading.Semaphores.Keys.Clone()) {
        Remove-PodeSemaphore -Name $name
    }
}
src\Public\Timers.ps1
<#
.SYNOPSIS
Adds a new Timer with logic to periodically invoke.

.DESCRIPTION
Adds a new Timer with logic to periodically invoke, with options to only run a specific number of times.

.PARAMETER Name
The Name of the Timer.

.PARAMETER Interval
The number of seconds to periodically invoke the Timer's ScriptBlock.

.PARAMETER ScriptBlock
The script for the Timer.

.PARAMETER Limit
The number of times the Timer should be invoked before being removed. (If 0, it will run indefinitely)

.PARAMETER Skip
The number of "invokes" to skip before the Timer actually runs.

.PARAMETER ArgumentList
An array of arguments to supply to the Timer's ScriptBlock.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Timer's logic.

.PARAMETER OnStart
If supplied, the timer will trigger when the server starts.

.EXAMPLE
Add-PodeTimer -Name 'Hello' -Interval 10 -ScriptBlock { 'Hello, world!' | Out-Default }

.EXAMPLE
Add-PodeTimer -Name 'RunOnce' -Interval 1 -Limit 1 -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeTimer -Name 'RunAfter60secs' -Interval 10 -Skip 6 -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeTimer -Name 'Args' -Interval 2 -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeTimer {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [int]
        $Interval,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Limit = 0,

        [Parameter()]
        [int]
        $Skip = 0,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [switch]
        $OnStart
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeTimer' -ThrowError

    # ensure the timer doesn't already exist
    if ($PodeContext.Timers.Items.ContainsKey($Name)) {
        throw "[Timer] $($Name): Timer already defined"
    }

    # is the interval valid?
    if ($Interval -le 0) {
        throw "[Timer] $($Name): Interval must be greater than 0"
    }

    # is the limit valid?
    if ($Limit -lt 0) {
        throw "[Timer] $($Name): Cannot have a negative limit"
    }

    # is the skip valid?
    if ($Skip -lt 0) {
        throw "[Timer] $($Name): Cannot have a negative skip value"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # calculate the next tick time (based on Skip)
    $NextTriggerTime = [DateTime]::Now.AddSeconds($Interval)
    if ($Skip -gt 1) {
        $NextTriggerTime = $NextTriggerTime.AddSeconds($Interval * $Skip)
    }

    # add the timer
    $PodeContext.Timers.Enabled = $true
    $PodeContext.Timers.Items[$Name] = @{
        Name            = $Name
        Interval        = $Interval
        Limit           = $Limit
        Count           = 0
        Skip            = $Skip
        NextTriggerTime = $NextTriggerTime
        LastTriggerTime = $null
        Script          = $ScriptBlock
        UsingVariables  = $usingVars
        Arguments       = $ArgumentList
        OnStart         = $OnStart
        Completed       = $false
    }
}


<#
.SYNOPSIS
Adhoc invoke a Timer's logic.

.DESCRIPTION
Adhoc invoke a Timer's logic outside of its defined interval. This invocation doesn't count towards the Timer's limit.

.PARAMETER Name
The Name of the Timer.

.PARAMETER ArgumentList
An array of arguments to supply to the Timer's ScriptBlock.

.EXAMPLE
Invoke-PodeTimer -Name 'timer-name'
#>
function Invoke-PodeTimer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [object[]]
        $ArgumentList = $null
    )

    # ensure the timer exists
    if (!$PodeContext.Timers.Items.ContainsKey($Name)) {
        throw "Timer '$($Name)' does not exist"
    }

    # run timer logic
    Invoke-PodeInternalTimer -Timer $PodeContext.Timers.Items[$Name] -ArgumentList $ArgumentList
}

<#
.SYNOPSIS
Removes a specific Timer.

.DESCRIPTION
Removes a specific Timer.

.PARAMETER Name
The Name of Timer to be removed.

.EXAMPLE
Remove-PodeTimer -Name 'SaveState'
#>
function Remove-PodeTimer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    $null = $PodeContext.Timers.Items.Remove($Name)
}

<#
.SYNOPSIS
Removes all Timers.

.DESCRIPTION
Removes all Timers.

.EXAMPLE
Clear-PodeTimers
#>
function Clear-PodeTimers {
    [CmdletBinding()]
    param()

    $PodeContext.Timers.Items.Clear()
}

<#
.SYNOPSIS
Edits an existing Timer.

.DESCRIPTION
Edits an existing Timer's properties, such as interval or scriptblock.

.PARAMETER Name
The Name of the Timer.

.PARAMETER Interval
The new Interval for the Timer in seconds.

.PARAMETER ScriptBlock
The new ScriptBlock for the Timer.

.PARAMETER ArgumentList
Any new Arguments for the Timer.

.EXAMPLE
Edit-PodeTimer -Name 'Hello' -Interval 10
#>
function Edit-PodeTimer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Interval = 0,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure the timer exists
    if (!$PodeContext.Timers.Items.ContainsKey($Name)) {
        throw "Timer '$($Name)' does not exist"
    }

    $_timer = $PodeContext.Timers.Items[$Name]

    # edit interval if supplied
    if ($Interval -gt 0) {
        $_timer.Interval = $Interval
    }

    # edit scriptblock if supplied
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $_timer.Script = $ScriptBlock
        $_timer.UsingVariables = $usingVars
    }

    # edit arguments if supplied
    if (!(Test-PodeIsEmpty $ArgumentList)) {
        $_timer.Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Returns any defined timers.

.DESCRIPTION
Returns any defined timers, with support for filtering.

.PARAMETER Name
Any timer Names to filter the timers.

.EXAMPLE
Get-PodeTimer

.EXAMPLE
Get-PodeTimer -Name Name1, Name2
#>
function Get-PodeTimer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    $timers = $PodeContext.Timers.Items.Values

    # further filter by timer names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $timers = @(foreach ($_name in $Name) {
                foreach ($timer in $timers) {
                    if ($timer.Name -ine $_name) {
                        continue
                    }

                    $timer
                }
            })
    }

    # return
    return $timers
}

<#
.SYNOPSIS
Tests whether the passed Timer exists.

.DESCRIPTION
Tests whether the passed Timer exists by its name.

.PARAMETER Name
The Name of the Timer.

.EXAMPLE
if (Test-PodeTimer -Name TimerName) { }
#>
function Test-PodeTimer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (($null -ne $PodeContext.Timers.Items) -and $PodeContext.Timers.Items.ContainsKey($Name))
}

<#
.SYNOPSIS
Automatically loads timer ps1 files

.DESCRIPTION
Automatically loads timer ps1 files from either a /timers folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeTimers

.EXAMPLE
Use-PodeTimers -Path './my-timers'
#>
function Use-PodeTimers {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'timers'
}
src\Public\Utilities.ps1
<#
.SYNOPSIS
Dispose and close streams, tokens, and other Disposables.

.DESCRIPTION
Dispose and close streams, tokens, and other Disposables.

.PARAMETER Disposable
The Disposable object to dispose and close.

.PARAMETER Close
Should the Disposable also be closed, as well as disposed?

.PARAMETER CheckNetwork
If an error is thrown, check the reason - if it's network related ignore the error.

.EXAMPLE
Close-PodeDisposable -Disposable $stream -Close
#>
function Close-PodeDisposable {
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.IDisposable]
        $Disposable,

        [switch]
        $Close,

        [switch]
        $CheckNetwork
    )

    if ($null -eq $Disposable) {
        return
    }

    try {
        if ($Close) {
            $Disposable.Close()
        }
    }
    catch [exception] {
        if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) {
            return
        }

        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $Disposable.Dispose()
    }
}

<#
.SYNOPSIS
Returns the literal path of the server.

.DESCRIPTION
Returns the literal path of the server.

.EXAMPLE
$path = Get-PodeServerPath
#>
function Get-PodeServerPath {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    return $PodeContext.Server.Root
}

<#
.SYNOPSIS
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.

.DESCRIPTION
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.

.PARAMETER Name
The name of the Stopwatch.

.PARAMETER ScriptBlock
The ScriptBlock to time.

.EXAMPLE
Start-PodeStopwatch -Name 'ReadFile' -ScriptBlock { $content = Get-Content './file.txt' }
#>
function Start-PodeStopwatch {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock
    )

    try {
        $watch = [System.Diagnostics.Stopwatch]::StartNew()
        . $ScriptBlock
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $watch.Stop()
        "[Stopwatch]: $($watch.Elapsed) [$($Name)]" | Out-PodeHost
    }
}

<#
.SYNOPSIS
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.

.DESCRIPTION
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.

.PARAMETER Stream
The Stream to use and then dispose.

.PARAMETER ScriptBlock
The ScriptBlock to invoke. It will be supplied the Stream.

.EXAMPLE
$content = (Use-PodeStream -Stream $stream -ScriptBlock { return $args[0].ReadToEnd() })
#>
function Use-PodeStream {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [System.IDisposable]
        $Stream,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )

    try {
        return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $Stream -Return -NoNewClosure)
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $Stream.Dispose()
    }
}

<#
.SYNOPSIS
Loads a script, by dot-sourcing, at the supplied path.

.DESCRIPTION
Loads a script, by dot-sourcing, at the supplied path. If the path is relative, the server's path is prepended.

.PARAMETER Path
The path, literal or relative to the server, to some script.

.EXAMPLE
Use-PodeScript -Path './scripts/tools.ps1'
#>
function Use-PodeScript {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    # if path is '.', replace with server root
    $_path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve

    # we have a path, if it's a directory/wildcard then loop over all files
    if (![string]::IsNullOrWhiteSpace($_path)) {
        $_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps1'
        if (!(Test-PodeIsEmpty $_paths)) {
            foreach ($_path in $_paths) {
                Use-PodeScript -Path $_path
            }

            return
        }
    }

    # check if the path exists
    if (!(Test-PodePath $_path -NoStatus)) {
        throw "The script path does not exist: $(Protect-PodeValue -Value $_path -Default $Path)"
    }

    # dot-source the script
    . $_path

    # load any functions from the file into pode's runspaces
    Import-PodeFunctionsIntoRunspaceState -FilePath $_path
}

<#
.SYNOPSIS
Returns the loaded configuration of the server.

.DESCRIPTION
Returns the loaded configuration of the server.

.EXAMPLE
$s = Get-PodeConfig
#>
function Get-PodeConfig {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    return $PodeContext.Server.Configuration
}

<#
.SYNOPSIS
Adds a ScriptBlock as Endware to run at the end of each web Request.

.DESCRIPTION
Adds a ScriptBlock as Endware to run at the end of each web Request.

.PARAMETER ScriptBlock
The ScriptBlock to add. It will be supplied the current web event.

.PARAMETER ArgumentList
An array of arguments to supply to the Endware's ScriptBlock.

.EXAMPLE
Add-PodeEndware -ScriptBlock { /* logic */ }
#>
function Add-PodeEndware {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the scriptblock to array of endware that needs to be run
    $PodeContext.Server.Endware += @{
        Logic          = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = $ArgumentList
    }
}

<#
.SYNOPSIS
Automatically loads endware ps1 files

.DESCRIPTION
Automatically loads endware ps1 files from either a /endware folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeEndware

.EXAMPLE
Use-PodeEndware -Path './endware'
#>
function Use-PodeEndware {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'endware'
}

<#
.SYNOPSIS
Imports a Module into the current, and all runspaces that Pode uses.

.DESCRIPTION
Imports a Module into the current, and all runspaces that Pode uses. Modules can also be imported from the ps_modules directory.

.PARAMETER Name
The name of a globally installed Module, or one within the ps_modules directory, to import.

.PARAMETER Path
The path, literal or relative, to a Module to import.

.EXAMPLE
Import-PodeModule -Name IISManager

.EXAMPLE
Import-PodeModule -Path './modules/utilities.psm1'
#>
function Import-PodeModule {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path
    )

    # script root path
    $rootPath = $null
    if ($null -eq $PodeContext) {
        $rootPath = (Protect-PodeValue -Value $MyInvocation.PSScriptRoot -Default $pwd.Path)
    }

    # get the path of a module, or import modules on mass
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'name' {
            $modulePath = Join-PodeServerRoot -Folder ([System.IO.Path]::Combine('ps_modules', $Name)) -Root $rootPath
            if (Test-PodePath -Path $modulePath -NoStatus) {
                $Path = (Get-ChildItem ([System.IO.Path]::Combine($modulePath, '*', "$($Name).ps*1")) -Recurse -Force | Select-Object -First 1).FullName
            }
            else {
                $Path = Find-PodeModuleFile -Name $Name -ListAvailable
            }
        }

        'path' {
            $Path = Get-PodeRelativePath -Path $Path -RootPath $rootPath -JoinRoot -Resolve
            $paths = Get-PodeWildcardFiles -Path $Path -RootPath $rootPath -Wildcard '*.ps*1'
            if (!(Test-PodeIsEmpty $paths)) {
                foreach ($_path in $paths) {
                    Import-PodeModule -Path $_path
                }

                return
            }
        }
    }

    # if it's still empty, error
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "Failed to import module: $(Protect-PodeValue -Value $Path -Default $Name)"
    }

    # check if the path exists
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "The module path does not exist: $(Protect-PodeValue -Value $Path -Default $Name)"
    }

    $null = Import-Module $Path -Force -DisableNameChecking -Scope Global -ErrorAction Stop
}

<#
.SYNOPSIS
Imports a Snapin into the current, and all runspaces that Pode uses.

.DESCRIPTION
Imports a Snapin into the current, and all runspaces that Pode uses.

.PARAMETER Name
The name of a Snapin to import.

.EXAMPLE
Import-PodeSnapin -Name 'WDeploySnapin3.0'
#>
function Import-PodeSnapin {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    # if non-windows or core, fail
    if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
        throw 'Snapins are only supported on Windows PowerShell'
    }

    # import the snap-in
    $null = Add-PSSnapin -Name $Name
}

<#
.SYNOPSIS
Protects a value, by returning a default value is the main one is null/empty.

.DESCRIPTION
Protects a value, by returning a default value is the main one is null/empty.

.PARAMETER Value
The main value to use.

.PARAMETER Default
A default value to return should the main value be null/empty.

.EXAMPLE
$Name = Protect-PodeValue -Value $Name -Default 'Rick'
#>
function Protect-PodeValue {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter()]
        $Value,

        [Parameter()]
        $Default
    )

    return (Resolve-PodeValue -Check (Test-PodeIsEmpty $Value) -TrueValue $Default -FalseValue $Value)
}

<#
.SYNOPSIS
Resolves a query, and returns a value based on the response.

.DESCRIPTION
Resolves a query, and returns a value based on the response.

.PARAMETER Check
The query, or variable, to evalulate.

.PARAMETER TrueValue
The value to use if evaluated to True.

.PARAMETER FalseValue
The value to use if evaluated to False.

.EXAMPLE
$Port = Resolve-PodeValue -Check $AllowSsl -TrueValue 443 -FalseValue -80
#>
function Resolve-PodeValue {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [bool]
        $Check,

        [Parameter()]
        $TrueValue,

        [Parameter()]
        $FalseValue
    )

    if ($Check) {
        return $TrueValue
    }

    return $FalseValue
}

<#
.SYNOPSIS
Invokes a ScriptBlock.

.DESCRIPTION
Invokes a ScriptBlock, supplying optional arguments, splatting, and returning any optional values.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Arguments
Any arguments that should be supplied to the ScriptBlock.

.PARAMETER UsingVariables
Optional array of "using-variable" values, which will be automatically prepended to any supplied Arguments when supplied to the ScriptBlock.

.PARAMETER Scoped
Run the ScriptBlock in a scoped context.

.PARAMETER Return
Return any values that the ScriptBlock may return.

.PARAMETER Splat
Spat the argument onto the ScriptBlock.

.PARAMETER NoNewClosure
Don't create a new closure before invoking the ScriptBlock.

.EXAMPLE
Invoke-PodeScriptBlock -ScriptBlock { Write-Host 'Hello!' }

.EXAMPLE
Invoke-PodeScriptBlock -Arguments 'Morty' -ScriptBlock { /* logic */ }
#>
function Invoke-PodeScriptBlock {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $Arguments = $null,

        [Parameter()]
        [object[]]
        $UsingVariables = $null,

        [switch]
        $Scoped,

        [switch]
        $Return,

        [switch]
        $Splat,

        [switch]
        $NoNewClosure
    )

    # force no new closure if running serverless
    if ($PodeContext.Server.IsServerless) {
        $NoNewClosure = $true
    }

    # if new closure needed, create it
    if (!$NoNewClosure) {
        $ScriptBlock = ($ScriptBlock).GetNewClosure()
    }

    # merge arguments together, if we have using vars supplied
    if (($null -ne $UsingVariables) -and ($UsingVariables.Length -gt 0)) {
        $Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables)
    }

    # invoke the scriptblock
    if ($Scoped) {
        if ($Splat) {
            $result = (& $ScriptBlock @Arguments)
        }
        else {
            $result = (& $ScriptBlock $Arguments)
        }
    }
    else {
        if ($Splat) {
            $result = (. $ScriptBlock @Arguments)
        }
        else {
            $result = (. $ScriptBlock $Arguments)
        }
    }

    # if needed, return the result
    if ($Return) {
        return $result
    }
}

<#
.SYNOPSIS
Merges Arguments and Using Variables together.

.DESCRIPTION
Merges Arguments and Using Variables together to be supplied to a ScriptBlock.
The Using Variables will be prepended so then are supplied first to a ScriptBlock.

.PARAMETER ArgumentList
And optional array of Arguments.

.PARAMETER UsingVariables
And optional array of "using-variable" values to be prepended.

.EXAMPLE
$Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables)

.EXAMPLE
$Arguments = @(Merge-PodeScriptblockArguments -UsingVariables $UsingVariables)
#>
function Merge-PodeScriptblockArguments {
    param(
        [Parameter()]
        [object[]]
        $ArgumentList = $null,

        [Parameter()]
        [object[]]
        $UsingVariables = $null
    )

    if ($null -eq $ArgumentList) {
        $ArgumentList = @()
    }

    if (($null -eq $UsingVariables) -or ($UsingVariables.Length -le 0)) {
        return $ArgumentList
    }

    $_vars = @()
    foreach ($_var in $UsingVariables) {
        $_vars += , $_var.Value
    }

    return ($_vars + $ArgumentList)
}

<#
.SYNOPSIS
Tests if a value is empty - the value can be of any type.

.DESCRIPTION
Tests if a value is empty - the value can be of any type.

.PARAMETER Value
The value to test.

.EXAMPLE
if (Test-PodeIsEmpty @{}) { /* logic */ }
#>
function Test-PodeIsEmpty {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter()]
        $Value
    )

    if ($null -eq $Value) {
        return $true
    }

    if ($Value -is [string]) {
        return [string]::IsNullOrWhiteSpace($Value)
    }

    if ($Value -is [array]) {
        return ($Value.Length -eq 0)
    }

    if (($Value -is [hashtable]) -or ($Value -is [System.Collections.Specialized.OrderedDictionary])) {
        return ($Value.Count -eq 0)
    }

    if ($Value -is [scriptblock]) {
        return ([string]::IsNullOrWhiteSpace($Value.ToString()))
    }

    if ($Value -is [valuetype]) {
        return $false
    }

    return ([string]::IsNullOrWhiteSpace($Value) -or ((Get-PodeCount $Value) -eq 0))
}

<#
.SYNOPSIS
Tests if the the current session is running in PowerShell Core.

.DESCRIPTION
Tests if the the current session is running in PowerShell Core.

.EXAMPLE
if (Test-PodeIsPSCore) { /* logic */ }
#>
function Test-PodeIsPSCore {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return (Get-PodePSVersionTable).PSEdition -ieq 'core'
}

<#
.SYNOPSIS
Tests if the current OS is Unix.

.DESCRIPTION
Tests if the current OS is Unix.

.EXAMPLE
if (Test-PodeIsUnix) { /* logic */ }
#>
function Test-PodeIsUnix {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return (Get-PodePSVersionTable).Platform -ieq 'unix'
}

<#
.SYNOPSIS
Tests if the current OS is Windows.

.DESCRIPTION
Tests if the current OS is Windows.

.EXAMPLE
if (Test-PodeIsWindows) { /* logic */ }
#>
function Test-PodeIsWindows {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    $v = Get-PodePSVersionTable
    return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop'))
}

<#
.SYNOPSIS
Tests if the current OS is MacOS.

.DESCRIPTION
Tests if the current OS is MacOS.

.EXAMPLE
if (Test-PodeIsMacOS) { /* logic */ }
#>
function Test-PodeIsMacOS {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return ([bool]$IsMacOS)
}

<#
.SYNOPSIS
Tests if the scope you're in is currently within a Pode runspace.

.DESCRIPTION
Tests if the scope you're in is currently within a Pode runspace.

.EXAMPLE
If (Test-PodeInRunspace) { ... }
#>
function Test-PodeInRunspace {
    [CmdletBinding()]
    param()

    return ([bool]$PODE_SCOPE_RUNSPACE)
}

<#
.SYNOPSIS
Outputs an object to the main Host.

.DESCRIPTION
Due to Pode's use of runspaces, this will output a given object back to the main Host.
It's advised to use this function, so that any output respects the -Quiet flag of the server.

.PARAMETER InputObject
The object to output.

.EXAMPLE
'Hello, world!' | Out-PodeHost

.EXAMPLE
@{ Name = 'Rick' } | Out-PodeHost
#>
function Out-PodeHost {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject
    )

    if (!$PodeContext.Server.Quiet) {
        $InputObject | Out-Default
    }
}

<#
.SYNOPSIS
Writes an object to the Host.

.DESCRIPTION
Writes an object to the Host.
It's advised to use this function, so that any output respects the -Quiet flag of the server.

.PARAMETER Object
The object to write.

.PARAMETER ForegroundColor
An optional foreground colour.

.PARAMETER NoNewLine
Whether or not to write a new line.

.PARAMETER Explode
Show the object content

.PARAMETER ShowType
Show the Object Type

.EXAMPLE
'Some output' | Write-PodeHost -ForegroundColor Cyan
#>
function Write-PodeHost {
    [CmdletBinding(DefaultParameterSetName = 'inbuilt')]
    param(
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [object]
        $Object,

        [Parameter()]
        [System.ConsoleColor]
        $ForegroundColor,

        [switch]
        $NoNewLine,

        [Parameter( Mandatory = $true, ParameterSetName = 'object')]
        [switch]
        $Explode,

        [Parameter( Mandatory = $false, ParameterSetName = 'object')]
        [switch]
        $ShowType
    )

    if ($PodeContext.Server.Quiet) {
        return
    }

    if ($Explode.IsPresent ) {
        if ($null -eq $Object) {
            if ($ShowType) {
                $Object = "`tNull Value"
            }
        }
        else {
            $type = $Object.gettype().FullName
            $Object = $Object | Out-String
            if ($ShowType) {
                $Object = "`tTypeName: $type`n$Object"
            }
        }
    }

    if ($ForegroundColor) {
        Write-Host -Object $Object -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine
    }
    else {
        Write-Host -Object $Object -NoNewline:$NoNewLine
    }
}

<#
.SYNOPSIS
Returns whether or not the server is running via IIS.

.DESCRIPTION
Returns whether or not the server is running via IIS.

.EXAMPLE
if (Test-PodeIsIIS) { }
#>
function Test-PodeIsIIS {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.IsIIS
}

<#
.SYNOPSIS
Returns the IIS application path.

.DESCRIPTION
Returns the IIS application path, or null if not using IIS.

.EXAMPLE
$path = Get-PodeIISApplicationPath
#>
function Get-PodeIISApplicationPath {
    [CmdletBinding()]
    param()

    if (!$PodeContext.Server.IsIIS) {
        return $null
    }

    return $PodeContext.Server.IIS.Path.Raw
}

<#
.SYNOPSIS
Returns whether or not the server is running via Heroku.

.DESCRIPTION
Returns whether or not the server is running via Heroku.

.EXAMPLE
if (Test-PodeIsHeroku) { }
#>
function Test-PodeIsHeroku {
    [CmdletBinding()]
    param()

    return $PodeContext.Server.IsHeroku
}

<#
.SYNOPSIS
Returns whether or not the server is being hosted behind another application.

.DESCRIPTION
Returns whether or not the server is being hosted behind another application, such as Heroku or IIS.

.EXAMPLE
if (Test-PodeIsHosted) { }
#>
function Test-PodeIsHosted {
    [CmdletBinding()]
    param()

    return ((Test-PodeIsIIS) -or (Test-PodeIsHeroku))
}

<#
.SYNOPSIS
Defines variables to be created when the Pode server stops.

.DESCRIPTION
Allows you to define a variable, with a value, that should be created on the in the main scope after the Pode server is stopped.

.PARAMETER Name
The Name of the variable to be set

.PARAMETER Value
The Value of the variable to be set

.EXAMPLE
Out-PodeVariable -Name ExampleVar -Value @{ Name = 'Bob' }
#>
function Out-PodeVariable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ValueFromPipeline = $true)]
        [object]
        $Value
    )

    $PodeContext.Server.Output.Variables[$Name] = $Value
}

<#
.SYNOPSIS
A helper function to generate cron expressions.

.DESCRIPTION
A helper function to generate cron expressions, which can be used for Schedules and other functions that use cron expressions.
This helper function only covers simple cron use-cases, with some advanced use-cases. If you need further advanced cron
expressions it would be best to write the expression by hand.

.PARAMETER Minute
This is an array of Minutes that the expression should use between 0-59.

.PARAMETER Hour
This is an array of Hours that the expression should use between 0-23.

.PARAMETER Date
This is an array of Dates in the monnth that the expression should use between 1-31.

.PARAMETER Month
This is an array of Months that the expression should use between January-December.

.PARAMETER Day
This is an array of Days in the week that the expression should use between Monday-Sunday.

.PARAMETER Every
This can be used to more easily specify "Every Hour" than writing out all the hours.

.PARAMETER Interval
This can only be used when using the Every parameter, and will setup an interval on the "every" used.
If you want "every 2 hours" then Every should be set to Hour and Interval to 2.

.EXAMPLE
New-PodeCron -Every Day                                             # every 00:00

.EXAMPLE
New-PodeCron -Every Day -Day Tuesday, Friday -Hour 1                # every tuesday and friday at 01:00

.EXAMPLE
New-PodeCron -Every Month -Date 15                                  # every 15th of the month at 00:00

.EXAMPLE
New-PodeCron -Every Date -Interval 2 -Date 2                        # every month, every other day from 2nd, at 00:00

.EXAMPLE
New-PodeCron -Every Year -Month June                                # every 1st june, at 00:00

.EXAMPLE
New-PodeCron -Every Hour -Hour 1 -Interval 1                        # every hour, starting at 01:00

.EXAMPLE
New-PodeCron -Every Minute -Hour 1, 2, 3, 4, 5 -Interval 15         # every 15mins, starting at 01:00 until 05:00

.EXAMPLE
New-PodeCron -Every Hour -Day Monday                                # every hour of every monday

.EXAMPLE
New-PodeCron -Every Quarter                                         # every 1st jan, apr, jul, oct, at 00:00
#>
function New-PodeCron {
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter()]
        [ValidateRange(0, 59)]
        [int[]]
        $Minute = $null,

        [Parameter()]
        [ValidateRange(0, 23)]
        [int[]]
        $Hour = $null,

        [Parameter()]
        [ValidateRange(1, 31)]
        [int[]]
        $Date = $null,

        [Parameter()]
        [ValidateSet('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December')]
        [string[]]
        $Month = $null,

        [Parameter()]
        [ValidateSet('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')]
        [string[]]
        $Day = $null,

        [Parameter()]
        [ValidateSet('Minute', 'Hour', 'Day', 'Date', 'Month', 'Quarter', 'Year', 'None')]
        [string]
        $Every = 'None',

        [Parameter()]
        [int]
        $Interval = 0
    )

    # cant have None and Interval
    if (($Every -ieq 'none') -and ($Interval -gt 0)) {
        throw 'Cannot supply an interval when -Every is set to None'
    }

    # base cron
    $cron = @{
        Minute = '*'
        Hour   = '*'
        Date   = '*'
        Month  = '*'
        Day    = '*'
    }

    # convert month/day to numbers
    if ($Month.Length -gt 0) {
        $MonthInts = @(foreach ($item in $Month) {
            (@{
                    January   = 1
                    February  = 2
                    March     = 3
                    April     = 4
                    May       = 5
                    June      = 6
                    July      = 7
                    August    = 8
                    September = 9
                    October   = 10
                    November  = 11
                    December  = 12
                })[$item]
            })
    }

    if ($Day.Length -gt 0) {
        $DayInts = @(foreach ($item in $Day) {
            (@{
                    Sunday    = 0
                    Monday    = 1
                    Tuesday   = 2
                    Wednesday = 3
                    Thursday  = 4
                    Friday    = 5
                    Saturday  = 6
                })[$item]
            })
    }

    # set "every" defaults
    switch ($Every.ToUpperInvariant()) {
        'MINUTE' {
            if (Set-PodeCronInterval -Cron $cron -Type 'Minute' -Value $Minute -Interval $Interval) {
                $Minute = @()
            }
        }

        'HOUR' {
            $cron.Minute = '0'

            if (Set-PodeCronInterval -Cron $cron -Type 'Hour' -Value $Hour -Interval $Interval) {
                $Hour = @()
            }
        }

        'DAY' {
            $cron.Minute = '0'
            $cron.Hour = '0'

            if (Set-PodeCronInterval -Cron $cron -Type 'Day' -Value $DayInts -Interval $Interval) {
                $DayInts = @()
            }
        }

        'DATE' {
            $cron.Minute = '0'
            $cron.Hour = '0'

            if (Set-PodeCronInterval -Cron $cron -Type 'Date' -Value $Date -Interval $Interval) {
                $Date = @()
            }
        }

        'MONTH' {
            $cron.Minute = '0'
            $cron.Hour = '0'

            if ($DayInts.Length -eq 0) {
                $cron.Date = '1'
            }

            if (Set-PodeCronInterval -Cron $cron -Type 'Month' -Value $MonthInts -Interval $Interval) {
                $MonthInts = @()
            }
        }

        'QUARTER' {
            $cron.Minute = '0'
            $cron.Hour = '0'
            $cron.Date = '1'
            $cron.Month = '1,4,7,10'

            if ($Interval -gt 0) {
                throw 'Cannot supply interval value for every quarter'
            }
        }

        'YEAR' {
            $cron.Minute = '0'
            $cron.Hour = '0'
            $cron.Date = '1'
            $cron.Month = '1'

            if ($Interval -gt 0) {
                throw 'Cannot supply interval value for every year'
            }
        }
    }

    # set any custom overrides
    if ($Minute.Length -gt 0) {
        $cron.Minute = $Minute -join ','
    }

    if ($Hour.Length -gt 0) {
        $cron.Hour = $Hour -join ','
    }

    if ($DayInts.Length -gt 0) {
        $cron.Day = $DayInts -join ','
    }

    if ($Date.Length -gt 0) {
        $cron.Date = $Date -join ','
    }

    if ($MonthInts.Length -gt 0) {
        $cron.Month = $MonthInts -join ','
    }

    # build and return
    return "$($cron.Minute) $($cron.Hour) $($cron.Date) $($cron.Month) $($cron.Day)"
}



<#
.SYNOPSIS
Gets 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]'.

.PARAMETER None
This function does not accept any parameters.

.OUTPUTS
System.String
Returns a string indicating the version of the Pode module or '[dev]' if on a development version.

.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '1.2.3' }
PS> Get-PodeVersion

Returns 'v1.2.3'.

.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '$version$' }
PS> Get-PodeVersion

Returns '[dev]'.

.NOTES
This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion.

#>
function Get-PodeVersion {
    $moduleManifest = Get-PodeModuleManifest
    if ($moduleManifest.ModuleVersion -ne '$version$') {
        return "v$($moduleManifest.ModuleVersion)"
    }
    else {
        return '[dev]'
    }
}

<#
.SYNOPSIS
Converts an XML node to a PowerShell hashtable.

.DESCRIPTION
The ConvertFrom-PodeXml function converts an XML node, including all its child nodes and attributes, into an ordered hashtable. This is useful for manipulating XML data in a more PowerShell-centric way.

.PARAMETER node
The XML node to convert. This parameter takes an XML node and processes it, along with its child nodes and attributes.

.PARAMETER Prefix
A string prefix used to indicate an attribute. Default is an empty string.

.PARAMETER ShowDocElement
Indicates whether to show the document element. Default is false.

.PARAMETER KeepAttributes
If set, the function keeps the attributes of the XML nodes in the resulting hashtable.

.EXAMPLE
$node = [xml](Get-Content 'path\to\file.xml').DocumentElement
ConvertFrom-PodeXml -node $node

Converts the XML document's root node to a hashtable.

.INPUTS
System.Xml.XmlNode
You can pipe a XmlNode to ConvertFrom-PodeXml.

.OUTPUTS
System.Collections.Hashtable
Outputs an ordered hashtable representing the XML node structure.

.NOTES
This cmdlet is useful for transforming XML data into a structure that's easier to manipulate in PowerShell scripts.

.LINK
https://badgerati.github.io/Pode/Functions/Utility/ConvertFrom-PodeXml

#>
function ConvertFrom-PodeXml {
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [System.Xml.XmlNode]$node, #we are working through the nodes
        [string]$Prefix = '', #do we indicate an attribute with a prefix?
        $ShowDocElement = $false, #Do we show the document element?,
        [switch]
        $KeepAttributes
    )
    #if option set, we skip the Document element
    if ($node.DocumentElement -and !($ShowDocElement))
    { $node = $node.DocumentElement }
    $oHash = [ordered] @{ } # start with an ordered hashtable.
    #The order of elements is always significant regardless of what they are
    if ($null -ne $node.Attributes  ) {
        #if there are elements
        # record all the attributes first in the ordered hash
        $node.Attributes | ForEach-Object {
            $oHash.$("$Prefix$($_.FirstChild.parentNode.LocalName)") = $_.FirstChild.value
        }
    }
    # check to see if there is a pseudo-array. (more than one
    # child-node with the same name that must be handled as an array)
    $node.ChildNodes | #we just group the names and create an empty
        #array for each
        Group-Object -Property LocalName | Where-Object { $_.count -gt 1 } | Select-Object Name |
        ForEach-Object {
            $oHash.($_.Name) = @() <# create an empty array for each one#>
        }
    foreach ($child in $node.ChildNodes) {
        #now we look at each node in turn.
        $childName = $child.LocalName
        if ($child -is [system.xml.xmltext]) {
            # if it is simple XML text
            $oHash.$childname += $child.InnerText
        }
        # if it has a #text child we may need to cope with attributes
        elseif ($child.FirstChild.Name -eq '#text' -and $child.ChildNodes.Count -eq 1) {
            if ($null -ne $child.Attributes -and $KeepAttributes ) {
                #hah, an attribute
                <#we need to record the text with the #text label and preserve all
					the attributes #>
                $aHash = [ordered]@{ }
                $child.Attributes | ForEach-Object {
                    $aHash.$($_.FirstChild.parentNode.LocalName) = $_.FirstChild.value
                }
                #now we add the text with an explicit name
                $aHash.'#text' += $child.'#text'
                $oHash.$childname += $aHash
            }
            else {
                #phew, just a simple text attribute.
                $oHash.$childname += $child.FirstChild.InnerText
            }
        }
        elseif ($null -ne $child.'#cdata-section' ) {
            # if it is a data section, a block of text that isnt parsed by the parser,
            # but is otherwise recognized as markup
            $oHash.$childname = $child.'#cdata-section'
        }
        elseif ($child.ChildNodes.Count -gt 1 -and
                        ($child | Get-Member -MemberType Property).Count -eq 1) {
            $oHash.$childname = @()
            foreach ($grandchild in $child.ChildNodes) {
                $oHash.$childname += (ConvertFrom-PodeXml $grandchild)
            }
        }
        else {
            # create an array as a value  to the hashtable element
            $oHash.$childname += (ConvertFrom-PodeXml $child)
        }
    }
    return $oHash

}
src\Public\Verbs.ps1
<#
.SYNOPSIS
Adds a Verb for a TCP data.

.DESCRIPTION
Adds a Verb for a TCP data.

.PARAMETER Verb
The Verb for the Verb.

.PARAMETER ScriptBlock
A ScriptBlock for the Verb's main logic.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Verb should be bound against.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Verb's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Verb's ScriptBlock.

.PARAMETER UpgradeToSsl
If supplied, the Verb will auto-upgrade the connection to use SSL.

.PARAMETER Close
If supplied, the Verb will auto-close the connection.

.EXAMPLE
Add-PodeVerb -Verb 'Hello' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeVerb -Verb 'Hello' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'

.EXAMPLE
Add-PodeVerb -Verb 'Quit' -Close

.EXAMPLE
Add-PodeVerb -Verb 'StartTls' -UpgradeToSsl
#>
function Add-PodeVerb {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Verb,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [switch]
        $UpgradeToSsl,

        [switch]
        $Close
    )

    # find placeholder parameters in verb (ie: COMMAND :parameter)
    $Verb = Resolve-PodePlaceholders -Path $Verb

    # get endpoints from name
    $endpoints = Find-PodeEndpoints -EndpointName $EndpointName

    # ensure the verb doesn't already exist for each endpoint
    foreach ($_endpoint in $endpoints) {
        Test-PodeVerbAndError -Verb $Verb -Protocol $_endpoint.Protocol -Address $_endpoint.Address
    }

    # if scriptblock and file path are all null/empty, error
    if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and !$Close -and !$UpgradeToSsl) {
        throw "[Verb] $($Verb): No logic passed"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # add the verb(s)
    Write-Verbose "Adding Verb: $($Verb)"
    $PodeContext.Server.Verbs[$Verb] += @(foreach ($_endpoint in $endpoints) {
            @{
                Logic          = $ScriptBlock
                UsingVariables = $usingVars
                Endpoint       = @{
                    Protocol = $_endpoint.Protocol
                    Address  = $_endpoint.Address.Trim()
                    Name     = $_endpoint.Name
                }
                Arguments      = $ArgumentList
                Verb           = $Verb
                Connection     = @{
                    UpgradeToSsl = $UpgradeToSsl
                    Close        = $Close
                }
            }
        })
}

<#
.SYNOPSIS
Remove a specific Verb.

.DESCRIPTION
Remove a specific Verb.

.PARAMETER Verb
The Verb of the Verb to remove.

.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Verb to be removed.

.EXAMPLE
Remove-PodeVerb -Verb 'Hello'

.EXAMPLE
Remove-PodeVerb -Verb 'Hello :username' -EndpointName User
#>
function Remove-PodeVerb {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Verb,

        [Parameter()]
        [string]
        $EndpointName
    )

    # ensure the verb placeholders are replaced
    $Verb = Resolve-PodePlaceholders -Path $Verb

    # ensure verb does exist
    if (!$PodeContext.Server.Verbs.Contains($Verb)) {
        return
    }

    # remove the verb's logic
    $PodeContext.Server.Verbs[$Verb] = @($PodeContext.Server.Verbs[$Verb] | Where-Object {
            $_.Endpoint.Name -ine $EndpointName
        })

    # if the verb has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Verbs[$Verb]) -eq 0) {
        $null = $PodeContext.Server.Verbs.Remove($Verb)
    }
}

<#
.SYNOPSIS
Removes all added Verbs.

.DESCRIPTION
Removes all added Verbs.

.EXAMPLE
Clear-PodeVerbs
#>
function Clear-PodeVerbs {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Verbs.Clear()
}

<#
.SYNOPSIS
Get a Verb(s).

.DESCRIPTION
Get a Verb(s).

.PARAMETER Verb
A Verb to filter the verbs.

.PARAMETER EndpointName
The name of an endpoint to filter verbs.

.EXAMPLE
Get-PodeVerb -Verb 'Hello'

.EXAMPLE
Get-PodeVerb -Verb 'Hello :username' -EndpointName User
#>
function Get-PodeVerb {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Verb,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    # start off with every verb
    $verbs = @()

    # if we have a verb, filter
    if (![string]::IsNullOrWhiteSpace($Verb)) {
        $Verb = Resolve-PodePlaceholders -Path $Verb
        $verbs = $PodeContext.Server.Verbs[$Verb]
    }
    else {
        foreach ($v in $PodeContext.Server.Verbs.Values) {
            $verbs += $v
        }
    }

    # further filter by endpoint names
    if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
        $verbs = @(foreach ($name in $EndpointName) {
                foreach ($v in $verbs) {
                    if ($v.Endpoint.Name -ine $name) {
                        continue
                    }

                    $v
                }
            })
    }

    # return
    return $verbs
}

<#
.SYNOPSIS
Automatically loads verb ps1 files

.DESCRIPTION
Automatically loads verb ps1 files from either a /verbs folder, or a custom folder. Saves space dot-sourcing them all one-by-one.

.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.

.EXAMPLE
Use-PodeVerbs

.EXAMPLE
Use-PodeVerbs -Path './my-verbs'
#>
function Use-PodeVerbs {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'verbs'
}
src\Public\WebSockets.ps1
using namespace Pode

<#
.SYNOPSIS
Set the maximum number of concurrent WebSocket connection threads.

.DESCRIPTION
Set the maximum number of concurrent WebSocket connection threads.

.PARAMETER Maximum
The Maximum number of threads available to process WebSocket connection messages received.

.EXAMPLE
Set-PodeWebSocketConcurrency -Maximum 5
#>
function Set-PodeWebSocketConcurrency {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [int]
        $Maximum
    )

    # error if <=0
    if ($Maximum -le 0) {
        throw "Maximum concurrent WebSocket threads must be >=1 but got: $($Maximum)"
    }

    # add 1, for the waiting script
    $Maximum++

    # ensure max > min
    $_min = 1
    if ($null -ne $PodeContext.RunspacePools.WebSockets) {
        $_min = $PodeContext.RunspacePools.WebSockets.Pool.GetMinRunspaces()
    }

    if ($_min -gt $Maximum) {
        throw "Maximum concurrent WebSocket threads cannot be less than the minimum of $($_min) but got: $($Maximum)"
    }

    # set the max tasks
    $PodeContext.Threads.WebSockets = $Maximum
    if ($null -ne $PodeContext.RunspacePools.WebSockets) {
        $PodeContext.RunspacePools.WebSockets.Pool.SetMaxRunspaces($Maximum)
    }
}

<#
.SYNOPSIS
Connect to an external WebSocket.

.DESCRIPTION
Connect to an external WebSocket.

.PARAMETER Name
The Name of the WebSocket connection.

.PARAMETER Url
The URL of the WebSocket. Should start with either ws:// or wss://.

.PARAMETER ScriptBlock
The ScriptBlock to invoke for processing received messages from the WebSocket. The ScriptBlock will have access to a $WsEvent variable with details of the received message.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the WebSocket's logic.

.PARAMETER ContentType
An optional ContentType for parsing/converting received/sent messages. (default: application/json)

.PARAMETER ArgumentList
AN optional array of extra arguments, that will be passed to the ScriptBlock.

.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { ... }

.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { param($arg1, $arg2) ... } -ArgumentList 'arg1', 'arg2'

.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -FilePath './some/path/file.ps1'

.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { ... } -ContentType 'text/xml'
#>
function Connect-PodeWebSocket {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter(ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]
        $FilePath,

        [Parameter()]
        [string]
        $ContentType = 'application/json',

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure we have a receiver
    New-PodeWebSocketReceiver

    # fail if already exists
    if (Test-PodeWebSocket -Name $Name) {
        throw "Already connected to websocket with name '$($Name)'"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # connect
    try {
        $PodeContext.Server.WebSockets.Receiver.ConnectWebSocket($Name, $Url, $ContentType)
    }
    catch {
        throw "Failed to connect to websocket: $($_.Exception.Message)"
    }

    $PodeContext.Server.WebSockets.Connections[$Name] = @{
        Name           = $Name
        Url            = $Url
        Logic          = $ScriptBlock
        UsingVariables = $usingVars
        Arguments      = $ArgumentList
    }
}

<#
.SYNOPSIS
Disconnect from a WebSocket connection.

.DESCRIPTION
Disconnect from a WebSocket connection. These connections can be reconnected later using Reset-PodeWebSocket

.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).

.EXAMPLE
Disconnect-PodeWebSocket -Name 'Example'
#>
function Disconnect-PodeWebSocket {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
        $Name = $WsEvent.Request.WebSocket.Name
    }

    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw 'No Name for a WebSocket to disconnect from supplied'
    }

    if (Test-PodeWebSocket -Name $Name) {
        $PodeContext.Server.WebSockets.Receiver.DisconnectWebSocket($Name)
    }
}

<#
.SYNOPSIS
Remove a WebSocket connection.

.DESCRIPTION
Disconnects and then removes a WebSocket connection.

.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).

.EXAMPLE
Remove-PodeWebSocket -Name 'Example'
#>
function Remove-PodeWebSocket {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
        $Name = $WsEvent.Request.WebSocket.Name
    }

    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw 'No Name for a WebSocket to remove supplied'
    }

    $PodeContext.Server.WebSockets.Receiver.RemoveWebSocket($Name)
    $PodeContext.Server.WebSockets.Connections.Remove($Name)
}

<#
.SYNOPSIS
Send a message back to a WebSocket connection.

.DESCRIPTION
Send a message back to a WebSocket connection.

.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).

.PARAMETER Message
The Message to send. Can either be a raw string, hashtable, or psobject. Non-strings will be parsed to JSON, or the WebSocket's ContentType.

.PARAMETER Depth
An optional Depth to parse any JSON or XML messages. (default: 10)

.PARAMETER Type
An optional message Type. (default: Text)

.EXAMPLE
Send-PodeWebSocket -Name 'Example' -Message @{ message = 'Hello, there' }
#>
function Send-PodeWebSocket {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        $Message,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [ValidateSet('Text', 'Binary')]
        [string]
        $Type = 'Text'
    )

    # get ws name
    if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
        $Name = $WsEvent.Request.WebSocket.Name
    }

    # do we have a name?
    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw 'No Name for a WebSocket to send message to supplied'
    }

    # do the socket exist?
    if (!(Test-PodeWebSocket -Name $Name)) {
        return
    }

    # get the websocket
    $ws = $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name)

    # parse message
    $Message = ConvertTo-PodeResponseContent -InputObject $Message -ContentType $ws.ContentType -Depth $Depth

    # send message
    $ws.Send($Message, $Type)
}

<#
.SYNOPSIS
Reset an existing WebSocket connection.

.DESCRIPTION
Reset an existing WebSocket connection, either using it's current URL or a new one.

.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).

.PARAMETER Url
An optional new URL to reset the connection to. If not supplied, the connection's original URL will be used.

.EXAMPLE
Reset-PodeWebSocket -Name 'Example'

.EXAMPLE
Reset-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket'
#>
function Reset-PodeWebSocket {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Url
    )

    if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
        $WsEvent.Request.WebSocket.Reconnect($Url)
        return
    }

    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw 'No Name for a WebSocket to reset supplied'
    }

    if (Test-PodeWebSocket -Name $Name) {
        $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name).Reconnect($Url)
    }
}

<#
.SYNOPSIS
Test whether an WebSocket connection exists.

.DESCRIPTION
Test whether an WebSocket connection exists for the given Name.

.PARAMETER Name
The Name of the WebSocket connection.

.EXAMPLE
Test-PodeWebSocket -Name 'Example'
#>
function Test-PodeWebSocket {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $found = ($null -ne $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name))
    if ($found) {
        return $true
    }

    if ($PodeContext.Server.WebSockets.Connections.ContainsKey($Name)) {
        Remove-PodeWebSocket -Name $Name
    }

    return $false
}
src\VERIFICATION.txt
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.

This embedded PowerShell module is packaged and distributed by the author.

The contents of which can be found on the releases pages at <https://github.com/Badgerati/Pode/releases>.

To verify contents, either:
1. Compare the Checksum on the release notes against the Module's source.
2. Download the zip from the release, run 'checksum -t sha256' on it, and compare.
tools\ChocolateyInstall.ps1
$ErrorActionPreference = 'Stop'


# create the module directory, and copy files over
function Install-PodeModule($path, $version)
{
    # Create module
    $path = Join-Path $path 'Pode'
    if (![string]::IsNullOrWhiteSpace($version)) {
        $path = Join-Path $path $version
    }

    if (!(Test-Path $path))
    {
        Write-Host "Creating module directory: $($path)"
        New-Item -ItemType Directory -Path $path -Force | Out-Null
        if (!$?) {
            throw "Failed to create: $path"
        }
    }

    # Copy contents to module
    Write-Host 'Copying scripts to module path'

    try
    {
        Push-Location (Join-Path $toolsDir 'src')

        # which folders do we need?
        $folders = @('Private', 'Public', 'Misc', 'Libs')

        # create the directories, then copy the source
        $folders | ForEach-Object {
            New-Item -ItemType Directory -Path (Join-Path $path $_) -Force | Out-Null
            Copy-Item -Path "./$($_)/*" -Destination (Join-Path $path $_) -Force -Recurse | Out-Null
        }

        # copy general files
        Copy-Item -Path ./Pode.psm1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./Pode.psd1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./Pode.Internal.psm1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./Pode.Internal.psd1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./LICENSE.txt -Destination $path -Force | Out-Null
    }
    finally {
        Pop-Location
    }
}



# Determine which Program Files path to use
$progFiles = [string]$env:ProgramFiles

# determine the path to choco tools
$toolsDir = Split-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Definition)

# Install PS Module
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'WindowsPowerShell' 'Modules')

# Check to see if Modules path is in PSModulePaths
$psModules = $env:PSModulePath
if (!$psModules.Contains($modulePath))
{
    Write-Host 'Adding module path to PSModulePaths'
    $psModules += ";$modulePath"
    Install-ChocolateyEnvironmentVariable -VariableName 'PSModulePath' -VariableValue $psModules -VariableType Machine
    $env:PSModulePath = $psModules
}

# create the module
if ($PSVersionTable.PSVersion.Major -ge 5) {
    Install-PodeModule $modulePath '2.10.0'
}
else {
    Install-PodeModule $modulePath
}


# Install PS-Core Module
$def = (Get-Command pwsh -ErrorAction SilentlyContinue).Definition

if (![string]::IsNullOrWhiteSpace($def))
{
    # Set the module path
    $modulePath = Join-Path $progFiles (Join-Path 'PowerShell' 'Modules')

    # create the module
    Install-PodeModule $modulePath '2.10.0'
}
tools\ChocolateyUninstall.ps1
function Remove-PodeModule($path)
{
    $path = Join-Path $path 'Pode'
    if (Test-Path $path)
    {
        Write-Host "Deleting module directory: $($path)"
        Remove-Item -Path $path -Recurse -Force | Out-Null
        if (!$?) {
            throw "Failed to delete: $path"
        }
    }
}



# Determine which Program Files path to use
$progFiles = [string]$env:ProgramFiles

# Remove PS Module
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'WindowsPowerShell' 'Modules')

# Delete module
Remove-PodeModule $modulePath


# Remove PS-Core Module
$def = (Get-Command pwsh -ErrorAction SilentlyContinue).Definition

if (![string]::IsNullOrWhiteSpace($def))
{
    # Set the module path
    $modulePath = Join-Path $progFiles (Join-Path 'PowerShell' 'Modules')

    # Delete module
    Remove-PodeModule $modulePath
}

Log in or click on link to see number of positives.

In cases where actual malware is found, the packages are subject to removal. Software sometimes has false positives. Moderators do not necessarily validate the safety of the underlying software, only that a package retrieves software from the official distribution point and/or validate embedded software against official distribution point (where distribution rights allow redistribution).

Chocolatey Pro provides runtime protection from possible malware.

Add to Builder Version Downloads Last Updated Status
Pode 2.9.0 182 Monday, October 30, 2023 Approved
Pode 2.8.0 238 Friday, February 3, 2023 Approved
Pode 2.7.2 131 Tuesday, October 25, 2022 Approved
Pode 2.7.1 113 Thursday, July 21, 2022 Approved
Pode 2.7.0 123 Wednesday, June 22, 2022 Approved
Pode 2.6.2 166 Wednesday, March 2, 2022 Approved
Pode 2.6.1 88 Monday, February 21, 2022 Approved
Pode 2.6.0 89 Thursday, February 10, 2022 Approved
Pode 2.5.2 101 Tuesday, January 4, 2022 Approved
Pode 2.5.1 95 Tuesday, December 21, 2021 Approved
Pode 2.5.0 112 Saturday, November 13, 2021 Approved
Pode 2.4.2 130 Monday, September 13, 2021 Approved
Pode 2.4.1 114 Monday, August 9, 2021 Approved
Pode 2.4.0 95 Wednesday, July 21, 2021 Approved
Pode 2.3.0 120 Tuesday, June 1, 2021 Approved
Pode 2.2.3 137 Saturday, April 10, 2021 Approved
Pode 2.2.2 91 Friday, April 9, 2021 Approved
Pode 2.2.1 95 Saturday, March 27, 2021 Approved
Pode 2.2.0 111 Sunday, March 21, 2021 Approved
Pode 2.1.1 114 Friday, February 19, 2021 Approved
Pode 2.1.0 1164 Wednesday, February 3, 2021 Approved
Pode 2.0.3 148 Monday, December 21, 2020 Approved
Pode 2.0.2 122 Saturday, December 5, 2020 Approved
Pode 2.0.1 106 Sunday, November 29, 2020 Approved
Pode 2.0.0 177 Saturday, November 14, 2020 Approved
Pode 1.8.4 167 Friday, October 16, 2020 Approved
Pode 1.8.3 150 Sunday, September 20, 2020 Approved
Pode 1.8.2 190 Friday, July 31, 2020 Approved
Pode 1.8.1 169 Friday, June 26, 2020 Approved
Pode 1.8.0 182 Sunday, May 24, 2020 Approved
Pode 1.7.3 183 Sunday, May 10, 2020 Approved
Pode 1.7.2 163 Monday, April 27, 2020 Approved
Pode 1.7.1 156 Friday, April 17, 2020 Approved
Pode 1.7.0 171 Friday, April 10, 2020 Approved
Pode 1.6.1 223 Saturday, March 7, 2020 Approved
Pode 1.6.0 184 Tuesday, March 3, 2020 Approved
Pode 1.5.0 220 Sunday, February 2, 2020 Approved
Pode 1.4.0 193 Friday, January 10, 2020 Approved
Pode 1.3.0 180 Friday, December 27, 2019 Approved
Pode 1.2.1 200 Monday, December 2, 2019 Approved
Pode 1.2.0 186 Wednesday, November 13, 2019 Approved
Pode 1.1.0 202 Saturday, September 28, 2019 Approved
Pode 1.0.1 197 Wednesday, September 4, 2019 Approved
Pode 1.0.0 193 Monday, September 2, 2019 Approved
Pode 0.32.0 231 Friday, June 28, 2019 Approved
Pode 0.31.0 196 Tuesday, June 11, 2019 Approved
Pode 0.30.0 194 Sunday, May 26, 2019 Approved
Pode 0.29.0 192 Friday, May 10, 2019 Approved
Pode 0.28.1 228 Tuesday, April 16, 2019 Approved
Pode 0.28.0 182 Saturday, April 13, 2019 Approved
Pode 0.27.3 201 Thursday, April 4, 2019 Approved
Pode 0.27.2 218 Wednesday, March 27, 2019 Approved
Pode 0.27.1 213 Saturday, March 16, 2019 Approved
Pode 0.27.0 210 Thursday, March 14, 2019 Approved
Pode 0.26.0 233 Sunday, February 17, 2019 Approved
Pode 0.25.0 230 Tuesday, February 5, 2019 Approved
Pode 0.24.0 256 Friday, January 18, 2019 Approved
Pode 0.23.0 246 Monday, December 24, 2018 Approved
Pode 0.22.0 236 Friday, December 7, 2018 Approved
Pode 0.21.0 260 Friday, November 2, 2018 Approved
Pode 0.20.0 262 Saturday, October 20, 2018 Approved
Pode 0.19.1 228 Tuesday, October 9, 2018 Approved
Pode 0.19.0 244 Friday, September 14, 2018 Approved
Pode 0.18.0 235 Saturday, August 25, 2018 Approved
Pode 0.17.0 205 Sunday, August 19, 2018 Approved
Pode 0.16.0 252 Wednesday, August 8, 2018 Approved
Pode 0.15.0 275 Friday, July 13, 2018 Approved
Pode 0.14.0 251 Friday, July 6, 2018 Approved
Pode 0.13.0 249 Saturday, June 23, 2018 Approved
Pode 0.12.0 240 Friday, June 15, 2018 Approved
Pode 0.11.3 275 Sunday, June 10, 2018 Approved
Pode 0.11.2 272 Friday, June 8, 2018 Approved
Pode 0.11.1 294 Friday, June 1, 2018 Approved
Pode 0.11.0 262 Wednesday, May 30, 2018 Approved
Pode 0.10.1 312 Wednesday, May 16, 2018 Approved
Pode 0.9.0 336 Thursday, January 11, 2018 Approved

This package has no dependencies.

Discussion for the Pode Package

Ground Rules:

  • This discussion is only about Pode and the Pode package. If you have feedback for Chocolatey, please contact the Google Group.
  • This discussion will carry over multiple versions. If you have a comment about a particular version, please note that in your comments.
  • The maintainers of this Chocolatey Package will be notified about new comments that are posted to this Disqus thread, however, it is NOT a guarantee that you will get a response. If you do not hear back from the maintainers after posting a message below, please follow up by using the link on the left side of this page or follow this link to contact maintainers. If you still hear nothing back, please follow the package triage process.
  • Tell us what you love about the package or Pode, or tell us what needs improvement.
  • Share your experiences with the package, or extra configuration or gotchas that you've found.
  • If you use a url, the comment will be flagged for moderation until you've been whitelisted. Disqus moderated comments are approved on a weekly schedule if not sooner. It could take between 1-5 days for your comment to show up.
comments powered by Disqus