[
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Unit Tests\n\non:\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python:\n          - \"3.10\"\n          - \"3.11\"\n          - \"3.12\"\n          - \"3.13\"\n          - \"3.14-dev\"\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python }}\n\n      - name: Install toolchain\n        run: pip install ruff==0.13.2 pytest\n\n      - name: Install package\n        run: pip install -e .\n\n      - name: Unit tests\n        run: python -m pytest\n\n      - name: Lint\n        run: ruff check secure tests\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n.idea\n\n.DS_Store"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n- Placeholder for upcoming changes.\n\n## [2.0.1] - 2026-04-21\n\nThis is the first stable v2 release. Version `2.0.0` was burned and should be skipped when tagging or publishing.\n\nv2 focuses on a cleaner public API, a redesigned preset model, first-class ASGI/WSGI middleware, and stricter, safer header handling.\n\n### Breaking Changes\n\n- `Secure.headers` is now strict and read-only\n  - v1: `headers` was a cached `dict[str, str]` and silently collapsed duplicate names\n  - v2: duplicate header names (case-insensitive) raise `ValueError`\n  - use `header_items()` for multi-valued output or `deduplicate_headers()` to resolve duplicates\n\n- Default headers have changed\n  - `Secure.with_default_headers()` now maps to `Preset.BALANCED`\n  - v1 default included `Cache-Control: no-store`; v2 does not\n  - applications relying on v1 defaults should explicitly configure required headers\n\n- Presets redesigned\n  - new `Preset.BALANCED` (recommended default)\n  - `Preset.BASIC` updated for Helmet.js parity and no longer matches v1 BASIC\n  - `Preset.STRICT` no longer enables HSTS preload by default\n  - cache behavior and header composition differ across presets compared to v1\n\n- FastAPI / ASGI integration model changed in practice\n  - v1 relied on per-response mutation (`set_headers` / `set_headers_async`)\n  - v2 introduces middleware-based integration as the recommended approach\n\n### Added\n\n- Middleware\n  - `SecureASGIMiddleware`\n  - `SecureWSGIMiddleware`\n  - `secure.middleware` module\n\n- Header pipeline helpers\n  - `allowlist_headers(...)`\n  - `deduplicate_headers(...)`\n  - `validate_and_normalize_headers(...)`\n  - `header_items()` for ordered `(name, value)` output\n\n- New header builders and constants\n  - `CrossOriginResourcePolicy`\n  - `XDnsPrefetchControl`\n  - `XPermittedCrossDomainPolicies`\n  - `MULTI_OK`, `COMMA_JOIN_OK`, `DEFAULT_ALLOWED_HEADERS`\n  - policy enums: `OnInvalidPolicy`, `OnUnexpectedPolicy`, `DeduplicateAction`\n\n### Changed\n\n- `Secure.with_default_headers()` now returns the balanced preset\n- Header handling is stricter and fails fast on invalid or duplicate configurations\n- Header normalization and validation are first-class operations\n- Response integration is more robust across sync and async frameworks\n- `headers_list` mutations are now reflected correctly (no stale cached state)\n- Documentation updated to emphasize middleware usage and preset selection\n\n### Migration Notes\n\n- Do not assume v1 defaults\n  - compare emitted headers and explicitly configure any required behavior\n\n- Audit any usage of `Secure.headers`\n  - treat as read-only in v2\n  - use `header_items()` or `deduplicate_headers()` when duplicates are possible\n\n- Move to middleware for ASGI/WSGI apps\n  - replace per-response `set_headers_async()` calls with `SecureASGIMiddleware` or `SecureWSGIMiddleware`\n\n- Explicitly configure behavior that changed\n  - add `Cache-Control` if you relied on v1 defaults\n  - add HSTS preload manually if required\n\n### Notes\n\n- Neither v1 nor v2 exposes `secure.__version__`; use package metadata for version checks\n\n## [1.0.1] - 2024-10-18\n\n### Fixed\n\n- Improved performance of `Secure.set_headers` by reducing redundant type checks. ([#26](https://github.com/TypeError/secure/issues/26))\n\n## [1.0.0] - 2024-09-27\n\n### Breaking Changes\n\n- Full redesign of the `secure.py` library with modern Python (3.10+) support.\n- Major API overhaul for improved usability and Pythonic design.\n\n### Added\n\n- Enhanced support for FastAPI and asynchronous frameworks.\n- Added type hints and better type annotations for a smoother developer experience.\n- Refined default security headers for improved protection across web frameworks.\n- Support for modern Python features such as the union operator (`|`) and `cached_property`.\n\n## [0.3.0] - 2021-04-27\n\n### Breaking Changes\n\n- Full redesign of Secure API.\n- Removal of cookie support.\n\n### Added\n\n- Added type hints for better developer experience.\n- Added support for FastAPI.\n\n### Changed\n\n- Replaced Feature-Policy with Permissions-Policy (#10).\n\n## [0.2.1] - 2018-12-24\n\n### Added\n\n- Added support for Masonite framework.\n- Added docstrings for `SecureHeaders` and `SecureCookie`.\n\n### Changed\n\n- Upper-cased SameSite enum to `SameSite.LAX` / `SameSite.STRICT`.\n- Modified hug implementation for SecureHeaders and SecureCookie.\n- Renamed `Feature.Values.All` to `Feature.Values.All_` to avoid conflict with the built-in `all`.\n\n### Fixed\n\n- Removed trailing semicolon from Feature Policy.\n\n## [0.2.0] - 2018-12-16\n\n### Added\n\n- Added policy builder `SecurePolicies` in `policies.py`.\n- Added `Expires` header for legacy browser support.\n- Added `max-age` directive to `Cache-Control` header.\n\n### Changed\n\n- Renamed `XXS` argument to `XXP`.\n- Modified `set-cookie` to use Flask's native method.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nThis Code of Conduct applies to all `secure` community spaces.\n\n## Our Pledge\n\nWe’re committed to a welcoming, safe, equitable community for everyone. Treat others with respect and assume good faith.\n\n## Expected Behavior\n\n- Be kind, constructive, and professional.\n- Respect different viewpoints and experiences.\n- Take responsibility for your actions and help repair harm.\n- Give and accept feedback gracefully.\n\n## Unacceptable Behavior\n\n- Harassment, threats, or hate/discrimination.\n- Personal attacks, sexualized behavior, or stereotyping.\n- Sharing someone’s private information without consent.\n- Impersonation, misleading identity, or evasions of enforcement.\n- Spam/promotional content outside community norms.\n\n## Reporting\n\nReport issues to **caleb@typeerror.com**. Maintainers will review reports promptly and handle them as confidentially as possible.\n\n## Enforcement\n\nMaintainers may take action appropriate to the situation, including warnings, temporary limits, suspension, or a permanent ban.\n\n## Scope\n\nApplies in project spaces (issues, PRs, discussions, chats) and when representing the project publicly.\n\n## Attribution\n\nAdapted from the Contributor Covenant v3.0: https://www.contributor-covenant.org/version/3/0/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThanks for helping make `secure` better. The following guidance keeps contributions aligned with the project’s release-quality standards.\n\n## Development environment\n\n1. Create a virtual environment and activate it:\n   ```bash\n   python -m venv .venv\n   source .venv/bin/activate\n   ```\n2. Install the package in editable mode so local changes are picked up automatically:\n   ```bash\n   pip install -e .\n   ```\n3. Install the tooling used by the project:\n   ```bash\n   pip install pytest ruff\n   ```\n   _Optional:_ `uv` is the package manager used by the project for releases; you can use `uv run pytest` and `uv add ...` to manage dependencies, but it is not required for local development.\n\n## Running tests, linting, and formatting\n\n- **Run unit tests:** `pytest`\n- **Run the linter:** `ruff check`\n- **Apply formatting / fix issues:** `ruff format`\n\nRun these commands before opening a pull request. If you rely on a different Python version, keep it within the supported range (Python 3.10+).\n\n## Adding a header document\n\n1. Add a new guide under `docs/headers/` named after the header (for example, `docs/headers/example_header.md`).\n2. Mirror the structure of the existing header docs:\n   - Start with a **Purpose** section that explains the header’s intent.\n   - Describe the **Default behavior** and mention how the builder models that default.\n   - Show a **Using with `Secure`** example and describe the builder API with method names.\n   - Include **Resources** / **Attribution** and any security caveats.\n3. Link the new document from `docs/README.md` (under the Security Headers list) so readers can discover it easily.\n4. Ensure code snippets use the public API (`from secure import ...`), reference the appropriate response types, and avoid framework-specific terminology unless a callout is necessary.\n\n## Adding a framework example\n\n1. Update `docs/frameworks.md`:\n   - Add the framework to the table of contents.\n   - Include a short intro describing the framework’s model (WSGI vs ASGI, sync vs async).\n   - Provide at least one working example showing how to wire `Secure` (middleware, hooks, or response-level helpers).\n   - Mention the correct response type (`Response`, `JSONResponse`, etc.) or highlight that you are working with the framework’s default response object.\n2. If the framework needs extra instructions (e.g., disabling Uvicorn’s `Server` header), document them in the same section.\n3. Keep the tone focused on security headers rather than broader framework guidance.\n\n## Commit conventions\n\n- Keep commit messages short (<72 characters) and in the imperative (e.g., `docs: clarify defaults`).\n- Prefix doc-only changes with `docs:` so reviewers immediately know the scope.\n- Reference any related issue or PR in the description when applicable.\n- Run linting/tests before committing to minimize follow-up work.\n\n## Pull request checklist\n\n- [ ] I have run `pytest` locally (or a representative suite) and addressed any failures.\n- [ ] I have run `ruff check` and `ruff format` (when formatting attr).\n- [ ] Documentation updates describe the new behavior (new header docs, framework guidance, etc.).\n- [ ] If applicable, I have updated the release notes/CHANGELOG entry for new user-visible behavior.\n- [ ] My changes follow the project’s security and contribution guidelines (this document).\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018-2024 Caleb Kinney\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# secure\n\n[![PyPI Version](https://img.shields.io/pypi/v/secure.svg)](https://pypi.org/project/secure/) [![Python Versions](https://img.shields.io/pypi/pyversions/secure.svg)](https://pypi.org/project/secure/) [![License](https://img.shields.io/pypi/l/secure.svg)](https://github.com/TypeError/secure/blob/main/LICENSE)\n\nDefine HTTP security headers once. Apply them consistently across Python web apps.\n\n`secure` provides a small, dependency-free API for configuring modern security headers across common Python web frameworks through ASGI middleware, WSGI middleware, or framework response hooks.\n\nUse it when you want to avoid copy-pasted header strings spread across handlers, hooks, and middleware.\n\n<p align=\"center\">\n  <img src=\"https://typeerror.com/assets/secure-hex.png\" alt=\"secure hex\" width=\"200\">\n</p>\n\nQuick links: [Quick start](#quick-start) · [Headers](#what-headers-are-applied-by-default) · [Middleware](#middleware)\n\n---\n\n## Why use `secure`\n\nSetting headers manually is fine for one endpoint. It gets harder to review when values are copied across routes, response hooks, reverse-proxy settings, and different framework integrations.\n\n`secure` helps you:\n\n- Keep one `Secure` policy object instead of scattered header strings\n- Apply the same policy through ASGI middleware, WSGI middleware, or response hooks\n- Start with practical presets, then customize headers for your application\n- Use builders for complex headers such as Content Security Policy and Permissions Policy\n- Make duplicate handling, header overwrites, and validation explicit\n\nThe defaults are a reasonable starting point, not a substitute for application-specific review. Content Security Policy in particular should be adjusted for the scripts, styles, assets, and third-party services your app actually uses.\n\n---\n\n## Installation\n\n```bash\nuv add secure\n# or\npip install secure\n```\n\n---\n\n## Quick start\n\n### FastAPI / ASGI middleware\n\nUse middleware when you can attach `secure` once and cover the whole application.\n\n```python\nfrom fastapi import FastAPI\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\n\napp = FastAPI()\nsecure_headers = Secure.with_default_headers()\n\napp.add_middleware(SecureASGIMiddleware, secure=secure_headers)\n```\n\n### Response hooks and handlers\n\nUse the same policy object in framework hooks, middleware callbacks, or handlers that expose a response object with headers support.\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\nsecure_headers.set_headers(response)\n# or\nawait secure_headers.set_headers_async(response)\n```\n\n`Secure.with_default_headers()` maps to `Preset.BALANCED`, a practical default designed to be customized.\n\n---\n\n## What headers are applied by default\n\n```http\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Resource-Policy: same-origin\nContent-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests\nStrict-Transport-Security: max-age=31536000; includeSubDomains\nPermissions-Policy: geolocation=(), microphone=(), camera=()\nReferrer-Policy: strict-origin-when-cross-origin\nX-Content-Type-Options: nosniff\nX-Frame-Options: SAMEORIGIN\n```\n\nThis preset reflects modern browser guidance from MDN and OWASP. It reduces cross-origin risk, prevents MIME sniffing, and provides a conservative Content Security Policy you can extend.\n\nThe default CSP avoids unsafe script execution by default, but CSP is context-sensitive. Interactive apps, dashboards, CDNs, analytics, embedded content, or other third-party integrations may require additional configuration.\n\n---\n\n## Presets\n\n```python\nfrom secure import Preset, Secure\n\nSecure.from_preset(Preset.BALANCED)\nSecure.from_preset(Preset.BASIC)\nSecure.from_preset(Preset.STRICT)\n```\n\n- **BALANCED**: practical default for many applications\n- **BASIC**: Helmet-style compatibility\n- **STRICT**: tighter CSP and isolation\n\nStart with BALANCED, review the generated headers, and move stricter only when needed.\n\n---\n\n## Middleware\n\n`secure` provides both ASGI and WSGI middleware.\n\n- Overwrites headers by default\n- Allows controlled duplication via `multi_ok`\n- Only modifies HTTP responses in ASGI apps\n\n### ASGI\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\n\nsecure_headers = Secure.with_default_headers()\napp = SecureASGIMiddleware(app, secure=secure_headers)\n```\n\n### WSGI\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureWSGIMiddleware\n\nsecure_headers = Secure.with_default_headers()\napp = SecureWSGIMiddleware(app, secure=secure_headers)\n```\n\n### Django example\n\n```python\nfrom secure import Secure\n\nclass SecureHeadersMiddleware:\n    def __init__(self, get_response):\n        self.get_response = get_response\n        self.secure = Secure.with_default_headers()\n\n    def __call__(self, request):\n        response = self.get_response(request)\n        self.secure.set_headers(response)\n        return response\n```\n\n---\n\n## Framework integration examples\n\nThese examples show common integration paths. See the full [framework integration guide](https://github.com/TypeError/secure/tree/main/docs/frameworks.md) for additional frameworks and notes about first-class middleware versus minimal fallback examples.\n\n### FastAPI\n\n```python\nfrom secure.middleware import SecureASGIMiddleware\n\napp.add_middleware(SecureASGIMiddleware, secure=secure_headers)\n```\n\n### Flask\n\n```python\n@app.after_request\ndef add_security_headers(response):\n    secure_headers.set_headers(response)\n    return response\n```\n\n### Shiny for Python\n\n```python\nfrom shiny import App\nfrom secure.middleware import SecureASGIMiddleware\n\napp = SecureASGIMiddleware(App(), secure=secure_headers)\n```\n\nInteractive apps often need CSP configuration for their script, style, and asset loading patterns.\n\n---\n\n## Policy builders\n\n### Content Security Policy\n\n```python\nfrom secure.headers import ContentSecurityPolicy\n\ncsp = (\n    ContentSecurityPolicy()\n    .default_src(\"'self'\")\n    .script_src(\"'self'\", \"cdn.example.com\")\n)\n```\n\n### Permissions Policy\n\n```python\nfrom secure.headers import PermissionsPolicy\n\npermissions = (\n    PermissionsPolicy()\n    .geolocation(\"'self'\")\n    .camera(\"'none'\")\n)\n```\n\n---\n\n## Header pipeline and validation\n\nUse the optional pipeline when you need stricter control:\n\n```python\nsecure_headers = (\n    Secure.with_default_headers()\n    .allowlist_headers(...)\n    .deduplicate_headers(...)\n    .validate_and_normalize_headers(...)\n)\n```\n\nThis makes header allowlists, deduplication, normalization, and validation explicit instead of leaving those choices spread across application code.\n\n---\n\n## Requirements\n\n- Python 3.10+\n- No external dependencies\n\n---\n\n## Documentation\n\nRead the full [documentation](https://github.com/TypeError/secure/tree/main/docs).\n\n---\n\n## Learn more\n\n- [MDN HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/)\n\n---\n\n## License\n\nMIT License\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome.\n\n---\n\n## Acknowledgements\n\nBuilt using guidance from MDN and OWASP secure headers recommendations.\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Documentation\n\nUse this index when you know what you need. For a first pass, start with the [top-level README](../README.md), then continue with [Usage](./usage.md).\n\nThe recommended default throughout the docs is `Secure.with_default_headers()`, which maps to `Preset.BALANCED`.\n\n## Start here\n\n- [Installation](./installation.md)\n- [Usage](./usage.md)\n- [Framework integration](./frameworks.md)\n- [Migration notes](./migration.md)\n\n## Reference\n\n- [Configuration](./configuration.md)\n- [Security considerations](./security_considerations.md)\n\n## Header builders\n\n- [Cache-Control](./headers/cache_control.md)\n- [Content-Security-Policy](./headers/content_security_policy.md)\n- [Cross-Origin-Embedder-Policy](./headers/cross_origin_embedder_policy.md)\n- [Cross-Origin-Opener-Policy](./headers/cross_origin_opener_policy.md)\n- [Cross-Origin-Resource-Policy](./headers/cross-origin-resource-policy.md)\n- [Custom Header](./headers/custom_header.md)\n- [Permissions-Policy](./headers/permissions_policy.md)\n- [Referrer-Policy](./headers/referrer_policy.md)\n- [Server](./headers/server.md)\n- [Strict-Transport-Security](./headers/strict_transport_security.md)\n- [X-Content-Type-Options](./headers/x_content_type_options.md)\n- [X-DNS-Prefetch-Control](./headers/dns_prefetch_control.md)\n- [X-Frame-Options](./headers/x_frame_options.md)\n- [X-Permitted-Cross-Domain-Policies](./headers/x-permitted-cross-domain-policies.md)\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# Configuration Guide\n\nThis guide covers the parts of `secure` you are most likely to customize after the quick start. Keep `Secure` as the public entry point and pass it the builders you need.\n\n## Default configuration\n\n`Secure.with_default_headers()` uses `Preset.BALANCED`, which provides a modern baseline while keeping the header set lean:\n\n- **Cross-Origin-Opener-Policy:** `same-origin`\n- **Cross-Origin-Resource-Policy:** `same-origin`\n- **Content-Security-Policy:** `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests`\n- **Strict-Transport-Security:** `max-age=31536000; includeSubDomains`\n- **Permissions-Policy:** `geolocation=(), microphone=(), camera=()`\n- **Referrer-Policy:** `strict-origin-when-cross-origin`\n- **Server:** empty string\n- **X-Content-Type-Options:** `nosniff`\n- **X-Frame-Options:** `SAMEORIGIN`\n\nBalanced intentionally skips `Cache-Control` and the compatibility headers (`X-Permitted-Cross-Domain-Policies`, `X-DNS-Prefetch-Control`, `Origin-Agent-Cluster`, `X-Download-Options`, `X-XSS-Protection`). Add them explicitly when your deployment still depends on them.\n\n`Preset.BALANCED` is the recommended default. `Preset.BASIC` is the compatibility-oriented option that most closely matches Helmet-style defaults. `Preset.STRICT` is available when you want tighter CSP and stronger isolation. It is not the default.\n\nThe balanced CSP includes `'unsafe-inline'` in `style-src` for compatibility. It does not allow inline scripts by default. If your app needs a looser CSP, treat that as an app-specific adjustment and test it against real behavior.\n\n## Customizing individual builders\n\nAll public header builders are re-exported from `secure`, so most applications can stay on the package-level API.\n\n### `X-Frame-Options`\n\nIf you want to allow framing only from the same origin, use:\n\n```python\nfrom secure import Secure, XFrameOptions\n\nsecure_headers = Secure(xfo=XFrameOptions().sameorigin())\n```\n\nThis protects against clickjacking while still allowing same-origin embedding.\n\n### `Strict-Transport-Security`\n\nTo enforce HTTPS for all subdomains and opt into preload when you are ready, configure HSTS explicitly:\n\n```python\nfrom secure import Secure, StrictTransportSecurity\n\nsecure_headers = Secure(\n    hsts=StrictTransportSecurity().max_age(63072000).include_subdomains().preload()\n)\n```\n\nThis enforces HTTPS for two years, applies the rule to subdomains, and opts into the preload list.\n\n## Adding custom headers\n\nUse `CustomHeader` for application-specific response headers that do not have a dedicated builder:\n\n```python\nfrom secure import CustomHeader, Secure\n\ncustom_header = CustomHeader(\"X-Custom-Header\", \"CustomValue\")\nsecure_headers = Secure(custom=[custom_header])\n```\n\n## Starting from a preset\n\nEvery `Secure` instance exposes its configured builders through `headers_list`, so you can replace or extend a preset after construction:\n\n```python\nfrom secure import Preset, Secure, StrictTransportSecurity\n\nsecure_headers = Secure.from_preset(Preset.BALANCED)\n\nsecure_headers.headers_list = [\n    header\n    for header in secure_headers.headers_list\n    if header.header_name != \"Strict-Transport-Security\"\n]\nsecure_headers.headers_list.append(\n    StrictTransportSecurity().max_age(63072000).include_subdomains()\n)\n```\n\nThis replaces the preset HSTS builder while leaving the rest of the preset untouched.\nIf you need the compatibility profile instead, start from `Preset.BASIC`.\n\n## Middleware behavior\n\nThe ASGI and WSGI middleware classes use the same `Secure` instance you would use in hooks or handlers:\n\n- Existing header values are overwritten by default for configured header names.\n- Pass `multi_ok` when a header name should be preserved and appended instead.\n- `SecureASGIMiddleware` only changes HTTP responses. WebSocket scopes pass through unchanged.\n\n## Validation and normalization\n\nIf you need stronger guarantees before emission, `Secure` also exposes optional pipeline helpers:\n\n- `allowlist_headers(...)` filters or rejects unexpected header names in the current `headers_list`.\n- `deduplicate_headers(...)` resolves duplicate header names in `headers_list` before you build a single-valued mapping.\n- `validate_and_normalize_headers(...)` validates and normalizes the current `header_items()`, then caches the single-valued mapping used by `.headers`, `set_headers()`, and `set_headers_async()`.\n\nIf you intentionally emit duplicate headers such as multiple `Content-Security-Policy` values, use `header_items()` instead of `.headers`.\n\nFor per-header builder details, see the docs under [headers](./headers).\n"
  },
  {
    "path": "docs/frameworks.md",
    "content": "# Framework Integration\n\n`secure` keeps the same `Secure` object across frameworks. What changes is how you attach it.\n\nSome sections below are first-class integrations with clear framework-level hooks or middleware. Others are intentionally minimal fallback examples where support is thinner or framework APIs vary by version.\n\n## How to choose an integration style\n\n- Use `set_headers()` when the response object is synchronous and you are already inside a response hook, middleware callback, or view.\n- Use `set_headers_async()` in async middleware, hooks, or handlers when you want one helper that works safely across async response objects.\n- Use `SecureWSGIMiddleware` when you want app-wide coverage and can wrap a WSGI application directly.\n- Use `SecureASGIMiddleware` when you want app-wide coverage in an ASGI stack such as FastAPI, Starlette, or Shiny.\n\nPrefer middleware when your framework makes it easy and you want app-wide coverage. Use per-response setters when you are integrating into an existing hook, view, or minimal handler path.\n\n## Middleware behavior\n\n`SecureASGIMiddleware` and `SecureWSGIMiddleware` share the same core behavior:\n\n- Configured header names are overwritten by default so stale values do not accumulate.\n- Pass `multi_ok` when a header should be preserved and `secure` should append its own value instead.\n- The default `secure.MULTI_OK` setting allows controlled duplication for headers such as `Content-Security-Policy`.\n- `SecureASGIMiddleware` only modifies HTTP responses. WebSocket and other non-HTTP scopes pass through unchanged.\n\nExample:\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\n\nsecure_headers = Secure.with_default_headers()\nsecured_app = SecureASGIMiddleware(app, secure=secure_headers, multi_ok={\"content-security-policy\"})\n```\n\n## Uvicorn `Server` header\n\nUvicorn adds `Server: uvicorn` by default. If you want `secure` to control the `Server` header, disable Uvicorn's default header with `--no-server-header` or `server_header=False`.\n\n```python\nimport uvicorn\n\nuvicorn.run(app, host=\"0.0.0.0\", port=8000, server_header=False)\n```\n\n## Table of contents\n\n- [aiohttp](#aiohttp)\n- [Bottle](#bottle)\n- [CherryPy](#cherrypy)\n- [Dash](#dash)\n- [Django](#django)\n- [Falcon](#falcon)\n- [FastAPI](#fastapi)\n- [Flask](#flask)\n- [Masonite](#masonite)\n- [Morepath](#morepath)\n- [Pyramid](#pyramid)\n- [Quart](#quart)\n- [Responder](#responder)\n- [Sanic](#sanic)\n- [Shiny](#shiny)\n- [Starlette](#starlette)\n- [Tornado](#tornado)\n- [TurboGears](#turbogears)\n- [Custom frameworks](#custom-frameworks)\n\n## aiohttp\n\nAsync framework with first-class middleware support.\n\n### Recommended: middleware with `set_headers_async()`\n\n```python\nfrom aiohttp import web\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\n@web.middleware\nasync def add_security_headers(request, handler):\n    response = await handler(request)\n    await secure_headers.set_headers_async(response)\n    return response\n\n\napp = web.Application(middlewares=[add_security_headers])\n```\n\n### Alternative: set headers in a single handler\n\n```python\nfrom aiohttp import web\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nasync def home(request):\n    response = web.Response(text=\"Hello, world\")\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\n## Bottle\n\nSmall WSGI framework with request hooks.\n\n### Recommended: `after_request` hook with `set_headers()`\n\n```python\nfrom bottle import Bottle, response\nfrom secure import Secure\n\napp = Bottle()\nsecure_headers = Secure.with_default_headers()\n\n\n@app.hook(\"after_request\")\ndef add_security_headers():\n    secure_headers.set_headers(response)\n```\n\n### Fallback: set headers in a route\n\n```python\nfrom bottle import Bottle, response\nfrom secure import Secure\n\napp = Bottle()\nsecure_headers = Secure.with_default_headers()\n\n\n@app.route(\"/\")\ndef home():\n    secure_headers.set_headers(response)\n    return \"Hello, world\"\n```\n\n## CherryPy\n\nMinimal fallback example. CherryPy exposes the response object in the handler, so handler-level mutation is the practical integration point.\n\n### Minimal fallback: set headers in the exposed method\n\n```python\nimport cherrypy\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass App:\n    @cherrypy.expose\n    def index(self):\n        secure_headers.set_headers(cherrypy.response)\n        return b\"Hello, world\"\n\n\ncherrypy.quickstart(App())\n```\n\n## Dash\n\nDash runs on top of Flask, so the usual Flask integration patterns apply.\n\n### Recommended: Flask `after_request` on `app.server`\n\n```python\nimport dash\nfrom dash import html\nfrom secure import Secure\n\napp = dash.Dash(__name__)\nserver = app.server\nsecure_headers = Secure.with_default_headers()\n\napp.layout = html.Div(\"Hello Dash!\")\n\n\n@server.after_request\ndef add_security_headers(response):\n    secure_headers.set_headers(response)\n    return response\n```\n\n### Alternative: `SecureWSGIMiddleware`\n\n```python\nimport dash\nfrom dash import html\nfrom secure import Secure\nfrom secure.middleware import SecureWSGIMiddleware\n\napp = dash.Dash(__name__)\nserver = app.server\nsecure_headers = Secure.with_default_headers()\n\napp.layout = html.Div(\"Hello Dash!\")\nserver.wsgi_app = SecureWSGIMiddleware(server.wsgi_app, secure=secure_headers)\n```\n\n## Django\n\nDjango is usually best integrated through Django middleware rather than raw WSGI wrapping.\n\n### Recommended: Django middleware class\n\nRegister the middleware class in your Django `MIDDLEWARE` setting.\n\n```python\nfrom secure import Secure\n\n\nclass SecureHeadersMiddleware:\n    def __init__(self, get_response):\n        self.get_response = get_response\n        self.secure = Secure.with_default_headers()\n\n    def __call__(self, request):\n        response = self.get_response(request)\n        self.secure.set_headers(response)\n        return response\n```\n\n### Fallback: set headers in a view\n\n```python\nfrom django.http import HttpResponse\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef home(request):\n    response = HttpResponse(\"Hello, world\")\n    secure_headers.set_headers(response)\n    return response\n```\n\n## Falcon\n\nFalcon exposes a clean response middleware hook.\n\n### Recommended: Falcon middleware\n\n```python\nimport falcon\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass SecureMiddleware:\n    def process_response(self, req, resp, resource, req_succeeded):\n        secure_headers.set_headers(resp)\n\n\napp = falcon.App(middleware=[SecureMiddleware()])\n```\n\n### Fallback: set headers in the resource\n\n```python\nimport falcon\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass HelloWorldResource:\n    def on_get(self, req, resp):\n        resp.text = \"Hello, world\"\n        secure_headers.set_headers(resp)\n\n\napp = falcon.App()\napp.add_route(\"/\", HelloWorldResource())\n```\n\n## FastAPI\n\nASGI framework. Middleware is the clearest default.\n\n### Recommended: `SecureASGIMiddleware`\n\n```python\nfrom fastapi import FastAPI\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\n\napp = FastAPI()\nsecure_headers = Secure.with_default_headers()\n\napp.add_middleware(SecureASGIMiddleware, secure=secure_headers)\n```\n\n### Alternative: `@app.middleware(\"http\")`\n\n```python\nfrom fastapi import FastAPI\nfrom secure import Secure\n\napp = FastAPI()\nsecure_headers = Secure.with_default_headers()\n\n\n@app.middleware(\"http\")\nasync def add_security_headers(request, call_next):\n    response = await call_next(request)\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\n### Fallback: set headers in one route\n\n```python\nfrom fastapi import FastAPI, Response\nfrom secure import Secure\n\napp = FastAPI()\nsecure_headers = Secure.with_default_headers()\n\n\n@app.get(\"/\")\ndef home(response: Response):\n    secure_headers.set_headers(response)\n    return {\"hello\": \"world\"}\n```\n\n## Flask\n\nWSGI framework with a straightforward response hook.\n\n### Recommended: `after_request`\n\n```python\nfrom flask import Flask\nfrom secure import Secure\n\napp = Flask(__name__)\nsecure_headers = Secure.with_default_headers()\n\n\n@app.after_request\ndef add_security_headers(response):\n    secure_headers.set_headers(response)\n    return response\n```\n\n### Alternative: `SecureWSGIMiddleware`\n\n```python\nfrom flask import Flask\nfrom secure import Secure\nfrom secure.middleware import SecureWSGIMiddleware\n\napp = Flask(__name__)\nsecure_headers = Secure.with_default_headers()\n\napp.wsgi_app = SecureWSGIMiddleware(app.wsgi_app, secure=secure_headers)\n```\n\n## Masonite\n\nMinimal fallback example. Masonite routing and response APIs vary by version, so apply `Secure` to the response object you actually return.\n\n### Minimal fallback: apply to the response you return\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef home(response):\n    rendered = response.json({\"hello\": \"world\"})\n    secure_headers.set_headers(rendered)\n    return rendered\n```\n\n## Morepath\n\nMinimal fallback example. Morepath does not expose a conventional middleware layer for this, so view-level mutation is the practical integration point.\n\n### Minimal fallback: set headers in the view\n\n```python\nimport morepath\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass App(morepath.App):\n    pass\n\n\n@App.path(path=\"\")\nclass Root:\n    pass\n\n\n@App.view(model=Root)\ndef home(self, request):\n    response = morepath.Response(\"Hello, world\")\n    secure_headers.set_headers(response)\n    return response\n```\n\n## Pyramid\n\nPyramid applications commonly use tweens for cross-cutting response changes.\n\n### Recommended: tween\n\nRegister the tween in your `Configurator` with `config.add_tween(\"yourpackage.security.add_security_headers\")`.\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef add_security_headers(handler, registry):\n    def tween(request):\n        response = handler(request)\n        secure_headers.set_headers(response)\n        return response\n\n    return tween\n```\n\n### Fallback: set headers in a view\n\n```python\nfrom pyramid.response import Response\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef home(request):\n    response = Response(\"Hello, world\")\n    secure_headers.set_headers(response)\n    return response\n```\n\n## Quart\n\nAsync Flask-compatible framework.\n\n### Recommended: `after_request` with `set_headers_async()`\n\n```python\nfrom quart import Quart\nfrom secure import Secure\n\napp = Quart(__name__)\nsecure_headers = Secure.with_default_headers()\n\n\n@app.after_request\nasync def add_security_headers(response):\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\n### Fallback: set headers in a route\n\n```python\nfrom quart import Quart, Response\nfrom secure import Secure\n\napp = Quart(__name__)\nsecure_headers = Secure.with_default_headers()\n\n\n@app.route(\"/\")\nasync def home():\n    response = Response(\"Hello, world\")\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\n## Responder\n\nMinimal fallback example. Route handlers typically own the response, so route-level mutation is the practical integration point.\n\n### Minimal fallback: set headers in the route\n\n```python\nimport responder\nfrom secure import Secure\n\napi = responder.API()\nsecure_headers = Secure.with_default_headers()\n\n\n@api.route(\"/\")\nasync def home(req, resp):\n    resp.text = \"Hello, world\"\n    await secure_headers.set_headers_async(resp)\n```\n\n## Sanic\n\nSanic exposes response middleware for app-wide coverage.\n\n### Recommended: response middleware with `set_headers_async()`\n\n```python\nfrom sanic import Sanic\nfrom secure import Secure\n\napp = Sanic(\"secure-app\")\nsecure_headers = Secure.with_default_headers()\n\n\n@app.middleware(\"response\")\nasync def add_security_headers(request, response):\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\nUse route-level setters when you only need a small integration or do not want extra app wiring in tests.\n\n### Fallback: set headers in a route\n\n```python\nfrom sanic import Sanic, response\nfrom secure import Secure\n\napp = Sanic(\"secure-app\")\nsecure_headers = Secure.with_default_headers()\n\n\n@app.get(\"/\")\nasync def home(request):\n    resp = response.text(\"Hello, world\")\n    await secure_headers.set_headers_async(resp)\n    return resp\n```\n\n## Shiny\n\nShiny applications are ASGI apps, so ASGI middleware is the cleanest and most direct path.\n\n### Recommended: `SecureASGIMiddleware`\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\nfrom shiny import App, ui\n\nsecure_headers = Secure.with_default_headers()\n\napp_ui = ui.page_fluid(\"Hello Shiny!\")\n\n\ndef server(input, output, session):\n    pass\n\n\napp = App(app_ui, server)\napp = SecureASGIMiddleware(app, secure=secure_headers)\n```\n\nIf your Shiny app loads external assets or opens additional browser connections, you may need to extend CSP directives such as `connect-src`, `script-src`, or `style-src`. Start from the balanced preset and test the running app before relaxing the policy.\n\n## Starlette\n\nASGI framework. Use ASGI middleware unless you only need route-level control.\n\n### Recommended: `SecureASGIMiddleware`\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\nfrom starlette.applications import Starlette\nfrom starlette.responses import PlainTextResponse\nfrom starlette.routing import Route\n\nsecure_headers = Secure.with_default_headers()\n\n\nasync def home(request):\n    return PlainTextResponse(\"Hello, world\")\n\n\napp = Starlette(routes=[Route(\"/\", home)])\napp.add_middleware(SecureASGIMiddleware, secure=secure_headers)\n```\n\n### Alternative: set headers in an endpoint\n\n```python\nfrom secure import Secure\nfrom starlette.applications import Starlette\nfrom starlette.responses import Response\nfrom starlette.routing import Route\n\nsecure_headers = Secure.with_default_headers()\n\n\nasync def home(request):\n    response = Response(\"Hello, world\")\n    await secure_headers.set_headers_async(response)\n    return response\n\n\napp = Starlette(routes=[Route(\"/\", home)])\n```\n\n## Tornado\n\nMinimal fallback example. Tornado usually applies headers inside request handlers, so handler-level mutation is the practical integration point.\n\n### Minimal fallback: set headers in the handler\n\n```python\nimport tornado.web\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass MainHandler(tornado.web.RequestHandler):\n    def get(self):\n        self.write(\"Hello, world\")\n        secure_headers.set_headers(self)\n\n\napp = tornado.web.Application([(r\"/\", MainHandler)])\n```\n\n## TurboGears\n\nMinimal fallback example. If you do not already have a framework-level hook in place, controller-level mutation is the practical integration point.\n\n### Minimal fallback: set headers in the controller\n\n```python\nfrom tg import Response, TGController, expose\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nclass RootController(TGController):\n    @expose()\n    def index(self):\n        response = Response(\"Hello, world\")\n        secure_headers.set_headers(response)\n        return response\n\n\nroot = RootController()\n```\n\n## Custom frameworks\n\nIf your framework is not listed here, the integration rule is still simple: configure one `Secure` instance, then apply it to the response as late as possible before it is sent.\n\n### Recommended: use the response object's setter or headers mapping\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef add_security_headers(response):\n    secure_headers.set_headers(response)\n    return response\n```\n\n### Fallback: emit header pairs manually\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\nfor name, value in secure_headers.header_items():\n    response.headers[name] = value\n```\n"
  },
  {
    "path": "docs/headers/cache_control.md",
    "content": "# Cache-Control\n\n## What it does\n\n`Cache-Control` is a comma-separated list of **directives** that control caching behavior for both **requests** and **responses**. Used correctly, it helps prevent sensitive data from being cached and improves performance for cacheable assets.\n\n## Minimal example\n\n```python\nfrom secure import CacheControl, Secure\n\nsecure_headers = Secure(\n    cache=CacheControl().no_store().max_age(0)\n)\n```\n\n## Resulting header\n\n```http\nCache-Control: no-store, max-age=0\n```\n\n## Practical note\n\nUse `no-store` for sensitive pages such as sign-in or account settings. `no-cache` is different. It allows storage but requires revalidation before reuse.\n\n## Default behavior\n\nIf you create `CacheControl()` and do not add directives, it returns the library default value:\n\n- **Default header value:** `no-store, max-age=0`\n\nThis is a secure baseline intended to prevent storage of sensitive responses.\n\n## Using with `Secure`\n\nIf you do not configure any directives, the default value is emitted.\n`Preset.STRICT` includes `Cache-Control: no-store, max-age=0`; `Preset.BASIC` and `Preset.BALANCED` leave caching unchanged unless you add this builder.\n\n## Common recipes\n\n### 1) Prevent storing (recommended for sensitive responses)\n\n```python\nfrom secure import CacheControl\n\ncc = CacheControl()  # default: no-store, max-age=0\nprint(cc.header_name)   # Cache-Control\nprint(cc.header_value)  # no-store, max-age=0\n```\n\n### 2) Always revalidate (useful for dynamic HTML)\n\n```python\ncc = CacheControl().no_cache()\nprint(cc.header_value)  # no-cache\n```\n\n> Note: `no-cache` does **not** mean “do not store.” It means “store, but revalidate before reuse.”\n\n### 3) Cache-busted static assets (long-lived)\n\nIf your assets are fingerprinted (e.g., `/app.4f3c1.js`), you can cache them aggressively:\n\n```python\ncc = CacheControl().public().max_age(31536000).immutable()\nprint(cc.header_value)  # public, max-age=31536000, immutable\n```\n\n### 4) Shared caches (CDNs/proxies) vs browser caches\n\n```python\ncc = CacheControl().s_maxage(604800).max_age(60)\nprint(cc.header_value)  # s-maxage=604800, max-age=60\n```\n\n`s-maxage` applies to shared caches and overrides `max-age` for them.\n\n### 5) Stale content during revalidation / on error\n\n```python\ncc = (\n    CacheControl()\n    .max_age(604800)\n    .stale_while_revalidate(86400)\n    .stale_if_error(86400)\n)\nprint(cc.header_value)  # max-age=604800, stale-while-revalidate=86400, stale-if-error=86400\n```\n\n## Builder API\n\n### Boolean directives (no value)\n\n- `.no_store()`, `.no_cache()`, `.no_transform()`\n- `.public()`, `.private()`\n- `.must_revalidate()`, `.proxy_revalidate()`\n- `.immutable()`\n- `.must_understand()` (recommended to pair with `.no_store()` for safe fallback)\n\n### Parameterized directives (integer seconds)\n\n- `.max_age(seconds)`\n- `.s_maxage(seconds)`\n- `.min_fresh(seconds)` (request)\n- `.stale_while_revalidate(seconds)`\n- `.stale_if_error(seconds)`\n\n### Request directives\n\n- `.only_if_cached()`\n- `.max_stale(seconds=None)` (if omitted, accepts staleness of any age)\n\n## Escape hatches\n\n### `.value(\"...\")`\n\nSet an explicit header value (replaces all configured directives):\n\n```python\ncc = CacheControl().value(\"no-store, max-age=0\")\nprint(cc.header_value)  # no-store, max-age=0\n```\n\n### `.custom(\"token\")`\n\nAdd a **non-standard / non-MDN** directive token (for niche proxies/CDNs):\n\n```python\ncc = CacheControl().custom(\"x-cache-mode=aggressive\")\nprint(cc.header_value)  # x-cache-mode=aggressive\n```\n\n### `.clear()`\n\nReset to the default (no directives configured; default value will be returned).\n\n## Deterministic output & overwrites\n\n- Directives are rendered as a **stable**, comma-separated list.\n- Repeating a parameterized directive overwrites the previous value (e.g., calling `.max_age(60)` then `.max_age(0)` results in `max-age=0`).\n- The builder rejects obvious header-splitting primitives (CR/LF) in `.value(...)` and `.custom(...)`.\n\n## Attribution\n\nThis library implements security recommendations and behavior described by:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cache-control) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/content_security_policy.md",
    "content": "# Content-Security-Policy (CSP)\n\n## What it does\n\nThe `Content-Security-Policy` (CSP) response header helps mitigate cross-site scripting (XSS), data injection, and related attacks by restricting where content can be loaded from (scripts, styles, images, fonts, connections, frames, etc.).\n\nCSP is expressed as a list of **directives** separated by semicolons:\n\n```http\nContent-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'\n```\n\n## Minimal example\n\n```python\nfrom secure import ContentSecurityPolicy, Secure\n\ncsp = (\n    ContentSecurityPolicy()\n    .default_src(\"'self'\")\n    .object_src(\"'none'\")\n    .base_uri(\"'self'\")\n)\n\nsecure_headers = Secure(csp=csp)\n```\n\n## Resulting header\n\n```http\nContent-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'\n```\n\n## Practical note\n\nDo not treat `'unsafe-inline'` as a default starting point for scripts. Use it only as an app-specific compatibility adjustment and test the real app before and after any CSP change.\n\n## Library defaults\n\nIf you create a `ContentSecurityPolicy()` and do not configure any directives, it returns the library default:\n\n```text\ndefault-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'\n```\n\nThis matches the builder default.\nThe built-in presets use explicit CSP builders rather than this bare builder default. `Preset.BASIC` and `Preset.BALANCED` add `font-src`, `img-src`, `script-src-attr`, `style-src`, and `upgrade-insecure-requests`.\n\n## Best-practice baseline\n\nA common “safe baseline” CSP includes:\n\n- `default-src 'self'`\n- `object-src 'none'`\n- `base-uri 'self'`\n- `frame-ancestors 'self'` (or `'none'` if you never want to be framed)\n- `form-action 'self'`\n- optionally `upgrade-insecure-requests`\n\n> CSP is powerful but can break applications if rolled out too aggressively. Start with a report-only policy, review violations, then enforce.\n\n---\n\n## Configuration with `Secure`\n\nOnce you configure `csp`, apply it in your framework integration:\n\n```python\n# Flask example\nfrom flask import Flask, Response\n\napp = Flask(__name__)\nsecure_headers = Secure(csp=csp)\n\n@app.after_request\ndef add_security_headers(response: Response) -> Response:\n    secure_headers.set_headers(response)\n    return response\n```\n\n### Report-only mode\n\nTo observe violations without enforcing (recommended for rollout):\n\n```python\ncsp_report_only = (\n    ContentSecurityPolicy()\n    .report_only()\n    .default_src(\"'self'\")\n    .script_src(\"'self'\")\n)\n\nsecure_headers = Secure(csp=csp_report_only)\n```\n\nUse `.enforce()` to switch back to the enforcing header name.\n\n---\n\n## Fluent directive methods\n\nCommon methods include:\n\n- Fetch directives: `default_src`, `script_src`, `style_src`, `img_src`, `font_src`, `connect_src`, `media_src`, `frame_src`, `worker_src`, `manifest_src`, `fenced_frame_src`, `object_src`\n- Navigation / embedding: `base_uri`, `form_action`, `frame_ancestors`\n- Policy controls: `sandbox`, `upgrade_insecure_requests`\n- Reporting: `report_to`, `report_uri` _(deprecated in MDN)_\n\n### Deterministic output and deduplication\n\n- Each directive name appears at most once.\n- Tokens passed to a directive are deduplicated (first-seen order preserved).\n- Serialization is deterministic and uses `\"; \"` between directives.\n\n---\n\n## Helper utilities\n\n### Keywords\n\nUse `keyword()` to safely produce quoted CSP keywords like `'self'` and `'none'`:\n\n```python\nContentSecurityPolicy.keyword(\"self\")  # \"'self'\"\nContentSecurityPolicy.keyword(\"none\")  # \"'none'\"\n```\n\n### Nonces\n\nUse `nonce()` to produce a CSP nonce source expression:\n\n```python\nnonce_value = \"abc123==\"  # base64 / url-safe base64\nContentSecurityPolicy.nonce(nonce_value)  # \"'nonce-abc123=='\"\n```\n\n---\n\n## Escape hatches\n\n### Set an exact policy string\n\nIf you need full control (or want to carry over an existing CSP string), use `.value(...)`:\n\n```python\ncsp = ContentSecurityPolicy().value(\n    \"default-src 'self'; script-src 'self' https://cdn.example; object-src 'none'\"\n)\n```\n\n`.set(...)` is an alias for `.value(...)`.\n\n### Clear configuration\n\n```python\ncsp = ContentSecurityPolicy().default_src(ContentSecurityPolicy.keyword(\"self\"))\ncsp.clear()  # resets back to library default behavior\n```\n\n### Custom directives\n\nIf you need a directive not covered by a helper method:\n\n```python\ncsp = (\n    ContentSecurityPolicy()\n    .custom_directive(\"default-src\", \"'self'\")\n    .custom_directive(\"script-src\", \"'self'\")\n)\n\n# `.custom(...)` is an alias\n```\n\n---\n\n## Nonce + `strict-dynamic` example (recommended pattern)\n\nWhen you use nonces, the nonce must be generated **per response** and also placed in your HTML script tag(s).\nA common pattern is to generate the nonce in request context, then build CSP using it.\n\n### Framework-agnostic CSP construction\n\n```python\nimport secrets\nfrom secure import ContentSecurityPolicy\n\nnonce = secrets.token_urlsafe(16)\n\ncsp = (\n    ContentSecurityPolicy()\n    .default_src(\"'self'\")\n    .script_src(\n        ContentSecurityPolicy.nonce(nonce),\n        ContentSecurityPolicy.keyword(\"strict-dynamic\"),\n    )\n    .object_src(\"'none'\")\n)\n\nprint(csp.header_value)\n# default-src 'self'; script-src 'nonce-...' 'strict-dynamic'; object-src 'none'\n```\n\n### Flask pattern (nonce shared via `g`)\n\n```python\nimport secrets\nfrom flask import Flask, Response, g\n\nfrom secure import ContentSecurityPolicy, Secure\n\napp = Flask(__name__)\n\n@app.before_request\ndef set_nonce() -> None:\n    g.csp_nonce = secrets.token_urlsafe(16)\n\n@app.after_request\ndef add_security_headers(response: Response) -> Response:\n    csp = (\n        ContentSecurityPolicy()\n        .default_src(\"'self'\")\n        .script_src(\n            ContentSecurityPolicy.nonce(g.csp_nonce),\n            ContentSecurityPolicy.keyword(\"strict-dynamic\"),\n        )\n        .style_src(\"'self'\")\n        .object_src(\"'none'\")\n    )\n    Secure(csp=csp).set_headers(response)\n    return response\n```\n\nIn your HTML rendering, use the same nonce:\n\n```html\n<script nonce=\"{{ g.csp_nonce }}\">\n  console.log(\"Allowed because nonce matches CSP\");\n</script>\n```\n\n---\n\n## Reporting notes (MDN)\n\n- `report-to` is the modern mechanism.\n- `report-uri` is deprecated in MDN; some browsers that support `report-to` may ignore `report-uri`.\n- If you need broad compatibility during migration, you may specify both.\n\n---\n\n## References\n\n- [MDN: Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)\n- [MDN: CSP guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#content-security-policy)\n\n## Attribution\n\nThis library implements security recommendations and reference material from:\n\n- MDN Web Docs (licensed under CC-BY-SA 2.5)\n- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0)\n"
  },
  {
    "path": "docs/headers/cross-origin-resource-policy.md",
    "content": "# Cross-Origin-Resource-Policy (CORP)\n\n## What it does\n\nThe `Cross-Origin-Resource-Policy` (CORP) response header lets a **resource owner** declare what sites/origins are allowed to load that resource.\n\nThis header is commonly used to reduce cross-origin data leaks by controlling who can load your resources (images, scripts, etc.) and by blocking certain cross-origin/cross-site `no-cors` requests when the policy is more restrictive.\n\n## Minimal example\n\n```python\nfrom secure import CrossOriginResourcePolicy, Secure\n\nsecure_headers = Secure(\n    corp=CrossOriginResourcePolicy().same_origin()\n)\n```\n\n## Resulting header\n\n```http\nCross-Origin-Resource-Policy: same-origin\n```\n\n## Practical note\n\n`same-origin` is a good default for sensitive resources. If you serve shared assets across subdomains, test `same-site` carefully before widening the policy further.\n\n## Best Practices\n\n- **`same-origin`**: Strong default for sensitive resources; only allow loads from the same origin.\n- **`same-site`**: Useful when you need to share resources across subdomains on the same “site” but not with unrelated sites.\n- **`cross-origin`**: Most permissive; allow any origin to load the resource (use intentionally, not by accident).\n\n## Configuration with `Secure`\n\nThe `CrossOriginResourcePolicy` class provides a fluent API for setting CORP directives and integrates cleanly with `Secure(...)`.\n\n> Library default: if you do not change it, the library’s default value is `same-origin`.\n> Presets: `Preset.BASIC` and `Preset.BALANCED` include `same-origin`; `Preset.STRICT` does not add CORP by default.\n\n### Methods Available\n\n- **`same_origin()`**: Set `Cross-Origin-Resource-Policy: same-origin`\n- **`same_site()`**: Set `Cross-Origin-Resource-Policy: same-site`\n- **`cross_origin()`**: Set `Cross-Origin-Resource-Policy: cross-origin`\n- **`value(value)`**: Set an explicit value (escape hatch; canonicalizes known directives)\n- **`clear()`**: Reset to the library default value\n- **`set(value)`**: Backwards-compatible alias for `value(...)`\n\n## Example Usage\n\nTo restrict resource loading to the same origin:\n\n```python\ncorp = CrossOriginResourcePolicy().same_origin()\nprint(corp.header_name)   # Output: 'Cross-Origin-Resource-Policy'\nprint(corp.header_value)  # Output: 'same-origin'\n```\n\nTo allow resource loading from the same site (useful for subdomains):\n\n```python\ncorp = CrossOriginResourcePolicy().same_site()\nprint(corp.header_value)  # Output: 'same-site'\n```\n\n## **Attribution**\n\nThis library implements security recommendations from trusted sources:\n\n- [MDN Web Docs: `Cross-Origin-Resource-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project: Cross-Origin-Resource-Policy](https://owasp.org/www-project-secure-headers/#cross-origin-resource-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/cross_origin_embedder_policy.md",
    "content": "# Cross-Origin-Embedder-Policy (COEP)\n\n## What it does\n\nThe **`Cross-Origin-Embedder-Policy`** response header configures the current document’s policy for **loading and embedding cross-origin resources**.\n\nAt a high level, COEP lets you:\n\n- keep the default behavior (`unsafe-none`),\n- require explicit opt-in via **CORP** (`Cross-Origin-Resource-Policy`) and/or **CORS** (`require-corp`), or\n- allow some cross-origin loading while **stripping credentials** (`credentialless`).\n\n## Minimal example\n\n```python\nfrom secure import CrossOriginEmbedderPolicy, CrossOriginOpenerPolicy, Secure\n\nsecure_headers = Secure(\n    coep=CrossOriginEmbedderPolicy().require_corp(),\n    coop=CrossOriginOpenerPolicy().same_origin(),\n)\n```\n\n## Resulting header\n\n```http\nCross-Origin-Embedder-Policy: require-corp\n```\n\n## Practical note\n\nCOEP is most useful when you are intentionally working toward cross-origin isolation. It can break third-party assets that do not send compatible CORP or CORS headers, so test the full app before enabling it broadly.\n\n## Directive values\n\nCOEP is a **single-value** header (choose one):\n\n| Value            | Meaning                                                                                                                                                                                                     |\n| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `unsafe-none`    | Allows cross-origin resources **without** explicit permission via CORP or CORS. _(This is the browser default if the header is not sent.)_                                                                  |\n| `require-corp`   | Blocks cross-origin resource loading unless the resource is permitted via **CORP** (for `no-cors` requests) or via **CORS** (for `cors` requests).                                                          |\n| `credentialless` | Allows `no-cors` cross-origin resource loading **without** CORP opt-in, but sends requests **without credentials** (cookies omitted and ignored). For other request modes, behavior matches `require-corp`. |\n\n## Library default vs browser default\n\n- **Browser behavior when the header is absent:** `unsafe-none`.\n- **This library’s builder default:** `require-corp` (a stricter, security-forward default).\n\nIf you want “no-op” behavior, you must explicitly choose it:\n\n```python\nfrom secure import CrossOriginEmbedderPolicy\n\ncoep = CrossOriginEmbedderPolicy().unsafe_none()\n```\n\n## Cross-origin isolation (COOP + COEP)\n\nSome powerful browser features require your document to be **cross-origin isolated**. To enable this, you generally need:\n\n- `Cross-Origin-Embedder-Policy: require-corp` **or** `credentialless`, and\n- `Cross-Origin-Opener-Policy: same-origin`.\n\n## Usage with `Secure`\n\nYou can inspect the emitted header pairs with `secure_headers.header_items()` if you need to confirm the final output.\n\n`Preset.STRICT` includes COEP by default; `Preset.BASIC` and `Preset.BALANCED` do not.\n\n## Header builder API\n\n```python\nfrom secure import CrossOriginEmbedderPolicy\n\ncoep = (\n    CrossOriginEmbedderPolicy()\n    .credentialless()   # or .require_corp() / .unsafe_none()\n)\n\nprint(coep.header_name)   # \"Cross-Origin-Embedder-Policy\"\nprint(coep.header_value)  # \"credentialless\"\n```\n\n### Methods\n\n- `unsafe_none()`: set the value to `unsafe-none`\n- `require_corp()`: set the value to `require-corp`\n- `credentialless()`: set the value to `credentialless`\n- `set(value)`: set a custom value\n- `clear()`: reset to the library default (`require-corp`)\n\n## Notes / gotchas\n\n- `require-corp` can break embedding third-party resources unless they opt-in via CORP or are requested in `cors` mode.\n- `credentialless` can be a pragmatic alternative for some `no-cors` resources, but it comes with the tradeoff of **no cookies/credentials**.\n\n## Attribution\n\nThis library implements security recommendations and definitions from trusted sources:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy) (CC-BY-SA 2.5)\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cross-origin-embedder-policy) (CC-BY-SA 4.0)\n"
  },
  {
    "path": "docs/headers/cross_origin_opener_policy.md",
    "content": "# Cross-Origin-Opener-Policy\n\n## What it does\n\nThe `Cross-Origin-Opener-Policy` (COOP) response header controls whether documents opened via `Window.open()` (or navigations) share the same **browsing context group (BCG)** as their opener. When a document is opened into a new BCG, references between the opener and the opened document are severed, which helps mitigate cross-origin attacks often referred to as **XS-Leaks**.\n\n## Minimal example\n\n```python\nfrom secure import CrossOriginOpenerPolicy, Secure\n\nsecure_headers = Secure(\n    coop=CrossOriginOpenerPolicy().same_origin()\n)\n```\n\n## Resulting header\n\n```http\nCross-Origin-Opener-Policy: same-origin\n```\n\n## Practical note\n\n`same-origin` is a strong default, but popup-based flows such as OAuth or payment providers sometimes need `same-origin-allow-popups`. Test those flows before tightening COOP.\n\n## Defaults\n\n- **Browser/spec behavior:** If the header is **absent**, the effective behavior is equivalent to `unsafe-none` (opt-out).\n- **Library default:** This library’s builder defaults to `same-origin` (a secure default), and the built-in presets also configure COOP as `same-origin`.\n\n## Best Practices\n\n- **`same-origin`**: Strong isolation; commonly used for cross-origin isolation (often paired with COEP).\n- **`same-origin-allow-popups`**: Like `same-origin`, but relaxes behavior for integrations that open trusted popups/tabs that opt out (e.g., OAuth/payment flows).\n- **`noopener-allow-popups`**: Always isolates into a new BCG (except when opened by a same-origin document that also uses `noopener-allow-popups`). Useful when you need to isolate **same-origin** apps from each other (e.g., `/chat` vs `/passwords`) while still allowing popups.\n- **`unsafe-none`**: Opts out of COOP isolation.\n\n## Configuration with `Secure`\n\nUse the `CrossOriginOpenerPolicy` builder and pass it into `Secure(...)`.\n\n## Methods Available\n\nDirective helpers (recommended):\n\n- `same_origin()`\n- `same_origin_allow_popups()`\n- `noopener_allow_popups()`\n- `unsafe_none()`\n\nEscape hatches:\n\n- `value(\"...\")` / `custom(\"...\")`: Set a raw value (rejects CR/LF).\n- `set(\"...\")`: Backwards-compatible alias for `value(...)`.\n- `clear()`: Reset back to the library default (`same-origin`).\n\n## Example Usage\n\n```python\nfrom secure import CrossOriginOpenerPolicy, Secure\n\ncoop = CrossOriginOpenerPolicy().same_origin()\nprint(coop.header_name)   # 'Cross-Origin-Opener-Policy'\nprint(coop.header_value)  # 'same-origin'\n\nsecure_headers = Secure(coop=coop)\n```\n\n## Notes\n\n- For **cross-origin isolation** (e.g., `SharedArrayBuffer`), COOP is typically paired with **COEP** (often `require-corp`), and your app must satisfy other isolation requirements.\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- [MDN Web Docs: Cross-Origin-Opener-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cross-origin-opener-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/custom_header.md",
    "content": "# CustomHeader Class\n\n## What it does\n\nThe `CustomHeader` class lets you create arbitrary HTTP response headers when `secure` does not provide a dedicated builder.\n\n## Minimal example\n\n```python\nfrom secure import CustomHeader, Secure\n\ncustom_header = CustomHeader(\"X-Custom-Header\", \"CustomValue\")\nsecure_headers = Secure(custom=[custom_header])\n```\n\n## Resulting header\n\n```http\nX-Custom-Header: CustomValue\n```\n\n## Practical note\n\nUse `CustomHeader` for app-specific or infrastructure-specific headers. If you later call `allowlist_headers(...)`, remember to allow the custom name explicitly when needed.\n\n## Best Practices\n\n- Prefer standard header names when they exist; use custom names only for application- or infrastructure-specific behavior.\n- If you use `allowlist_headers(...)`, remember that custom names may need to be added through `allow_extra=...`.\n\n## Configuration with `Secure`\n\nUse `CustomHeader` when you need a header without a dedicated builder. You can set the name and value directly, then update the value later if needed.\n\n### Methods Available\n\n- **`set(value)` / `value(value)`**: Updates the value of the custom header.\n- **`header_value`**: Property that retrieves the current value of the custom header.\n\n## Example Usage\n\nTo define a custom header and use it in a secure configuration:\n\n```python\nfrom secure import CustomHeader\n\ncustom_header = CustomHeader(\"X-Custom-Header\", \"CustomValue\")\nprint(custom_header.header_name)   # Output: 'X-Custom-Header'\nprint(custom_header.header_value)  # Output: 'CustomValue'\n\n# Update the value\ncustom_header.set(\"NewValue\")\nprint(custom_header.header_value)  # Output: 'NewValue'\n```\n\n## **Resources**\n\n- [MDN Web Docs: HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)\n\n## **Attribution**\n\nThis library implements security recommendations from trusted sources:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/dns_prefetch_control.md",
    "content": "# X-DNS-Prefetch-Control\n\n## What it does\n\n`X-DNS-Prefetch-Control` controls **DNS prefetching**, where browsers may proactively resolve domain names for links and referenced subresources (images, CSS, JS, etc.) in the background to reduce perceived latency.\n\n## Minimal example\n\n```python\nfrom secure import Secure, XDnsPrefetchControl\n\nsecure_headers = Secure(\n    xdfc=XDnsPrefetchControl().off()\n)\n```\n\n## Resulting header\n\n```http\nX-DNS-Prefetch-Control: off\n```\n\n## Practical note\n\nWhen this header is absent, supporting browsers commonly behave as if DNS prefetching is on. Add the header only when you want to state a clear preference.\n\n## Default behavior\n\nIf you create `XDnsPrefetchControl()` and do not set a directive, it returns the library default value:\n\n- **Default header value:** `off`\n\n> Note (MDN behavior): In browsers that support DNS prefetching, if this header is **not present**, the effective behavior is typically **`on`**. This library’s default is **privacy-first** when you choose to emit the header.\n\n## Using with `Secure`\n\nIf you don’t configure anything, the default value is emitted.\n`Preset.BASIC` includes `X-DNS-Prefetch-Control: off`; `Preset.BALANCED` and `Preset.STRICT` leave it out unless you add it explicitly.\n\n## Common recipes\n\n### 1) Disable DNS prefetching (recommended when you don’t control outbound links)\n\n```python\nfrom secure import XDnsPrefetchControl\n\nxdfc = XDnsPrefetchControl()  # default: off\nprint(xdfc.header_name)   # X-DNS-Prefetch-Control\nprint(xdfc.header_value)  # off\n```\n\n### 2) Enable DNS prefetching\n\n```python\nxdfc = XDnsPrefetchControl().on()\nprint(xdfc.header_value)  # on\n```\n\n### 3) Backwards-compatible builder names\n\nIf you prefer the older API vocabulary:\n\n```python\nxdfc = XDnsPrefetchControl().allow()   # == .on()\nxdfc = XDnsPrefetchControl().disable() # == .off()\n```\n\n## Builder API\n\n### Canonical directives\n\n- `.on()`\n  Enables DNS prefetching (commonly the effective behavior when the header is absent in supporting browsers).\n\n- `.off()`\n  Disables DNS prefetching (useful to reduce information leakage to third-party domains).\n\n### Backwards-compatible aliases\n\n- `.allow()` → same as `.on()`\n- `.disable()` → same as `.off()`\n\n## Escape hatches\n\n### `.value(\"...\")` / `.set(\"...\")`\n\nSet an explicit header value (replaces the current value):\n\n```python\nxdfc = XDnsPrefetchControl().value(\"off\")\nprint(xdfc.header_value)  # off\n```\n\nIf you pass `ON` / `Off` (any casing), the builder normalizes to `on` / `off` for stable output.\n\n### `.custom(\"token\")`\n\nSet a **non-standard / non-MDN** token (escape hatch):\n\n```python\nxdfc = XDnsPrefetchControl().custom(\"off\")\nprint(xdfc.header_value)  # off\n```\n\n(For this header, non-`on`/`off` values are unusual, but the escape hatch exists for consistency across the library.)\n\n### `.clear()`\n\nReset to the library default:\n\n```python\nxdfc = XDnsPrefetchControl().on().clear()\nprint(xdfc.header_value)  # off\n```\n\n## Deterministic output & overwrites\n\n- Output is always a **single token** (`on` or `off`) when using `.on()` / `.off()` (stable and deterministic).\n- Setting the value multiple times overwrites the previous value (last call wins).\n- `.set(...)`, `.value(...)`, and `.custom(...)` reject CR/LF; `Secure.validate_and_normalize_headers(...)` performs the broader normalization pass.\n\n## Compatibility notes\n\n- This header is **non-standard**.\n- Browser behavior differs across engines and versions; treat this as a best-effort control rather than a guaranteed security boundary.\n\n## Attribution\n\nThis library implements security recommendations and behavior described by:\n\n- [MDN Web Docs: X-DNS-Prefetch-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-DNS-Prefetch-Control) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project: X-DNS-Prefetch-Control](https://owasp.org/www-project-secure-headers/#x-dns-prefetch-control) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/permissions_policy.md",
    "content": "# Permissions-Policy\n\n## What it does\n\nThe `Permissions-Policy` HTTP response header lets you enable or disable access to selected browser features and powerful APIs in the current document and in nested browsing contexts (iframes). It replaces the deprecated `Feature-Policy` header.\n\nIn this library, `PermissionsPolicy` is a fluent builder for producing a single `Permissions-Policy` header value, suitable for applying via `Secure`.\n\n## Minimal example\n\n```python\nfrom secure import PermissionsPolicy, Secure\n\nsecure_headers = Secure(\n    permissions=PermissionsPolicy()\n        .geolocation()\n        .microphone()\n        .camera()\n)\n```\n\n## Resulting header\n\n```http\nPermissions-Policy: geolocation=(), microphone=(), camera=()\n```\n\n## Practical note\n\nBrowser support varies by feature. Keep the policy restrictive, then test the specific features your app actually needs in real browsers.\n\n## Best practices\n\n- Start restrictive: disable features you don’t need to reduce attack surface and protect privacy.\n- Enable selectively: allow features only where required, and only for trusted origins.\n- Validate in real browsers: support varies by feature and browser; test the behaviors you rely on.\n\n## Configuration with `Secure`\n\n`Preset.BALANCED` and `Preset.STRICT` include `geolocation=(), microphone=(), camera=()` by default; `Preset.BASIC` does not add `Permissions-Policy`.\n\n## Allowlist syntax\n\n`PermissionsPolicy` uses MDN-style allowlist syntax for each directive:\n\n- **No tokens** → `()` (feature disabled)\n- **`\"*\"`** → `*` (feature allowed everywhere; must be used alone)\n- **`\"self\"` / `\"src\"`** → tokens for same-origin / iframe source origin\n- **Origins** → pass a URL (e.g. `\"https://a.example.com\"`); it is emitted as a double-quoted origin in the header value\n\nExamples:\n\n```python\npolicy = (\n    PermissionsPolicy()\n    .geolocation(\"*\")  # geolocation=*\n    .camera(\"self\", \"https://a.example.com\")  # camera=(self \"https://a.example.com\")\n    .microphone()  # microphone=()\n)\nprint(policy.header_value)\n```\n\n## Methods\n\nCommon methods you’ll use:\n\n- **`geolocation(*allowlist)`**, **`camera(*allowlist)`**, **`microphone(*allowlist)`**, etc.: configure specific directives.\n- **`add_directive(directive, *allowlist)`** (alias: **`directive(...)`**): set any directive by name (future-proof when browsers add new ones).\n- **`value(raw)`** (alias: **`set(raw)`**): set a complete prebuilt header value (escape hatch; bypasses directive building).\n- **`clear()`**: remove all configured directives and any raw override.\n\n## Example usage\n\n```python\nfrom secure import PermissionsPolicy, Secure\n\npermissions_policy = (\n    PermissionsPolicy()\n    .geolocation()  # disabled\n    .camera(\"self\", \"https://a.example.com\")\n    .microphone(\"self\")\n)\n\nprint(permissions_policy.header_name)   # 'Permissions-Policy'\nprint(permissions_policy.header_value)  # 'geolocation=(), camera=(self \"https://a.example.com\"), microphone=(self)'\n\nsecure_headers = Secure(permissions=permissions_policy)\n```\n\n## Resources\n\n- [MDN Web Docs: Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy)\n- [OWASP Secure Headers Project: Permissions-Policy](https://owasp.org/www-project-secure-headers/#permissions-policy)\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- MDN Web Docs (licensed under CC-BY-SA 2.5)\n- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0)\n"
  },
  {
    "path": "docs/headers/referrer_policy.md",
    "content": "# Referrer-Policy\n\n## What it does\n\nThe `Referrer-Policy` response header controls how much referrer information (sent via the `Referer` header) is included with outgoing requests. It is primarily a privacy and data-minimization control, with important security implications when navigating across origins or downgrading from HTTPS to HTTP.\n\n> Note: `Referer` is intentionally misspelled in HTTP. `Referrer-Policy` does **not** share that misspelling.\n\n## Minimal example\n\n```python\nfrom secure import ReferrerPolicy, Secure\n\nsecure_headers = Secure(\n    referrer=ReferrerPolicy().strict_origin_when_cross_origin()\n)\n```\n\n## Resulting header\n\n```http\nReferrer-Policy: strict-origin-when-cross-origin\n```\n\n## Practical note\n\n`strict-origin-when-cross-origin` is the recommended default because it preserves same-origin behavior while avoiding full URL leakage across origins. Move to `no-referrer` only when you want the strictest privacy posture.\n\n## Default behavior\n\n**Default header value:** `strict-origin-when-cross-origin`\n\nThis matches modern browser defaults: if no policy is specified (or the provided value is invalid), the effective policy is `strict-origin-when-cross-origin`.\n\n## Best practices (recommended choices)\n\n- **`strict-origin-when-cross-origin` (recommended default)**  \n  Sends the full referrer (origin + path + query) for same-origin requests; sends **origin only** for cross-origin HTTPS→HTTPS; sends **no `Referer`** when downgrading (HTTPS→HTTP).\n- **`no-referrer` (max privacy)**  \n  Omits the `Referer` header entirely for all requests.\n- **`same-origin` (strict privacy across sites)**  \n  Sends referrer only for same-origin requests; omits it for cross-origin requests.\n- **Avoid `unsafe-url`** unless you fully understand the impact (it can leak sensitive URL data across origins and to insecure destinations).\n\n## Configuration with `Secure`\n\n```python\nfrom secure import ReferrerPolicy, Secure\n\nsecure_headers = Secure(\n    referrer=ReferrerPolicy()  # uses the default: strict-origin-when-cross-origin\n)\n```\n\n`Preset.BALANCED` uses `strict-origin-when-cross-origin`; `Preset.BASIC` and `Preset.STRICT` use `no-referrer`.\n\n### Set a single explicit policy\n\nUse `value(...)` (or `custom(...)`) when you want to **replace** any configured policies and set exactly one value:\n\n```python\nfrom secure import ReferrerPolicy, Secure\n\nsecure_headers = Secure(\n    referrer=ReferrerPolicy().value(\"no-referrer\")\n)\n```\n\nYou can also use the fluent directive helpers:\n\n```python\nsecure_headers = Secure(\n    referrer=ReferrerPolicy().no_referrer()\n)\n```\n\n### Specify a fallback policy list (HTTP header only)\n\nBrowsers support a **comma-separated list** in the `Referrer-Policy` HTTP header. The desired (most modern) policy should be listed **last**.\n\n```python\nfrom secure import ReferrerPolicy\n\nrp = ReferrerPolicy().fallback(\"no-referrer\", \"strict-origin-when-cross-origin\")\nprint(rp.header_name)   # Referrer-Policy\nprint(rp.header_value)  # no-referrer, strict-origin-when-cross-origin\n```\n\nYou can build the same list with `.add(...)`:\n\n```python\nrp = (\n    ReferrerPolicy()\n    .clear()\n    .add(\"no-referrer\")\n    .add(\"strict-origin-when-cross-origin\")\n)\n```\n\n> Note: the fallback _list_ behavior is supported in the HTTP header, but not in the HTML `referrerpolicy` attribute.\n\n## API reference (ReferrerPolicy)\n\n### Core builder methods\n\n- `value(\"...\")` / `custom(\"...\")`\n  Replace all configured policies with the provided value (supports comma-separated lists).\n- `add(\"...\")` / `set(\"...\")`\n  Append one or more policy tokens (supports comma-separated lists). Duplicate tokens are ignored.\n- `fallback(*policies)`\n  Replace the current policies with an explicit ordered fallback list.\n- `clear()`\n  Clear configured policies (returns to default behavior unless you add values afterward).\n\n### Directive helpers (MDN policies)\n\nEach of these appends the corresponding token (same behavior as `add(\"token\")`):\n\n- `no_referrer()` → `no-referrer`\n  Omits the `Referer` header entirely.\n- `no_referrer_when_downgrade()` → `no-referrer-when-downgrade`\n  Sends full referrer for same-or-more secure requests; omits referrer on downgrade (HTTPS→HTTP).\n- `origin()` → `origin`\n  Sends only the origin (scheme + host + port).\n- `origin_when_cross_origin()` → `origin-when-cross-origin`\n  Same-origin: full referrer; cross-origin and downgrade: origin only.\n- `same_origin()` → `same-origin`\n  Same-origin: full referrer; cross-origin: omit referrer.\n- `strict_origin()` → `strict-origin`\n  Sends only origin for same-security requests; omits on downgrade (HTTPS→HTTP).\n- `strict_origin_when_cross_origin()` → `strict-origin-when-cross-origin`\n  Same-origin: full referrer; cross-origin HTTPS→HTTPS: origin only; downgrade: omit.\n- `unsafe_url()` → `unsafe-url`\n  Sends origin + path + query for all requests (generally discouraged; may leak sensitive data).\n\n## Example usage\n\n```python\nfrom secure import ReferrerPolicy\n\nreferrer_policy = ReferrerPolicy().strict_origin_when_cross_origin()\nprint(referrer_policy.header_name)   # 'Referrer-Policy'\nprint(referrer_policy.header_value)  # 'strict-origin-when-cross-origin'\n```\n\n## Resources\n\n- MDN Web Docs: Referrer-Policy\n  [https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy)\n- OWASP Secure Headers Project: Referrer-Policy\n  [https://owasp.org/www-project-secure-headers/#referrer-policy](https://owasp.org/www-project-secure-headers/#referrer-policy)\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- MDN Web Docs (licensed under CC-BY-SA 2.5)\n  [https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor](https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor)\n  [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/)\n- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0)\n  [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)\n"
  },
  {
    "path": "docs/headers/server.md",
    "content": "# Server Header\n\n## What it does\n\nThe `Server` header can reveal details about the software handling the request. In `secure`, the builder defaults to an empty string so your application can avoid adding identifying detail when the surrounding stack allows it.\n\n## Minimal example\n\n```python\nfrom secure import Secure, Server\n\nsecure_headers = Secure(\n    server=Server().set(\"\")\n)\n```\n\n## Resulting header\n\n```http\nServer:\n```\n\n## Practical note\n\nApplication code can only control this header if the surrounding stack does not re-add its own value. Check your ASGI server, WSGI server, proxy, or CDN settings too.\n\n## Best Practices\n\n- **Set an empty value or custom string**: Use an empty or generic value when you want `secure` to control the header.\n- **Avoid exposing server information**: Avoid leaving the default server response, which may expose sensitive version information.\n- **Check upstream defaults**: Proxies, ASGI servers, and framework middleware may still add their own `Server` header unless you disable that behavior.\n\n## Configuration with `Secure`\n\nUse `Server` to control the `Server` header value. Its default value is an empty string.\n\n### Methods Available\n\n- **`set(value)`**: Set a custom value for the `Server` header.\n- **`clear()`**: Clear any custom value and revert the header to its default secure value (an empty string).\n\n## Example Usage\n\nTo set up the `Server` header and hide the server information:\n\n```python\nfrom secure import Server\n\nserver_header = Server().set(\"\")\nprint(server_header.header_name)   # Output: 'Server'\nprint(server_header.header_value)  # Output: ''\n```\n\nThen pass it into `Secure`:\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure(server=server_header)\n```\n\n### Special Considerations for Frameworks\n\nSome frameworks like Uvicorn automatically inject a `Server` header. If you're using Uvicorn and need to override or remove this header, refer to the [framework integration guide](../frameworks.md) for specific instructions on how to disable Uvicorn's default `Server` header.\n\n## **Resources**\n\n- [MDN Web Docs: Server Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server)\n- [OWASP Secure Headers Project: Server Header](https://owasp.org/www-project-secure-headers/#server-header)\n\n## **Attribution**\n\nThis library implements security recommendations from trusted sources:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#server-header) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/strict_transport_security.md",
    "content": "# Strict-Transport-Security (HSTS)\n\n## What it does\n\nThe `Strict-Transport-Security` (HSTS) header tells browsers that a host **must only be accessed over HTTPS**. Once a browser has received this header, it will automatically upgrade future HTTP navigations to HTTPS for the configured duration, helping prevent man-in-the-middle and downgrade attacks.\n\n> Important: Browsers **ignore** `Strict-Transport-Security` if it is delivered over **insecure HTTP**. You must send it over HTTPS only.\n\n## Minimal example\n\n```python\nfrom secure import Secure, StrictTransportSecurity\n\nsecure_headers = Secure(\n    hsts=StrictTransportSecurity().max_age(31536000).include_subdomains()\n)\n```\n\n## Resulting header\n\n```http\nStrict-Transport-Security: max-age=31536000; includeSubDomains\n```\n\n## Practical note\n\nDo not enable `includeSubDomains` until every subdomain is HTTPS-ready. Treat `preload()` as a deliberate rollout step because removal is slow once a domain is on the preload list.\n\n## Default behavior\n\nIf you do not configure any directives, this library emits the default header value:\n\n- **Default header value:** `max-age=31536000` (one year)\n\n## Best practices\n\n- **Use a long `max-age`**: One year (`31536000` seconds) is a common baseline.\n- **Include subdomains (carefully)**: Add `includeSubDomains` only if _all_ subdomains are HTTPS-ready.\n- **Only use `preload` when you mean it**:\n  - `preload` is intended for submitting your domain to the HSTS preload list.\n  - When using `preload`, the library enforces MDN’s requirements:\n    - `max-age` must be **at least 31536000**\n    - `includeSubDomains` must be present\n\n## Configuration with `Secure`\n\nUse `StrictTransportSecurity` for fluent, chainable configuration.\n\n`Preset.BASIC` and `Preset.BALANCED` use one year with `includeSubDomains`; `Preset.STRICT` uses two years with `includeSubDomains`.\n\n### Preload configuration\n\nIf you opt into preload, the library ensures preload requirements are satisfied:\n\n```python\nfrom secure import StrictTransportSecurity\n\nhsts = (\n    StrictTransportSecurity()\n    .max_age(31536000)\n    .include_subdomains()\n    .preload()\n)\n\nprint(hsts.header_name)   # 'Strict-Transport-Security'\nprint(hsts.header_value)  # 'max-age=31536000; includeSubDomains; preload'\n```\n\nIf `preload()` is enabled with a `max-age` less than `31536000`, the header builder will raise a `ValueError`.\n\n## Methods available\n\n- **`max_age(seconds)`**\n  Set `max-age`: how long (in seconds) the browser should remember to only use HTTPS for this host.\n\n- **`include_subdomains()`**\n  Add `includeSubDomains`: apply the HSTS policy to all subdomains as well.\n\n- **`preload()`**\n  Add `preload`: indicates intent to meet HSTS preload requirements. This library:\n  - automatically enables `includeSubDomains`\n  - enforces `max-age >= 31536000`\n\n- **`clear()`**\n  Clear configured directives and reset back to the library default behavior.\n\n- **`value(str)` / `set(str)`**\n  Escape hatch: set a raw header value (replaces any configured directives). The value must not contain CR/LF characters.\n\n## Example usage\n\nMinimal one-year HSTS:\n\n```python\nfrom secure import StrictTransportSecurity\n\nhsts = StrictTransportSecurity().max_age(31536000)\nprint(hsts.header_value)  # 'max-age=31536000'\n```\n\nOne-year HSTS including subdomains:\n\n```python\nfrom secure import StrictTransportSecurity\n\nhsts = StrictTransportSecurity().max_age(31536000).include_subdomains()\nprint(hsts.header_value)  # 'max-age=31536000; includeSubDomains'\n```\n\n## Resources\n\n- MDN Web Docs: Strict-Transport-Security\n  [https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)\n- OWASP Secure Headers Project\n  [https://owasp.org/www-project-secure-headers/](https://owasp.org/www-project-secure-headers/)\n- HSTS Preload List\n  [https://hstspreload.org/](https://hstspreload.org/)\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- MDN Web Docs: Strict-Transport-Security (licensed under CC-BY-SA 2.5)\n  [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/)\n- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0)\n  [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)\n"
  },
  {
    "path": "docs/headers/x-permitted-cross-domain-policies.md",
    "content": "# X-Permitted-Cross-Domain-Policies\n\n## What it does\n\n`X-Permitted-Cross-Domain-Policies` is a **response header** that sets a _meta-policy_ controlling whether site resources can be accessed cross-origin by documents running in legacy web clients (for example, Adobe Acrobat or Microsoft Silverlight).\n\nUsage is less common today because Flash/Silverlight have been deprecated, but many security testing tools still check for `X-Permitted-Cross-Domain-Policies: none` to reduce the risk of an overly-permissive cross-domain policy being present accidentally or maliciously.\n\n> This documentation format mirrors the style used in the existing header docs (e.g., Cache-Control).\n\n## Minimal example\n\n```python\nfrom secure import Secure, XPermittedCrossDomainPolicies\n\nsecure_headers = Secure(\n    xpcdp=XPermittedCrossDomainPolicies().none()\n)\n```\n\n## Resulting header\n\n```http\nX-Permitted-Cross-Domain-Policies: none\n```\n\n## Practical note\n\nMost modern apps do not need this header, but security scanners still look for it. Add it when you want an explicit deny policy for legacy cross-domain policy files.\n\n## Default behavior\n\nIf you create `XPermittedCrossDomainPolicies()` and do not set a policy, it returns the library default value:\n\n- **Default header value:** `none`\n\nThis is the least permissive option and is the most common secure setting when you do not need legacy cross-domain policy behavior.\n\n## Using with `Secure`\n\nIf you don’t configure anything, the default value is emitted.\n`Preset.BASIC` includes `X-Permitted-Cross-Domain-Policies: none`; `Preset.BALANCED` and `Preset.STRICT` leave it out unless you add it explicitly.\n\n## Common recipes\n\n### 1) Disallow cross-domain policy files (recommended default)\n\n```python\nfrom secure import XPermittedCrossDomainPolicies\n\nxpcdp = XPermittedCrossDomainPolicies()  # default: none\nprint(xpcdp.header_name)   # X-Permitted-Cross-Domain-Policies\nprint(xpcdp.header_value)  # none\n```\n\nMDN notes this is the typical configuration when you don’t need legacy clients.\n\n### 2) Allow only a master policy file\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().master_only()\nprint(xpcdp.header_value)  # master-only\n```\n\nThis allows cross-domain access to the master policy file defined on the same domain.\n\n### 3) Constrain policy files by content type (HTTP/HTTPS only)\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().by_content_type()\nprint(xpcdp.header_value)  # by-content-type\n```\n\nOnly policy files served with `Content-Type: text/x-cross-domain-policy` are allowed.\n\n### 4) Indicate this response should not be treated as a policy file\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().none_this_response()\nprint(xpcdp.header_value)  # none-this-response\n```\n\nThis directive is unique to the HTTP header and indicates the current document should not be used as a policy file.\n\n## Builder API\n\n### Policy directives (single value)\n\n- `.none()`\n- `.master_only()`\n- `.by_content_type()` (HTTP/HTTPS only)\n- `.by_ftp_filename()` (FTP only)\n- `.all()`\n- `.none_this_response()` (HTTP-header-only)\n\nThese map directly to the directive definitions described by MDN (and largely echoed by OWASP).\n\n### Typed helper\n\n- `.policy(\"none\" | \"master-only\" | \"by-content-type\" | \"by-ftp-filename\" | \"all\" | \"none-this-response\")`\n\nRaises `ValueError` for unsupported values (helps catch typos early).\n\n## Escape hatches\n\n### `.value(\"...\")`\n\nSet an explicit header value (replaces any configured directive):\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().value(\"none\")\nprint(xpcdp.header_value)  # none\n```\n\n### `.custom(\"token\")`\n\nAlias for `.value(...)` (use when you intentionally want a raw string value):\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().custom(\"master-only\")\nprint(xpcdp.header_value)  # master-only\n```\n\n### `.clear()`\n\nReset to the default:\n\n```python\nxpcdp = XPermittedCrossDomainPolicies().all().clear()\nprint(xpcdp.header_value)  # none\n```\n\n## Deterministic output & safety\n\n- The header value is rendered as a **single directive token**, so output is inherently deterministic.\n- Raw setters (`.value(...)` / `.custom(...)`) normalize obvious header-splitting primitives (CR/LF) before serialization; stricter validation can be enforced via `Secure.validate_and_normalize_headers(...)`.\n\n## Attribution\n\nThis library implements security recommendations and behavior described by:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Permitted-Cross-Domain-Policies) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/headers/x_content_type_options.md",
    "content": "# X-Content-Type-Options\n\n## What it does\n\nThe `X-Content-Type-Options` header tells browsers to **respect the MIME type declared in `Content-Type`** instead of trying to guess (\"sniff\") a different type.\n\nIn practice, setting `X-Content-Type-Options: nosniff` can cause browsers to **block**:\n\n- `style` requests not served as `text/css`\n- `script` requests not served with a JavaScript MIME type\n\nThis helps reduce the risk of content being interpreted as executable when it should not be.\n\n## Minimal example\n\n```python\nfrom secure import Secure, XContentTypeOptions\n\nsecure_headers = Secure(\n    xcto=XContentTypeOptions().nosniff(),\n)\n```\n\n## Resulting header\n\n```http\nX-Content-Type-Options: nosniff\n```\n\n## Practical note\n\n`nosniff` can expose incorrect MIME types in your app or asset pipeline. If enabling it breaks assets, fix the response `Content-Type` rather than weakening the header.\n\n## Best Practices\n\n- **Set to `nosniff`** (recommended): This is the standard and widely supported directive.\n- **Use correct `Content-Type` values**: `nosniff` is most effective when your server sends accurate MIME types.\n\n## Configuration with `Secure`\n\nThe `XContentTypeOptions` class configures `X-Content-Type-Options`.\n\n**Default header value:** `nosniff`\nAll built-in presets include it.\n\n### Methods available\n\n- **`nosniff()`**: Sets the header to `nosniff`, which blocks certain `script`/`style` requests when MIME types are incorrect.\n- **`set(value)` / `value(value)`**: Sets a raw/custom header value (escape hatch). `value` is an alias for `set`.\n- **`clear()`**: Resets the header to the library default (`nosniff`).\n\n> Note: `set/value` are escape hatches. If you use `Secure.validate_and_normalize_headers(...)`, that layer is responsible for sanitization and safety checks.\n\n## Example usage\n\n```python\nfrom secure import XContentTypeOptions\n\nxcto = XContentTypeOptions().nosniff()\nprint(xcto.header_name)   # 'X-Content-Type-Options'\nprint(xcto.header_value)  # 'nosniff'\n```\n\nApply via `Secure`:\n\n```python\nfrom secure import Secure, XContentTypeOptions\n\nsecure_headers = Secure(xcto=XContentTypeOptions().nosniff())\n```\n\n## Resources\n\n- MDN Web Docs: X-Content-Type-Options\n  [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options)\n- OWASP Secure Headers Project: X-Content-Type-Options\n  [https://owasp.org/www-project-secure-headers/#x-content-type-options](https://owasp.org/www-project-secure-headers/#x-content-type-options)\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- MDN Web Docs (CC-BY-SA 2.5)\n  [https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor](https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor)\n  [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/)\n- OWASP Secure Headers Project (CC-BY-SA 4.0)\n  [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)\n"
  },
  {
    "path": "docs/headers/x_frame_options.md",
    "content": "# X-Frame-Options\n\n## What it does\n\n`X-Frame-Options` is an HTTP **response header** that tells supporting browsers whether a page is allowed to render inside a `<frame>`, `<iframe>`, `<embed>`, or `<object>`. It is commonly used to reduce **clickjacking** risk by preventing (or restricting) framing.\n\n> Prefer `Content-Security-Policy: frame-ancestors ...` for modern, more flexible control. `X-Frame-Options` is kept for compatibility with older clients and simpler deployments.\n\n**Default header value:** `SAMEORIGIN`\n\n## Minimal example\n\n```python\nfrom secure import Secure, XFrameOptions\n\nsecure_headers = Secure(xfo=XFrameOptions().sameorigin())\n```\n\n## Resulting header\n\n```http\nX-Frame-Options: SAMEORIGIN\n```\n\n## Practical note\n\nPrefer CSP `frame-ancestors` for modern framing control. Keep `X-Frame-Options` as a compatibility layer, especially when you need support for older clients.\n\n## Important notes\n\n- **CSP is the modern replacement:** For comprehensive framing control, use CSP `frame-ancestors` (recommended).\n- **`<meta http-equiv=\"X-Frame-Options\" ...>` does nothing:** Browsers enforce `X-Frame-Options` only when it is sent as an HTTP response header.\n\n## Directives\n\n### `DENY`\n\nThe page **cannot** be displayed in a frame, regardless of what site is attempting to frame it (including the same site).\n\n### `SAMEORIGIN`\n\nThe page can be displayed only if **all ancestor frames** have the **same origin** as the page itself.\n\n### `ALLOW-FROM <origin>` (obsolete)\n\nThis directive is **obsolete**. Modern browsers that encounter `ALLOW-FROM` may **ignore the header completely**. Use CSP `frame-ancestors` instead.\n\n## Using this library\n\n### Minimal usage\n\nThe example above applies the default `SAMEORIGIN` behavior through `Secure`.\n\n### Choose a directive\n\n```python\nfrom secure import XFrameOptions\n\nxfo = XFrameOptions().deny()\nprint(xfo.header_name)   # 'X-Frame-Options'\nprint(xfo.header_value)  # 'DENY'\n```\n\n### Escape hatches\n\nIf you already have a fully-formed value, set it directly:\n\n```python\nfrom secure import XFrameOptions\n\nxfo = XFrameOptions().value(\"SAMEORIGIN\")\n# Aliases (for compatibility / readability):\nxfo = XFrameOptions().set(\"SAMEORIGIN\")\nxfo = XFrameOptions().custom(\"SAMEORIGIN\")\n```\n\nReset to the library default:\n\n```python\nxfo = XFrameOptions().deny().clear()\nprint(xfo.header_value)  # 'SAMEORIGIN'\n```\n\n### Obsolete directive (not recommended)\n\n```python\nfrom secure import XFrameOptions\n\n# Warning: obsolete; prefer CSP frame-ancestors\nxfo = XFrameOptions().allow_from(\"https://example.com\")\n```\n\n## How it fits with presets\n\nThe built-in presets include `X-Frame-Options` by default:\n\n- `Preset.BASIC` / `Preset.BALANCED`: `SAMEORIGIN`\n- `Preset.STRICT`: `DENY`\n\nIf you want full modern control, keep CSP `frame-ancestors` and treat `X-Frame-Options` as a compatibility layer.\n\n## Resources\n\n- [MDN: X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options)\n- [MDN: Clickjacking](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking)\n- [OWASP Secure Headers Project: X-Frame-Options](https://owasp.org/www-project-secure-headers/#x-frame-options)\n\n## Attribution\n\nThis library implements security recommendations from trusted sources:\n\n- MDN Web Docs (CC-BY-SA 2.5): [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/)\n- OWASP Secure Headers Project (CC-BY-SA 4.0): [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\n`secure` requires Python 3.10 or newer and has no external dependencies.\n\nInstall it with `uv` or `pip`:\n\n```bash\nuv add secure\n```\n\n```bash\npip install secure\n```\n\nIf you are following a framework example, install that framework separately.\n\n```bash\npip install fastapi\n```\n\nAfter installation, import the public API from the package root:\n\n```python\nfrom secure import Secure\n```\n\nMost public builders are also re-exported from the package root, so you can usually keep imports in the form `from secure import Secure, Preset, ContentSecurityPolicy`.\n"
  },
  {
    "path": "docs/migration.md",
    "content": "# v2 Migration Notes\n\nThe first stable v2 release is `2.0.1`. Skip `2.0.0`.\n\nIf your application already uses `Secure` to set headers on responses, the upgrade should be straightforward. Most changes are about preset names, package-level imports, and clearer sync versus async integration.\n\n## What stayed the same\n\n- `Secure` is still the main entry point.\n- Header builders such as `ContentSecurityPolicy` and `StrictTransportSecurity` are still the way to define custom policies.\n- `set_headers(response)` is still the sync path for supported response objects.\n\n## What changed\n\n- Import from the package root: `from secure import Secure, Preset, ContentSecurityPolicy`.\n- `Secure.with_default_headers()` now means `Secure.from_preset(Preset.BALANCED)`.\n- Presets are now `Preset.BALANCED`, `Preset.BASIC`, and `Preset.STRICT`.\n- `Preset.BALANCED` is the recommended default. `Preset.BASIC` is the compatibility-oriented option. `Preset.STRICT` is not the default.\n- `set_headers_async(response)` is available for async integrations and async response setters.\n- `secure.middleware` exposes `SecureWSGIMiddleware` and `SecureASGIMiddleware` for app-wide integration.\n\n## What might break\n\n- `Preset.MODERN` is gone. Replace it with `Preset.BALANCED` for the new default or `Preset.STRICT` if you specifically want a tighter profile.\n- The default profile is now `BALANCED`, which intentionally omits `Cache-Control` and the legacy compatibility headers from `BASIC`.\n- `Preset.STRICT` no longer enables HSTS preload by default. Add `.preload()` yourself if you rely on that behavior.\n- `set_headers()` is sync-only. If your response object only supports async setters, switch to `await set_headers_async(response)`.\n- If you set the `Server` header, disable framework or server defaults such as Uvicorn's `Server: uvicorn` to avoid duplicates.\n\n## Minimal upgrade path\n\nIf you previously relied on the default helpers, this is usually enough:\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\nsecure_headers.set_headers(response)\n```\n\nIf you want the new preset API explicitly:\n\n```python\nfrom secure import Preset, Secure\n\nsecure_headers = Secure.from_preset(Preset.BALANCED)\n```\n\nIf your old code expected stricter defaults, review `Preset.STRICT` before switching. The main thing to check is CSP behavior, caching, framing, and HSTS preload.\n"
  },
  {
    "path": "docs/security_considerations.md",
    "content": "# Security Considerations\n\n## Overview\n\nSecurity headers are one part of a web application's security posture. They help browsers enforce transport, embedding, content loading, and privacy rules, but they do not replace application-layer controls such as output encoding, CSRF protection, authentication, or input validation.\n\nThis guide keeps the advice tied to what `secure` actually emits and where the tradeoffs are operational rather than theoretical.\n\n## Importance of Security Headers\n\n### **Strict-Transport-Security (HSTS)**\n\nThe `Strict-Transport-Security` header ensures that browsers only connect to your site over HTTPS, preventing MITM attacks by forcing a secure connection. It tells the browser to remember to always access the site via HTTPS, even if the user tries to access it over HTTP.\n\n- [MDN Docs - Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)\n- **Best Practice**: Use a long `max-age` and include subdomains only when every subdomain is HTTPS-ready.\n- **Pitfall**: Be cautious when setting the `preload` directive, as it’s difficult to remove once added to the HSTS preload list.\n\n---\n\n### **Content-Security-Policy (CSP)**\n\nThe `Content-Security-Policy` header limits which sources the browser will trust for scripts, styles, images, frames, and other resource types. A well-tuned CSP reduces the impact of XSS and unsafe third-party content, but the policy still has to match how your frontend actually loads code and assets.\n\n- [MDN Docs - Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)\n- **Best Practice**: Start with a restrictive baseline and expand only where the application requires it. Use nonces or hashes for inline scripts when possible.\n- **Pitfall**: Overly permissive CSP rules such as `unsafe-inline`, `unsafe-eval`, or broad allowlists can leave your application vulnerable to XSS attacks.\n- **Library note**: `Preset.BALANCED` allows `'unsafe-inline'` in `style-src` for compatibility. It does not allow inline scripts by default. If you add `'unsafe-inline'` to `script-src`, treat it as an app-specific compatibility change and test the real app before rollout.\n\n---\n\n### **X-Frame-Options**\n\nThe `X-Frame-Options` header prevents clickjacking by controlling whether a page can be framed. In modern deployments, treat it as a compatibility header alongside CSP `frame-ancestors`.\n\n- [MDN Docs - X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options)\n- **Best Practice**: Set to `DENY` to completely block framing, or `SAMEORIGIN` if you only want to allow framing from your own domain.\n- **Pitfall**: Be careful when setting `SAMEORIGIN` if you allow content embedding. Incorrect settings can break legitimate functionality, such as embedded dashboards or widgets.\n\n---\n\n### **X-Content-Type-Options**\n\nThe `X-Content-Type-Options` header prevents MIME-sniffing by telling browsers to respect the declared `Content-Type`. In practice, it is most relevant for blocking incorrectly typed script and stylesheet responses.\n\n- [MDN Docs - X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options)\n- **Best Practice**: Always set this header to `nosniff`.\n- **Pitfall**: This header can surface incorrect `Content-Type` handling in your app or asset pipeline.\n\n---\n\n### **Referrer-Policy**\n\nThe `Referrer-Policy` header controls how much referrer information is included with requests. By limiting referrer data, you can prevent sensitive URL data from being exposed to third-party sites.\n\n- [MDN Docs - Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)\n- **Best Practice**: Use `strict-origin-when-cross-origin` to protect sensitive referrer information while preserving analytics functionality.\n- **Pitfall**: Using `unsafe-url` can expose full URLs, which may leak sensitive data.\n\n---\n\n### **Permissions-Policy**\n\nThe `Permissions-Policy` header allows you to disable or scope browser features such as geolocation, camera access, and microphone access.\n\n- [MDN Docs - Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy)\n- **Best Practice**: Disable unnecessary features (e.g., `camera`, `microphone`, `geolocation`) to reduce attack surface.\n- **Pitfall**: Incorrectly blocking required features may break functionality like video conferencing or map-based services.\n\n---\n\n### **Cross-Origin-Embedder-Policy (COEP)**\n\nThe `Cross-Origin-Embedder-Policy` header controls whether a document can load cross-origin resources that do not explicitly opt in via CORP or CORS.\n\n- [MDN Docs - Cross-Origin-Embedder-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy)\n- **Best Practice**: Use COEP when you need cross-origin isolation and can verify that your own and third-party resources are compatible.\n- **Pitfall**: Misconfiguration often breaks legitimate cross-origin assets before it improves anything.\n\n---\n\n### **Cross-Origin-Opener-Policy (COOP)**\n\nThe `Cross-Origin-Opener-Policy` header isolates a document's browsing context group, which helps reduce XS-Leaks and is typically paired with COEP when you need cross-origin isolation.\n\n- [MDN Docs - Cross-Origin-Opener-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy)\n- **Best Practice**: Set this to `same-origin` to protect against XS-Leaks and ensure that only same-origin documents can access the browsing context.\n- **Pitfall**: Popups, payment flows, or OAuth-style integrations may need a less strict value than `same-origin`.\n\n---\n\n### **Cache-Control**\n\nThe `Cache-Control` header controls how responses are cached. For security-sensitive responses, it helps prevent browsers and intermediaries from storing content that should not persist.\n\n- [MDN Docs - Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)\n- **Best Practice**: Use `no-store` for sensitive pages like login or payment forms to ensure that they are not cached.\n- **Pitfall**: Improper caching of sensitive data can lead to exposure of private information.\n\n---\n\n### **Server**\n\nThe `Server` header can disclose software details, but changing or clearing it should be treated as passive information reduction, not as a primary defense.\n\n- [MDN Docs - Server](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server)\n- **Best Practice**: Set a generic or empty value when your stack allows it, and disable framework or proxy defaults that would re-add a value upstream.\n- **Pitfall**: Do not assume hiding `Server` materially hardens a vulnerable application.\n\n---\n\n### **Custom Headers**\n\n`CustomHeader` is an escape hatch for application-specific response headers. Use it when you need to emit a header that does not have a dedicated builder, but keep the semantics and deployment expectations documented elsewhere in your application.\n\n---\n\n## Common Pitfalls\n\n- **Improper CSP configurations**: Using `unsafe-inline`, `unsafe-eval`, or broad source allowlists weakens CSP quickly.\n- **Weak HSTS rollout discipline**: Sending HSTS before all routes and subdomains are HTTPS-ready can break access just as easily as it improves transport security.\n\n## OWASP Guidelines\n\nFor further recommendations on security headers, refer to the [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/).\n\n---\n\n## **Attribution**\n\nThis library implements security recommendations from trusted sources:\n\n- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/))\n- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Usage\n\n`Secure` is the public entry point. Configure it once, then reuse it wherever your framework gives you access to the response.\n\n## Start with a preset\n\n`Secure.with_default_headers()` is the recommended starting point. It matches `Preset.BALANCED`.\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n```\n\nYou can also choose a preset explicitly:\n\n```python\nfrom secure import Preset, Secure\n\nbalanced = Secure.from_preset(Preset.BALANCED)\nbasic = Secure.from_preset(Preset.BASIC)\nstrict = Secure.from_preset(Preset.STRICT)\n```\n\n- `Preset.BALANCED`: recommended default for most applications.\n- `Preset.BASIC`: Helmet-style compatibility.\n- `Preset.STRICT`: tighter CSP, cross-origin isolation headers, disabled caching, and stricter framing rules.\n\n## Apply headers to a response\n\nUse `set_headers()` for synchronous response objects:\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\ndef add_security_headers(response):\n    secure_headers.set_headers(response)\n    return response\n```\n\nUse `set_headers_async()` in async code or when the response object may expose async setters:\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\n\nasync def add_security_headers(response):\n    await secure_headers.set_headers_async(response)\n    return response\n```\n\n`Secure` works with response objects that provide either:\n\n- `response.set_header(name, value)`\n- `response.headers[name] = value`\n\nIf your framework uses a different contract, emit headers manually with `header_items()`.\n\n## App-wide middleware\n\nIf you want application-wide coverage, use the middleware classes from `secure.middleware`.\n\n- Both `SecureASGIMiddleware` and `SecureWSGIMiddleware` overwrite configured header names by default.\n- Use `multi_ok` when you intentionally want controlled duplication instead of overwrite.\n- `SecureASGIMiddleware` only modifies HTTP responses. WebSocket and other non-HTTP scopes pass through unchanged.\n\n```python\nfrom secure import Secure\nfrom secure.middleware import SecureASGIMiddleware\n\nsecure_headers = Secure.with_default_headers()\nsecured_app = SecureASGIMiddleware(app, secure=secure_headers)\n```\n\nIf you need multiple `Content-Security-Policy` headers, keep those names in `multi_ok` and emit the policy values you intend to preserve.\n\n## Build an explicit configuration\n\nPresets are the shortest path. When you need more control, pass builder objects into `Secure`.\n\n```python\nfrom secure import (\n    ContentSecurityPolicy,\n    PermissionsPolicy,\n    Secure,\n    StrictTransportSecurity,\n)\n\nsecure_headers = Secure(\n    csp=(\n        ContentSecurityPolicy()\n        .default_src(\"'self'\")\n        .img_src(\"'self'\", \"https://images.example.com\")\n        .script_src(\"'self'\", \"https://cdn.example.com\")\n    ),\n    hsts=StrictTransportSecurity().max_age(63072000).include_subdomains(),\n    permissions=PermissionsPolicy().geolocation().microphone().camera(),\n)\n```\n\nThis keeps the configuration readable while avoiding hand-built header strings.\nStricter CSP changes should always be tested against the real application before rollout.\n\n## Optional validation pipeline\n\nMost applications do not need this. It is useful when headers are being merged, extended, or generated dynamically and you want validation before emission.\n\n```python\nfrom secure import Secure\n\nsecure_headers = (\n    Secure.with_default_headers()\n    .allowlist_headers()\n    .deduplicate_headers()\n    .validate_and_normalize_headers()\n)\n```\n\nAfter `validate_and_normalize_headers()`, the normalized mapping is available via `secure_headers.headers`.\n\n## Manual emission\n\nUse `header_items()` when a framework does not expose a supported response interface or when you need ordered header pairs.\n\n```python\nfrom secure import Secure\n\nsecure_headers = Secure.with_default_headers()\n\nfor name, value in secure_headers.header_items():\n    response.headers[name] = value\n```\n\nFor framework-specific examples, see [Framework Integration](./frameworks.md).\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=77\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"secure\"\nversion = \"2.0.1\"\ndescription = \"A lightweight package that adds security headers for Python web frameworks.\"\nreadme = { file = \"README.md\", content-type = \"text/markdown\" }\nlicense = \"MIT\"\nlicense-files = [\"LICENSE\"]\nauthors = [{ name = \"Caleb Kinney\", email = \"caleb@typeerror.com\" }]\nrequires-python = \">=3.10\"\nkeywords = [\"security\", \"headers\", \"web\", \"framework\", \"HTTP\"]\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Intended Audience :: Developers\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n  \"Typing :: Typed\",\n  \"Topic :: Software Development :: Libraries\",\n]\ndependencies = []\n\n[project.urls]\nHomepage = \"https://github.com/TypeError/secure\"\nDocumentation = \"https://github.com/TypeError/secure/tree/main/docs\"\nRepository = \"https://github.com/TypeError/secure\"\n\"Issue Tracker\" = \"https://github.com/TypeError/secure/issues\"\n\n# --- Setuptools package discovery ---\n[tool.setuptools.packages.find]\ninclude = [\"secure*\"]\nexclude = [\"tests*\", \"docs*\"]\n\n[tool.setuptools.package-data]\nsecure = [\"py.typed\"]\n\n[tool.pytest.ini_options]\npythonpath = [\".\"]\ntestpaths = [\"tests\"]\n\n# --- Ruff (formatter + linter) ---\n[tool.ruff]\ntarget-version = \"py310\"\nline-length = 120\nsrc = [\"secure\"]\nexclude = [\"build\", \"dist\", \".venv\", \"**/__pycache__\"]\nfix = true\n\n[tool.ruff.format]\nquote-style = \"preserve\"\nindent-style = \"space\"\ndocstring-code-format = true\n\n[tool.ruff.lint]\nselect = [\n  # Core\n  \"E\",\"F\",\n  # Import sorting (isort)\n  \"I\",\n  # Upgrades & quality\n  \"UP\",\"B\",\"C4\",\"SIM\",\n  # Typing / type-checking hygiene\n  \"ANN\",\"TCH\",\n  # Misc rule families used below\n  \"DTZ\",\"PTH\",\"T20\",\"ERA\",\"ISC\",\"TRY\",\"S\",\"PERF\",\"N\",\"Q\",\"PL\",\"RUF\"\n]\nignore = [\"TRY003\", \"EM101\"]\n\n\n[tool.ruff.lint.per-file-ignores]\n\"tests/**\" = [\"S101\",\"S311\",\"T20\",\"PLR2004\", \"ANN001\", \"ANN202\", \"ANN401\", \"ANN201\"]\n\"examples/**\" = [\"T20\"]\n\n[tool.ruff.lint.flake8-annotations]\nmypy-init-return = true\nsuppress-dummy-args = true\nsuppress-none-returning = false\nallow-star-arg-any = false\nignore-fully-untyped = false\n\n[tool.ruff.lint.flake8-type-checking]\nexempt-modules = [\"typing\", \"typing_extensions\"]\nruntime-evaluated-base-classes = [\"pydantic.BaseModel\"]\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"secure\"]\ncombine-as-imports = true\nforce-sort-within-sections = true\n\n[tool.ruff.lint.pydocstyle]\nconvention = \"google\"\n"
  },
  {
    "path": "secure/__init__.py",
    "content": "from secure.secure import (\n    COMMA_JOIN_OK,\n    DEFAULT_ALLOWED_HEADERS,\n    MULTI_OK,\n    Preset,\n    Secure,\n)\n\nfrom .headers.cache_control import CacheControl\nfrom .headers.content_security_policy import ContentSecurityPolicy\nfrom .headers.cross_origin_embedder_policy import CrossOriginEmbedderPolicy\nfrom .headers.cross_origin_opener_policy import CrossOriginOpenerPolicy\nfrom .headers.cross_origin_resource_policy import CrossOriginResourcePolicy\nfrom .headers.custom_header import CustomHeader\nfrom .headers.permissions_policy import PermissionsPolicy\nfrom .headers.referrer_policy import ReferrerPolicy\nfrom .headers.server import Server\nfrom .headers.strict_transport_security import StrictTransportSecurity\nfrom .headers.x_content_type_options import XContentTypeOptions\nfrom .headers.x_dns_prefetch_control import XDnsPrefetchControl\nfrom .headers.x_frame_options import XFrameOptions\nfrom .headers.x_permitted_cross_domain_policies import XPermittedCrossDomainPolicies\n\n__all__ = [\n    \"COMMA_JOIN_OK\",\n    \"DEFAULT_ALLOWED_HEADERS\",\n    \"MULTI_OK\",\n    \"CacheControl\",\n    \"ContentSecurityPolicy\",\n    \"CrossOriginEmbedderPolicy\",\n    \"CrossOriginOpenerPolicy\",\n    \"CrossOriginResourcePolicy\",\n    \"CustomHeader\",\n    \"PermissionsPolicy\",\n    \"Preset\",\n    \"ReferrerPolicy\",\n    \"Secure\",\n    \"Server\",\n    \"StrictTransportSecurity\",\n    \"XContentTypeOptions\",\n    \"XDnsPrefetchControl\",\n    \"XFrameOptions\",\n    \"XPermittedCrossDomainPolicies\",\n]\n"
  },
  {
    "path": "secure/_internal/__init__.py",
    "content": "\"\"\"Private implementation details for the secure package.\"\"\"\n"
  },
  {
    "path": "secure/_internal/configured_headers.py",
    "content": "from __future__ import annotations\n\nfrom types import MappingProxyType\nfrom typing import TYPE_CHECKING, Any, TypeAlias, overload\n\nfrom ..headers.base_header import BaseHeader\nfrom ..headers.custom_header import CustomHeader\nfrom .types import HeaderItems, HeaderPair\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable\n\nHeaderInput: TypeAlias = BaseHeader | HeaderPair | list[str]\nHEADER_PAIR_SIZE = 2\n\n\nclass ConfiguredHeaders(list[BaseHeader]):\n    \"\"\"Mutable header builder collection with centralized mutation bookkeeping.\"\"\"\n\n    def __init__(\n        self,\n        headers: Iterable[HeaderInput] = (),\n        *,\n        on_change: Callable[[], None],\n    ) -> None:\n        self._on_change = on_change\n        super().__init__(_coerce_header_object(header, operation=\"ConfiguredHeaders\") for header in headers)\n\n    def replace_all(self, headers: Iterable[HeaderInput]) -> None:\n        replacement = [_coerce_header_object(header, operation=\"headers_list\") for header in headers]\n        super().clear()\n        super().extend(replacement)\n        self._on_change()\n\n    def append(self, header: HeaderInput) -> None:\n        super().append(_coerce_header_object(header, operation=\"headers_list.append\"))\n        self._on_change()\n\n    def extend(self, headers: Iterable[HeaderInput]) -> None:\n        super().extend(_coerce_header_object(header, operation=\"headers_list.extend\") for header in headers)\n        self._on_change()\n\n    def insert(self, index: int, header: HeaderInput) -> None:\n        super().insert(index, _coerce_header_object(header, operation=\"headers_list.insert\"))\n        self._on_change()\n\n    @overload\n    def __setitem__(self, index: int, header: HeaderInput) -> None: ...\n\n    @overload\n    def __setitem__(self, index: slice, header: Iterable[HeaderInput]) -> None: ...\n\n    def __setitem__(self, index: int | slice, header: HeaderInput | Iterable[HeaderInput]) -> None:\n        if isinstance(index, slice):\n            super().__setitem__(\n                index,\n                [_coerce_header_object(item, operation=\"headers_list assignment\") for item in header],\n            )\n        else:\n            super().__setitem__(index, _coerce_header_object(header, operation=\"headers_list assignment\"))\n        self._on_change()\n\n    def __delitem__(self, index: int | slice) -> None:\n        super().__delitem__(index)\n        self._on_change()\n\n    def clear(self) -> None:\n        super().clear()\n        self._on_change()\n\n    def pop(self, index: int = -1) -> BaseHeader:\n        header = super().pop(index)\n        self._on_change()\n        return header\n\n    def remove(self, header: BaseHeader) -> None:\n        super().remove(header)\n        self._on_change()\n\n    def reverse(self) -> None:\n        super().reverse()\n        self._on_change()\n\n    def sort(self, *, key: Callable[[BaseHeader], Any] | None = None, reverse: bool = False) -> None:\n        super().sort(key=key, reverse=reverse)\n        self._on_change()\n\n    def __iadd__(self, headers: Iterable[HeaderInput]) -> ConfiguredHeaders:\n        self.extend(headers)\n        return self\n\n\ndef header_items_from_objects(headers: Iterable[BaseHeader]) -> HeaderItems:\n    return tuple((header.header_name, header.header_value) for header in headers)\n\n\ndef header_mapping_from_items(items: HeaderItems) -> MappingProxyType[str, str]:\n    headers: dict[str, str] = {}\n    seen_names: set[str] = set()\n\n    for name, value in items:\n        lowered_name = name.lower()\n        if lowered_name in seen_names:\n            raise ValueError(f\"Multiple '{name}' headers present; use `header_items()` when emitting multiples.\")\n        seen_names.add(lowered_name)\n        headers[name] = value\n\n    return MappingProxyType(headers)\n\n\ndef _coerce_header_object(header: HeaderInput, *, operation: str) -> BaseHeader:\n    if isinstance(header, BaseHeader):\n        return header\n\n    if (isinstance(header, tuple) and len(header) == HEADER_PAIR_SIZE) or (\n        isinstance(header, list) and len(header) == HEADER_PAIR_SIZE\n    ):\n        name, value = header\n        if isinstance(name, str) and isinstance(value, str):\n            return CustomHeader(header=name, value=value)\n\n    raise TypeError(f\"{operation} expects BaseHeader objects or (name, value) pairs\")\n"
  },
  {
    "path": "secure/_internal/constants.py",
    "content": "from __future__ import annotations\n\nimport re\n\n# Headers that may appear multiple times as separate fields.\nMULTI_OK: frozenset[str] = frozenset(\n    {\n        \"content-security-policy\",\n    }\n)\n\n# Headers where RFC7230-style comma merging is safe/expected.\nCOMMA_JOIN_OK: frozenset[str] = frozenset({\"cache-control\"})\n\n# A default allowlist of secure headers.\nDEFAULT_ALLOWED_HEADERS: frozenset[str] = frozenset(\n    {\n        \"cache-control\",\n        \"content-security-policy\",\n        \"content-security-policy-report-only\",\n        \"cross-origin-embedder-policy\",\n        \"cross-origin-opener-policy\",\n        \"cross-origin-resource-policy\",\n        \"origin-agent-cluster\",\n        \"permissions-policy\",\n        \"referrer-policy\",\n        \"server\",\n        \"strict-transport-security\",\n        \"x-content-type-options\",\n        \"x-dns-prefetch-control\",\n        \"x-download-options\",\n        \"x-frame-options\",\n        \"x-permitted-cross-domain-policies\",\n        \"x-xss-protection\",\n    }\n)\n\n# RFC 7230 token (visible ASCII except separators).\nHEADER_NAME_RE = re.compile(r\"^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$\")\n"
  },
  {
    "path": "secure/_internal/emit.py",
    "content": "from __future__ import annotations\n\nimport inspect\nfrom typing import TYPE_CHECKING, cast\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, MutableMapping\n\n    from .types import HeaderItems, ResponseProtocol\n\n\nclass HeaderSetError(RuntimeError):\n    \"\"\"Raised when applying a header to a response fails.\"\"\"\n\n\ndef set_headers_sync(response: ResponseProtocol, items: HeaderItems) -> None:\n    \"\"\"\n    Apply header items to a sync response object.\n    \"\"\"\n    if hasattr(response, \"set_header\"):\n        _apply_sync_setter(\n            response.set_header,\n            items,\n            coroutine_function_error=(\n                \"Async 'set_header' detected in sync context. Use 'await set_headers_async(response)'.\"\n            ),\n            awaitable_error=(\n                \"Async 'set_header' returned awaitable in sync context. Use 'await set_headers_async(response)'.\"\n            ),\n        )\n        return\n\n    _set_headers_container_sync(_get_headers_container(response), items)\n\n\nasync def set_headers_async(response: ResponseProtocol, items: HeaderItems) -> None:\n    \"\"\"\n    Apply header items to a sync or async response object from async code.\n    \"\"\"\n    if hasattr(response, \"set_header\"):\n        await _apply_async_setter(response.set_header, items)\n        return\n\n    await _set_headers_container_async(_get_headers_container(response), items)\n\n\ndef _get_headers_container(response: ResponseProtocol) -> object:\n    if hasattr(response, \"headers\"):\n        return cast(\"object\", response.headers)\n    raise AttributeError(\"Response object does not support setting headers.\")\n\n\ndef _set_headers_container_sync(headers: object, items: HeaderItems) -> None:\n    set_fn = getattr(headers, \"set\", None)\n    if callable(set_fn):\n        _apply_sync_setter(\n            set_fn,\n            items,\n            coroutine_function_error=(\n                \"Async headers setter detected in sync context. Use 'await set_headers_async(response)'.\"\n            ),\n            awaitable_error=(\n                \"Async headers setter returned awaitable in sync context. Use 'await set_headers_async(response)'.\"\n            ),\n        )\n        return\n\n    _set_headers_mapping_sync(headers, items)\n\n\nasync def _set_headers_container_async(headers: object, items: HeaderItems) -> None:\n    set_fn = getattr(headers, \"set\", None)\n    if callable(set_fn):\n        await _apply_async_setter(set_fn, items)\n        return\n\n    await _set_headers_mapping_async(headers, items)\n\n\ndef _apply_sync_setter(\n    setter: object,\n    items: HeaderItems,\n    *,\n    coroutine_function_error: str,\n    awaitable_error: str,\n) -> None:\n    if inspect.iscoroutinefunction(setter):\n        raise RuntimeError(coroutine_function_error)\n\n    typed_setter = cast(\"Callable[[str, str], object]\", setter)\n\n    try:\n        for name, value in items:\n            result = typed_setter(name, value)\n            if inspect.isawaitable(result):\n                raise RuntimeError(awaitable_error)\n    except (TypeError, ValueError, AttributeError) as error:\n        raise HeaderSetError(f\"Failed to set headers: {error}\") from error\n\n\nasync def _apply_async_setter(setter: object, items: HeaderItems) -> None:\n    typed_setter = cast(\"Callable[[str, str], object]\", setter)\n\n    try:\n        for name, value in items:\n            result = typed_setter(name, value)\n            if inspect.isawaitable(result):\n                await result\n    except (TypeError, ValueError, AttributeError) as error:\n        raise HeaderSetError(f\"Failed to set headers: {error}\") from error\n\n\ndef _set_headers_mapping_sync(headers: object, items: HeaderItems) -> None:\n    setitem = getattr(headers, \"__setitem__\", None)\n    if not callable(setitem):\n        raise AttributeError(  # noqa: TRY004\n            \"Response object has .headers but it does not support setting header values.\"\n        )\n\n    if inspect.iscoroutinefunction(setitem):\n        raise RuntimeError(\"Async headers mapping detected in sync context. Use 'await set_headers_async(response)'.\")\n\n    try:\n        headers_map = cast(\"MutableMapping[str, str]\", headers)\n        for name, value in items:\n            headers_map[name] = value\n    except (TypeError, ValueError, AttributeError) as error:\n        raise HeaderSetError(f\"Failed to set headers: {error}\") from error\n\n\nasync def _set_headers_mapping_async(headers: object, items: HeaderItems) -> None:\n    setitem = getattr(headers, \"__setitem__\", None)\n    if not callable(setitem):\n        raise AttributeError(  # noqa: TRY004\n            \"Response object has .headers but it does not support setting header values.\"\n        )\n\n    await _apply_async_setter(setitem, items)\n"
  },
  {
    "path": "secure/_internal/normalize.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nimport logging\nfrom types import MappingProxyType\nfrom typing import TYPE_CHECKING\n\nfrom .constants import HEADER_NAME_RE\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Mapping\n\n    from .types import OnInvalidPolicy\n\nSPACE_CODEPOINT = 0x20\nVISIBLE_CHAR_MIN = 0x21\nVISIBLE_CHAR_MAX = 0x7E\nOBS_TEXT_MIN = 0x80\nOBS_TEXT_MAX = 0xFF\n\n\n@dataclass(frozen=True)\nclass _NormalizationOptions:\n    on_invalid: OnInvalidPolicy\n    strict: bool\n    allow_obs_text: bool\n    logger: logging.Logger\n\n\ndef normalize_header_items(\n    items: Iterable[tuple[str, str]],\n    *,\n    on_invalid: OnInvalidPolicy = \"drop\",\n    strict: bool = False,\n    allow_obs_text: bool = False,\n    logger: logging.Logger | None = None,\n) -> Mapping[str, str]:\n    \"\"\"\n    Validate and normalize header items into a single-valued immutable mapping.\n    \"\"\"\n    options = _NormalizationOptions(\n        on_invalid=on_invalid,\n        strict=strict,\n        allow_obs_text=allow_obs_text,\n        logger=logger or logging.getLogger(__name__),\n    )\n    cleaned: dict[str, str] = {}\n    seen_lc: set[str] = set()\n\n    for name, value in items:\n        pair = _validate_header_pair(\n            name,\n            value,\n            options=options,\n        )\n        if pair is None:\n            continue\n\n        normalized_name, normalized_value = pair\n        lowered_name = normalized_name.lower()\n\n        if lowered_name in seen_lc:\n            raise ValueError(\n                f\"Duplicate header {normalized_name!r} encountered during normalization. \"\n                \"Run deduplicate_headers() first or use header_items() for multi-valued headers.\"\n            )\n\n        seen_lc.add(lowered_name)\n        cleaned[normalized_name] = normalized_value\n\n    return MappingProxyType(cleaned)\n\n\ndef _validate_header_pair(\n    name: str,\n    value: str,\n    *,\n    options: _NormalizationOptions,\n) -> tuple[str, str] | None:\n    normalized_name = name.strip()\n\n    if not HEADER_NAME_RE.match(normalized_name):\n        _handle_invalid(\n            f\"Invalid header name {normalized_name!r} (RFC 7230 token required)\",\n            options=options,\n        )\n        return None\n\n    if value.startswith((\" \", \"\\t\")):\n        _handle_invalid(\n            f\"Header {normalized_name!r} starts with forbidden whitespace\",\n            options=options,\n        )\n        return None\n\n    normalized_value = value\n    if (\"\\r\" in normalized_value) or (\"\\n\" in normalized_value):\n        if options.strict:\n            raise ValueError(f\"Header {normalized_name!r} contained CR/LF\")\n        normalized_value = \" \".join(normalized_value.splitlines())\n\n    normalized_value = normalized_value.strip()\n    if not normalized_value:\n        _handle_invalid(\n            f\"Dropping header {normalized_name!r}: empty value\",\n            options=options,\n        )\n        return None\n\n    if _value_is_allowed(normalized_value, allow_obs_text=options.allow_obs_text):\n        return normalized_name, normalized_value\n\n    sanitized_value = _sanitize_value(\n        normalized_name,\n        normalized_value,\n        strict=options.strict,\n        allow_obs_text=options.allow_obs_text,\n    )\n    if not sanitized_value:\n        _handle_invalid(\n            f\"Dropping header {normalized_name!r}: empty after sanitization\",\n            options=options,\n        )\n        return None\n\n    return normalized_name, sanitized_value\n\n\ndef _handle_invalid(message: str, *, options: _NormalizationOptions) -> None:\n    if options.on_invalid == \"warn\":\n        options.logger.warning(message)\n    elif options.on_invalid == \"raise\":\n        raise ValueError(message)\n\n\ndef _value_is_allowed(value: str, *, allow_obs_text: bool) -> bool:\n    return all(_is_allowed_value_char(char, allow_obs_text=allow_obs_text) for char in value)\n\n\ndef _is_allowed_value_char(char: str, *, allow_obs_text: bool) -> bool:\n    codepoint = ord(char)\n    return (\n        char == \"\\t\"\n        or codepoint == SPACE_CODEPOINT\n        or VISIBLE_CHAR_MIN <= codepoint <= VISIBLE_CHAR_MAX\n        or (allow_obs_text and OBS_TEXT_MIN <= codepoint <= OBS_TEXT_MAX)\n    )\n\n\ndef _sanitize_value(name: str, value: str, *, strict: bool, allow_obs_text: bool) -> str:\n    sanitized_characters: list[str] = []\n\n    for char in value:\n        codepoint = ord(char)\n        if _is_allowed_value_char(char, allow_obs_text=allow_obs_text):\n            sanitized_characters.append(char)\n            continue\n\n        if strict:\n            raise ValueError(f\"Header {name!r} contains disallowed char U+{codepoint:04X}\")\n\n        sanitized_characters.append(\" \")\n\n    return \"\".join(sanitized_characters).strip()\n"
  },
  {
    "path": "secure/_internal/policy.py",
    "content": "from __future__ import annotations\n\nfrom collections import defaultdict\nimport logging\nfrom typing import TYPE_CHECKING\n\nfrom ..headers.base_header import BaseHeader\nfrom ..headers.custom_header import CustomHeader\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\n    from .types import DeduplicateAction, OnUnexpectedPolicy\n\n\ndef deduplicate_header_objects(\n    headers: Iterable[BaseHeader],\n    *,\n    action: DeduplicateAction = \"raise\",\n    comma_join_ok: frozenset[str],\n    multi_ok: frozenset[str],\n    logger: logging.Logger | None = None,\n) -> list[BaseHeader]:\n    \"\"\"\n    Deduplicate header objects while preserving stable header ordering.\n    \"\"\"\n    log = logger or logging.getLogger(__name__)\n    groups: dict[str, list[tuple[int, BaseHeader]]] = defaultdict(list)\n\n    for index, header in enumerate(headers):\n        validated_header = _require_header_object(header, operation=\"deduplicate_headers\")\n        groups[validated_header.header_name.lower()].append((index, validated_header))\n\n    ordered_keys = sorted(groups.keys(), key=lambda key: groups[key][0][0])\n    deduplicated_headers: list[BaseHeader] = []\n    duplicate_errors: list[str] = []\n\n    for lowered_name in ordered_keys:\n        entries = groups[lowered_name]\n\n        if len(entries) == 1:\n            _, header = entries[0]\n            deduplicated_headers.append(_clone_as_custom_header(header.header_name, header.header_value))\n            continue\n\n        if lowered_name in multi_ok:\n            for _, header in entries:\n                deduplicated_headers.append(_clone_as_custom_header(header.header_name, header.header_value))\n            continue\n\n        resolved_headers, error_name = _resolve_duplicate_headers(\n            lowered_name,\n            entries,\n            action=action,\n            comma_join_ok=comma_join_ok,\n            logger=log,\n        )\n        deduplicated_headers.extend(resolved_headers)\n\n        if error_name is not None:\n            duplicate_errors.append(error_name)\n\n    if duplicate_errors:\n        names = \", \".join(sorted(set(duplicate_errors)))\n        raise ValueError(f\"Duplicate header(s) not allowed: {names}. Define each at most once.\")\n\n    return deduplicated_headers\n\n\ndef allowlist_header_objects(  # noqa: PLR0913\n    headers: Iterable[BaseHeader],\n    *,\n    allowed: Iterable[str],\n    allow_extra: Iterable[str] | None = None,\n    on_unexpected: OnUnexpectedPolicy = \"raise\",\n    allow_x_prefixed: bool = False,\n    logger: logging.Logger | None = None,\n) -> list[BaseHeader]:\n    \"\"\"\n    Filter header objects against a case-insensitive allowlist.\n    \"\"\"\n    log = logger or logging.getLogger(__name__)\n    allowed_lc = {header.lower() for header in allowed}\n    if allow_extra:\n        allowed_lc.update(header.lower() for header in allow_extra)\n\n    kept: list[BaseHeader] = []\n    unexpected_names: list[str] = []\n\n    for header in headers:\n        validated_header = _require_header_object(header, operation=\"allowlist_headers\")\n        header_name = validated_header.header_name\n        lowered_name = header_name.lower()\n\n        if _is_allowed_header_name(\n            lowered_name,\n            allowed=allowed_lc,\n            allow_x_prefixed=allow_x_prefixed,\n        ):\n            kept.append(validated_header)\n            continue\n\n        if on_unexpected == \"warn\":\n            log.warning(\"Unexpected header %r kept (not in allowlist)\", header_name)\n            kept.append(validated_header)\n        elif on_unexpected == \"drop\":\n            log.warning(\"Unexpected header %r dropped (not in allowlist)\", header_name)\n        else:\n            unexpected_names.append(header_name)\n\n    if unexpected_names:\n        names = \", \".join(sorted(set(unexpected_names)))\n        raise ValueError(\n            f\"Unexpected header(s) not in allowlist: {names}. Enable allow_extra or set on_unexpected to 'drop'/'warn'.\"\n        )\n\n    return kept\n\n\ndef _require_header_object(header: object, *, operation: str) -> BaseHeader:\n    if not isinstance(header, BaseHeader):\n        raise TypeError(f\"{operation}() requires BaseHeader objects only\")\n    return header\n\n\ndef _clone_as_custom_header(name: str, value: str) -> BaseHeader:\n    return CustomHeader(header=name, value=value)\n\n\ndef _resolve_duplicate_headers(\n    lowered_name: str,\n    entries: list[tuple[int, BaseHeader]],\n    *,\n    action: DeduplicateAction,\n    comma_join_ok: frozenset[str],\n    logger: logging.Logger,\n) -> tuple[list[BaseHeader], str | None]:\n    if action == \"first\":\n        _, header = entries[0]\n        if len(entries) > 1:\n            logger.warning(\"Dropping duplicate header(s) for %r (keeping first)\", header.header_name)\n        return [_clone_as_custom_header(header.header_name, header.header_value)], None\n\n    if action == \"last\":\n        _, header = entries[-1]\n        if len(entries) > 1:\n            logger.warning(\"Dropping duplicate header(s) for %r (keeping last)\", header.header_name)\n        return [_clone_as_custom_header(header.header_name, header.header_value)], None\n\n    if action == \"concat\":\n        if lowered_name in comma_join_ok:\n            name = entries[0][1].header_name\n            joined_value = \", \".join(header.header_value for _, header in entries)\n            return [_clone_as_custom_header(name, joined_value)], None\n\n        return [], entries[0][1].header_name\n\n    return [], entries[0][1].header_name\n\n\ndef _is_allowed_header_name(\n    lowered_name: str,\n    *,\n    allowed: set[str],\n    allow_x_prefixed: bool,\n) -> bool:\n    return lowered_name in allowed or (allow_x_prefixed and lowered_name.startswith(\"x-\"))\n"
  },
  {
    "path": "secure/_internal/presets.py",
    "content": "from __future__ import annotations\n\nfrom enum import Enum\n\nfrom ..headers import (\n    CacheControl,\n    ContentSecurityPolicy,\n    CrossOriginEmbedderPolicy,\n    CrossOriginOpenerPolicy,\n    CrossOriginResourcePolicy,\n    CustomHeader,\n    PermissionsPolicy,\n    ReferrerPolicy,\n    Server,\n    StrictTransportSecurity,\n    XContentTypeOptions,\n    XDnsPrefetchControl,\n    XFrameOptions,\n    XPermittedCrossDomainPolicies,\n)\n\n\nclass Preset(Enum):\n    \"\"\"Predefined security header presets for :class:`Secure`.\"\"\"\n\n    BASIC = \"basic\"\n    BALANCED = \"balanced\"\n    STRICT = \"strict\"\n\n\ndef _baseline_content_security_policy() -> ContentSecurityPolicy:\n    \"\"\"Shared CSP builder used by the BASIC and BALANCED presets.\"\"\"\n    return (\n        ContentSecurityPolicy()\n        .default_src(\"'self'\")\n        .base_uri(\"'self'\")\n        .font_src(\"'self'\", \"https:\", \"data:\")\n        .form_action(\"'self'\")\n        .frame_ancestors(\"'self'\")\n        .img_src(\"'self'\", \"data:\")\n        .object_src(\"'none'\")\n        .script_src(\"'self'\")\n        .script_src_attr(\"'none'\")\n        .style_src(\"'self'\", \"https:\", \"'unsafe-inline'\")\n        .upgrade_insecure_requests()\n    )\n\n\ndef preset_kwargs(preset: Preset) -> dict[str, object]:\n    \"\"\"Return constructor kwargs for a predefined :class:`Secure` preset.\"\"\"\n    match preset:\n        case Preset.BASIC:\n            return {\n                \"coop\": CrossOriginOpenerPolicy().same_origin(),\n                \"csp\": _baseline_content_security_policy(),\n                \"corp\": CrossOriginResourcePolicy().same_origin(),\n                \"hsts\": StrictTransportSecurity().max_age(31536000).include_subdomains(),\n                \"referrer\": ReferrerPolicy().no_referrer(),\n                \"xcto\": XContentTypeOptions().nosniff(),\n                \"xfo\": XFrameOptions().sameorigin(),\n                \"xdfc\": XDnsPrefetchControl().disable(),\n                \"xpcdp\": XPermittedCrossDomainPolicies().none(),\n                \"custom\": [\n                    CustomHeader(\n                        header=\"Origin-Agent-Cluster\",\n                        value=\"?1\",\n                    ),\n                    CustomHeader(\n                        header=\"X-Download-Options\",\n                        value=\"noopen\",\n                    ),\n                    CustomHeader(\n                        header=\"X-XSS-Protection\",\n                        value=\"0\",\n                    ),\n                ],\n            }\n        case Preset.BALANCED:\n            return {\n                \"coop\": CrossOriginOpenerPolicy().same_origin(),\n                \"corp\": CrossOriginResourcePolicy().same_origin(),\n                \"csp\": _baseline_content_security_policy(),\n                \"hsts\": StrictTransportSecurity().max_age(31536000).include_subdomains(),\n                \"permissions\": PermissionsPolicy().geolocation().microphone().camera(),\n                \"referrer\": ReferrerPolicy().strict_origin_when_cross_origin(),\n                \"server\": Server().set(\"\"),\n                \"xcto\": XContentTypeOptions().nosniff(),\n                \"xfo\": XFrameOptions().sameorigin(),\n            }\n        case Preset.STRICT:\n            return {\n                \"cache\": CacheControl().no_store().max_age(0),\n                \"coep\": CrossOriginEmbedderPolicy().require_corp(),\n                \"coop\": CrossOriginOpenerPolicy().same_origin(),\n                \"csp\": (\n                    ContentSecurityPolicy()\n                    .default_src(\"'self'\")\n                    .script_src(\"'self'\")\n                    .style_src(\"'self'\")\n                    .object_src(\"'none'\")\n                    .base_uri(\"'none'\")\n                    .frame_ancestors(\"'none'\")\n                ),\n                \"hsts\": StrictTransportSecurity().max_age(63072000).include_subdomains(),\n                \"permissions\": PermissionsPolicy().geolocation().microphone().camera(),\n                \"referrer\": ReferrerPolicy().no_referrer(),\n                \"server\": Server().set(\"\"),\n                \"xcto\": XContentTypeOptions().nosniff(),\n                \"xfo\": XFrameOptions().deny(),\n            }\n        case _:\n            raise ValueError(f\"Unknown preset: {preset}\")\n"
  },
  {
    "path": "secure/_internal/types.py",
    "content": "from __future__ import annotations\n\nfrom typing import Literal, Protocol, TypeAlias\n\nOnInvalidPolicy = Literal[\"drop\", \"warn\", \"raise\"]\nDeduplicateAction = Literal[\"raise\", \"first\", \"last\", \"concat\"]\nOnUnexpectedPolicy = Literal[\"raise\", \"drop\", \"warn\"]\n\n\nclass HeadersProtocol(Protocol):\n    @property\n    def headers(self) -> object: ...\n\n\nclass SetHeaderProtocol(Protocol):\n    def set_header(self, key: str, value: str) -> object | None: ...\n\n\nResponseProtocol: TypeAlias = HeadersProtocol | SetHeaderProtocol\nHeaderPair: TypeAlias = tuple[str, str]\nHeaderItems: TypeAlias = tuple[HeaderPair, ...]\n"
  },
  {
    "path": "secure/headers/__init__.py",
    "content": "from .base_header import BaseHeader\nfrom .cache_control import CacheControl\nfrom .content_security_policy import ContentSecurityPolicy\nfrom .cross_origin_embedder_policy import CrossOriginEmbedderPolicy\nfrom .cross_origin_opener_policy import CrossOriginOpenerPolicy\nfrom .cross_origin_resource_policy import CrossOriginResourcePolicy\nfrom .custom_header import CustomHeader\nfrom .permissions_policy import PermissionsPolicy\nfrom .referrer_policy import ReferrerPolicy\nfrom .server import Server\nfrom .strict_transport_security import StrictTransportSecurity\nfrom .x_content_type_options import XContentTypeOptions\nfrom .x_dns_prefetch_control import XDnsPrefetchControl\nfrom .x_frame_options import XFrameOptions\nfrom .x_permitted_cross_domain_policies import XPermittedCrossDomainPolicies\n\n__all__ = [\n    \"BaseHeader\",\n    \"CacheControl\",\n    \"ContentSecurityPolicy\",\n    \"CrossOriginEmbedderPolicy\",\n    \"CrossOriginOpenerPolicy\",\n    \"CrossOriginResourcePolicy\",\n    \"CustomHeader\",\n    \"PermissionsPolicy\",\n    \"ReferrerPolicy\",\n    \"Server\",\n    \"StrictTransportSecurity\",\n    \"XContentTypeOptions\",\n    \"XDnsPrefetchControl\",\n    \"XFrameOptions\",\n    \"XPermittedCrossDomainPolicies\",\n]\n"
  },
  {
    "path": "secure/headers/_validation.py",
    "content": "from __future__ import annotations\n\n\ndef normalize_header_value(value: str, *, what: str = \"header value\") -> str:\n    \"\"\"\n    Trim whitespace and reject CR/LF characters in header strings.\n\n    Args:\n        value: Raw header string.\n        what: Description of the value (used for error messages).\n\n    Returns:\n        The stripped string without CR/LF characters.\n\n    Raises:\n        ValueError: If CR or LF are present.\n    \"\"\"\n    if \"\\r\" in value or \"\\n\" in value:\n        raise ValueError(f\"{what} must not contain CR/LF characters\")\n    return value.strip()\n"
  },
  {
    "path": "secure/headers/base_header.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n# https://owasp.org/www-project-secure-headers/#cache-control\n\nfrom abc import abstractmethod\nfrom dataclasses import dataclass\nfrom enum import Enum\n\n\nclass HeaderName(Enum):\n    \"\"\"Enumeration of standard HTTP security headers.\n\n    This enum provides the header names for various security headers\n    like Cache-Control, Content-Security-Policy, Strict-Transport-Security, etc.\n    It is used to ensure consistency in header naming across the codebase.\n    \"\"\"\n\n    # Caching\n    CACHE_CONTROL = \"Cache-Control\"\n\n    # Content policy\n    CONTENT_SECURITY_POLICY = \"Content-Security-Policy\"\n\n    # Content policy (report-only)\n    CONTENT_SECURITY_POLICY_REPORT_ONLY = \"Content-Security-Policy-Report-Only\"\n\n    # Embedding security\n    CROSS_ORIGIN_EMBEDDER_POLICY = \"Cross-Origin-Embedder-Policy\"\n\n    # Context isolation\n    CROSS_ORIGIN_OPENER_POLICY = \"Cross-Origin-Opener-Policy\"\n\n    # Cross-origin resource sharing\n    CROSS_ORIGIN_RESOURCE_POLICY = \"Cross-Origin-Resource-Policy\"\n\n    # Permissions\n    PERMISSION_POLICY = \"Permissions-Policy\"\n\n    # Referrer control\n    REFERRER_POLICY = \"Referrer-Policy\"\n\n    # Server identification\n    SERVER = \"Server\"\n\n    # HTTPS enforcement\n    STRICT_TRANSPORT_SECURITY = \"Strict-Transport-Security\"\n\n    # MIME type protection\n    X_CONTENT_TYPE_OPTIONS = \"X-Content-Type-Options\"\n\n    # DNS prefetching control\n    X_DNS_PREFETCH_CONTROL = \"X-DNS-Prefetch-Control\"\n\n    # Clickjacking protection\n    X_FRAME_OPTIONS = \"X-Frame-Options\"\n\n    # Cross-domain policies\n    X_PERMITTED_CROSS_DOMAIN_POLICIES = \"X-Permitted-Cross-Domain-Policies\"\n\n\nclass HeaderDefaultValue(Enum):\n    \"\"\"Enumeration of default values for standard HTTP security headers.\n\n    This enum provides default values for headers like Cache-Control, Content-Security-Policy,\n    Strict-Transport-Security, and others. These values represent recommended security defaults\n    where applicable.\n    \"\"\"\n\n    # Cache-Control to prevent caching of sensitive data\n    CACHE_CONTROL = \"no-store, max-age=0\"\n\n    # Basic Content Security Policy to allow resources only from the same origin\n    CONTENT_SECURITY_POLICY = (\n        \"default-src 'self'; \"\n        \"script-src 'self'; \"\n        \"style-src 'self'; \"\n        \"object-src 'none'; \"\n        \"base-uri 'self'; \"\n        \"frame-ancestors 'self'; \"\n        \"form-action 'self'\"\n    )\n\n    # Cross-Origin Embedder Policy set to 'require-corp' to enforce stricter security.\n    # This ensures that embedded cross-origin resources must explicitly allow being embedded.\n    # Note: This may break third-party content that does not allow cross-origin embedding.\n    CROSS_ORIGIN_EMBEDDER_POLICY = \"require-corp\"\n\n    # Cross-Origin Opener Policy to isolate browsing contexts and prevent cross-origin leaks\n    CROSS_ORIGIN_OPENER_POLICY = \"same-origin\"\n\n    # Cross-Origin Resource Policy to restrict resource loading to the same origin\n    CROSS_ORIGIN_RESOURCE_POLICY = \"same-origin\"\n\n    # Permissions Policy to disable risky features by default (geolocation, microphone, camera)\n    PERMISSION_POLICY = \"geolocation=(), microphone=(), camera=()\"\n\n    # Referrer Policy to balance security and usability, limits information sent on cross-origin requests\n    REFERRER_POLICY = \"strict-origin-when-cross-origin\"\n\n    # Server header omitted to hide server details from attackers\n    SERVER = \"\"\n\n    # Strict Transport Security to enforce HTTPS for one year\n    STRICT_TRANSPORT_SECURITY = \"max-age=31536000\"\n\n    # Prevent MIME-type sniffing to block potential security threats from improperly typed content\n    X_CONTENT_TYPE_OPTIONS = \"nosniff\"\n\n    # X-DNS-Prefetch-Control to disable DNS prefetching for privacy\n    X_DNS_PREFETCH_CONTROL = \"off\"\n\n    # Clickjacking protection, allows framing only from the same origin\n    X_FRAME_OPTIONS = \"SAMEORIGIN\"\n\n    # X-Permitted-Cross-Domain-Policies to disallow all cross-domain policies\n    X_PERMITTED_CROSS_DOMAIN_POLICIES = \"none\"\n\n\n@dataclass\nclass BaseHeader:\n    \"\"\"Abstract base class for HTTP security headers.\n\n    This class defines the basic structure for security headers by requiring\n    derived classes to implement the `header_value` property, which provides\n    the value of the header.\n\n    Attributes:\n        header_name (str): The name of the security header.\n    \"\"\"\n\n    header_name: str\n\n    @property\n    @abstractmethod\n    def header_value(self) -> str:\n        \"\"\"Abstract property for getting the header value.\n\n        This property should be implemented by subclasses to return the\n        security header's value.\n\n        Returns:\n            str: The value of the header.\n        \"\"\"\n        ...\n"
  },
  {
    "path": "secure/headers/cache_control.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control\n# https://owasp.org/www-project-secure-headers/#cache-control\n#\n# Cache-Control by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nimport re\nfrom typing import ClassVar\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n_TOKEN_RE = re.compile(r\"^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$\")\n\n\n@dataclass\nclass CacheControl(BaseHeader):\n    \"\"\"\n    Fluent builder for the `Cache-Control` HTTP header.\n\n    Default header value: `no-store, max-age=0`\n\n    Notes:\n        * Directive names are case-insensitive; lowercase is the recommended form.\n        * Directives are comma-separated and resilient to repeated calls for the\n          same helper.\n        * Common directives follow a deterministic, canonical order to keep\n          serialized output stable regardless of call order.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control\n        - https://owasp.org/www-project-secure-headers/#cache-control\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.CACHE_CONTROL.value, repr=False)\n\n    # Directive storage:\n    # - Keys are lowercase directive names (e.g., \"max-age\", \"no-store\")\n    # - Values are None (valueless directive) or a string (e.g., \"60\")\n    _directives: dict[str, str | None] = field(default_factory=dict)\n\n    # Extra/unrecognized directives (escape hatch). Stored as fully-rendered tokens.\n    _extras: list[str] = field(default_factory=list)\n\n    # Exact override for the entire header value (escape hatch).\n    _raw_value: str | None = None\n\n    # Library default value for when no directives are set.\n    _default_value: str = HeaderDefaultValue.CACHE_CONTROL.value\n\n    # Deterministic serialization order (independent of call order).\n    _CANONICAL_ORDER: ClassVar[tuple[str, ...]] = (\n        # Common \"secure\" baseline and typical patterns first.\n        \"no-store\",\n        \"no-cache\",\n        \"private\",\n        \"public\",\n        # Age-based caching controls.\n        \"max-age\",\n        \"s-maxage\",\n        \"max-stale\",\n        \"min-fresh\",\n        # Validation / capability controls.\n        \"must-revalidate\",\n        \"proxy-revalidate\",\n        \"must-understand\",\n        # Transformation / immutability / stale extensions.\n        \"no-transform\",\n        \"immutable\",\n        \"stale-while-revalidate\",\n        \"stale-if-error\",\n        # Request-only.\n        \"only-if-cached\",\n    )\n\n    # -------------------------------------------------------------------------\n    # Serialization\n    # -------------------------------------------------------------------------\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `Cache-Control` header value, or the default if unset.\"\"\"\n        if self._raw_value is not None:\n            return self._raw_value\n\n        if not self._directives and not self._extras:\n            return self._default_value\n\n        parts: list[str] = []\n        present_names: set[str] = set()\n\n        def _add(name: str, val: str | None) -> None:\n            token = name if val is None else f\"{name}={val}\"\n            parts.append(token)\n            present_names.add(name)\n\n        for name in self._CANONICAL_ORDER:\n            if name in self._directives:\n                _add(name, self._directives[name])\n\n        for name in sorted(k for k in self._directives if k not in self._CANONICAL_ORDER):\n            _add(name, self._directives[name])\n\n        # Append extras, skipping anything that would duplicate a directive name.\n        # Extras are sorted for determinism.\n        for token in sorted(set(self._extras)):\n            extra_name = token.split(\"=\", 1)[0].strip().lower()\n            if not extra_name or extra_name in present_names:\n                continue\n            parts.append(token)\n\n        return \", \".join(parts)\n\n    # -------------------------------------------------------------------------\n    # Escape hatches / resets\n    # -------------------------------------------------------------------------\n\n    def value(self, value: str) -> CacheControl:\n        \"\"\"\n        Set an explicit header value, replacing all configured directives.\n\n        This is an escape hatch: it bypasses directive helpers.\n\n        Safety:\n        - Rejects CR/LF to prevent header-splitting.\n        - Strips leading/trailing whitespace and rejects empty results.\n        \"\"\"\n        v = normalize_header_value(value, what=\"Cache-Control value\")\n        if not v:\n            raise ValueError(\"Cache-Control value must not be empty\")\n\n        self._raw_value = v\n        self._directives.clear()\n        self._extras.clear()\n        return self\n\n    # Backwards-compatible alias (older versions used `set()`).\n    def set(self, value: str) -> CacheControl:\n        \"\"\"Alias for :meth:`value`.\"\"\"\n        return self.value(value)\n\n    def clear(self) -> CacheControl:\n        \"\"\"Clear all directives and explicit value, returning to the default state.\"\"\"\n        self._raw_value = None\n        self._directives.clear()\n        self._extras.clear()\n        return self\n\n    def custom(self, directive: str) -> CacheControl:\n        \"\"\"\n        Add a custom directive token (non-standard / extra).\n\n        This is intended for directives not covered by helper methods.\n\n        Examples:\n            .custom(\"foo\")\n            .custom(\"foo=bar\")\n\n        Safety:\n        - Rejects commas (would break tokenization).\n        - Rejects CR/LF (header-splitting).\n        - Validates the directive *name* as an RFC token.\n        \"\"\"\n        self._ensure_directive_mode()\n\n        d = directive.strip()\n        if not d:\n            raise ValueError(\"custom directive must be a non-empty string\")\n        if (\",\" in d) or (\"\\r\" in d) or (\"\\n\" in d):\n            raise ValueError(\"custom directive must not contain ',', CR, or LF characters\")\n\n        if \"=\" in d:\n            name, rest = d.split(\"=\", 1)\n            name = name.strip().lower()\n            if not name or not _TOKEN_RE.match(name):\n                raise ValueError(f\"custom directive name must be a valid token (got {name!r})\")\n            token = f\"{name}={rest.strip()}\"\n        else:\n            name = d.strip().lower()\n            if not _TOKEN_RE.match(name):\n                raise ValueError(f\"custom directive name must be a valid token (got {name!r})\")\n            token = name\n\n        if token not in self._extras:\n            self._extras.append(token)\n        return self\n\n    # -------------------------------------------------------------------------\n    # Internal helpers\n    # -------------------------------------------------------------------------\n\n    def _ensure_directive_mode(self) -> None:\n        if self._raw_value is not None:\n            self._raw_value = None\n\n    @staticmethod\n    def _validate_seconds(seconds: int) -> int:\n        if isinstance(seconds, bool) or not isinstance(seconds, int):\n            raise TypeError(\"seconds must be an integer\")\n        if seconds < 0:\n            raise ValueError(\"seconds must be a non-negative integer\")\n        return seconds\n\n    def _set_bool(self, name: str) -> None:\n        self._ensure_directive_mode()\n        self._directives[name] = None\n\n    def _set_seconds(self, name: str, seconds: int) -> None:\n        self._ensure_directive_mode()\n        n = self._validate_seconds(seconds)\n        self._directives[name] = str(n)\n\n    # -------------------------------------------------------------------------\n    # Directive helpers\n    # -------------------------------------------------------------------------\n\n    def immutable(self) -> CacheControl:\n        \"\"\"Indicate the response will not be updated while it is fresh.\"\"\"\n        self._set_bool(\"immutable\")\n        return self\n\n    def max_age(self, seconds: int) -> CacheControl:\n        \"\"\"Set `max-age=N` (freshness lifetime in responses, acceptable age in requests).\"\"\"\n        self._set_seconds(\"max-age\", seconds)\n        return self\n\n    def max_stale(self, seconds: int | None = None) -> CacheControl:\n        \"\"\"Allow reusing a stale response within `seconds`, or any stale age when omitted (request).\"\"\"\n        self._ensure_directive_mode()\n        if seconds is None:\n            self._directives[\"max-stale\"] = None\n        else:\n            self._directives[\"max-stale\"] = str(self._validate_seconds(seconds))\n        return self\n\n    def min_fresh(self, seconds: int) -> CacheControl:\n        \"\"\"Require a stored response to remain fresh for at least `seconds` (request).\"\"\"\n        self._set_seconds(\"min-fresh\", seconds)\n        return self\n\n    def must_revalidate(self) -> CacheControl:\n        \"\"\"Require revalidation with the origin server once a stored response becomes stale (response).\"\"\"\n        self._set_bool(\"must-revalidate\")\n        return self\n\n    def must_understand(self) -> CacheControl:\n        \"\"\"Store the response only if the cache understands the caching requirements for its status code.\"\"\"\n        self._set_bool(\"must-understand\")\n        return self\n\n    def no_cache(self) -> CacheControl:\n        \"\"\"Allow storing but require validation with the origin server before each reuse.\"\"\"\n        self._set_bool(\"no-cache\")\n        return self\n\n    def no_store(self) -> CacheControl:\n        \"\"\"Instruct caches (private or shared) not to store this response.\"\"\"\n        self._set_bool(\"no-store\")\n        return self\n\n    def no_transform(self) -> CacheControl:\n        \"\"\"Instruct intermediaries not to transform the request or response content.\"\"\"\n        self._set_bool(\"no-transform\")\n        return self\n\n    def only_if_cached(self) -> CacheControl:\n        \"\"\"Request an already-cached response; if none is available, a 504 may be returned (request).\"\"\"\n        self._set_bool(\"only-if-cached\")\n        return self\n\n    def private(self) -> CacheControl:\n        \"\"\"Indicate the response may be stored only in a private cache (e.g., a browser cache).\"\"\"\n        self._set_bool(\"private\")\n        return self\n\n    def proxy_revalidate(self) -> CacheControl:\n        \"\"\"Like `must-revalidate`, but for shared caches only (response).\"\"\"\n        self._set_bool(\"proxy-revalidate\")\n        return self\n\n    def public(self) -> CacheControl:\n        \"\"\"Indicate the response may be stored in a shared cache (response).\"\"\"\n        self._set_bool(\"public\")\n        return self\n\n    def s_maxage(self, seconds: int) -> CacheControl:\n        \"\"\"Set `s-maxage=N` (freshness lifetime in shared caches only).\"\"\"\n        self._set_seconds(\"s-maxage\", seconds)\n        return self\n\n    def s_max_age(self, seconds: int) -> CacheControl:\n        \"\"\"Alias for :meth:`s_maxage`.\"\"\"\n        return self.s_maxage(seconds)\n\n    def stale_if_error(self, seconds: int) -> CacheControl:\n        \"\"\"Allow reusing a stale response for `seconds` when a 500/502/503/504 error is encountered.\"\"\"\n        self._set_seconds(\"stale-if-error\", seconds)\n        return self\n\n    def stale_while_revalidate(self, seconds: int) -> CacheControl:\n        \"\"\"Allow reusing a stale response for `seconds` while revalidation happens in the background.\"\"\"\n        self._set_seconds(\"stale-while-revalidate\", seconds)\n        return self\n"
  },
  {
    "path": "secure/headers/content_security_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy\n# https://owasp.org/www-project-secure-headers/#content-security-policy\n#\n# Content-Security-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\nimport re\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n_DIRECTIVE_NAME_RE = re.compile(r\"^[A-Za-z0-9-]+$\")\n_NONCE_RE = re.compile(r\"^[A-Za-z0-9+/_=-]+$\")\n_ASCII_SPACE = 0x20\n_ASCII_DEL = 0x7F\n\n\n@dataclass\nclass ContentSecurityPolicy(BaseHeader):\n    \"\"\"\n    Fluent builder for the ``Content-Security-Policy`` HTTP response header.\n\n    Default header value:\n        `default-src 'self'; script-src 'self'; style-src 'self';\n         object-src 'none'; base-uri 'self'; frame-ancestors 'self';\n         form-action 'self'`\n\n    Notes:\n        * The structured helpers intentionally avoid full CSP validation; use\n          ``.value(...)`` when you need to emit an exact policy string.\n        * Multiple policies can be sent by instantiating another\n          ``ContentSecurityPolicy`` and adding it to ``Secure.headers_list``.\n        * MDN describes fallback behavior between directives (e.g., ``default-src``\n          acts as a fallback for fetch directives).\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP\n        - https://owasp.org/www-project-secure-headers/#content-security-policy\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.CONTENT_SECURITY_POLICY.value, repr=False)\n\n    # Structured directives built via fluent helpers. Each directive appears at most once.\n    # Values are stored as tokens (space-separated in serialization). A value of ``None``\n    # means the directive is valueless (for example: ``upgrade-insecure-requests``).\n    _directives: dict[str, list[str] | None] = field(default_factory=dict, repr=False)\n\n    # Escape hatch: if set, this raw string is used as the header value.\n    _raw_value: str | None = field(default=None, repr=False)\n\n    _default_value: str = field(init=False, default=HeaderDefaultValue.CONTENT_SECURITY_POLICY.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `Content-Security-Policy` header value.\"\"\"\n        if self._raw_value is not None:\n            return self._raw_value\n\n        if not self._directives:\n            return self._default_value\n\n        parts: list[str] = []\n        for directive, values in self._directives.items():\n            if values:\n                parts.append(f\"{directive} {' '.join(values)}\")\n            else:\n                parts.append(directive)\n        return \"; \".join(parts)\n\n    # -------------------------------------------------------------------------\n    # Low-level helpers / escape hatches\n    # -------------------------------------------------------------------------\n\n    def value(self, value: str) -> ContentSecurityPolicy:\n        \"\"\"Set an exact header value (escape hatch).\"\"\"\n        self._raw_value = normalize_header_value(value, what=\"Content-Security-Policy value\")\n        self._directives.clear()\n        return self\n\n    # Backwards-compatible alias.\n    def set(self, value: str) -> ContentSecurityPolicy:\n        \"\"\"Alias for :meth:`value`.\"\"\"\n        return self.value(value)\n\n    def clear(self) -> ContentSecurityPolicy:\n        \"\"\"Clear all configured directives and any raw override.\n\n        After calling this, the header value falls back to the library default.\n        \"\"\"\n        self._raw_value = None\n        self._directives.clear()\n        return self\n\n    def report_only(self) -> ContentSecurityPolicy:\n        \"\"\"Use the report-only header name (`Content-Security-Policy-Report-Only`).\"\"\"\n        self.header_name = HeaderName.CONTENT_SECURITY_POLICY_REPORT_ONLY.value\n        return self\n\n    def enforce(self) -> ContentSecurityPolicy:\n        \"\"\"Use the enforcing header name (`Content-Security-Policy`).\"\"\"\n        self.header_name = HeaderName.CONTENT_SECURITY_POLICY.value\n        return self\n\n    def custom(self, directive: str, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Alias for :meth:`custom_directive`.\"\"\"\n        return self.custom_directive(directive, *values)\n\n    def custom_directive(self, directive: str, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Add (or update) a directive.\n\n        - Directives are de-duplicated: each directive name appears at most once.\n        - Values are treated as tokens: duplicates are removed (preserving order).\n        - Passing no values sets a valueless directive (overwriting prior values).\n\n        Args:\n            directive: Directive name (for example, ``default-src``).\n            *values: Directive tokens (for example, ``'self'``, ``https:``, ``example.com``).\n\n        Returns:\n            The same instance, for method chaining.\n        \"\"\"\n        self._touch_structured()\n        d = self._normalize_directive_name(directive)\n\n        if not values:\n            # Valueless directive (or explicit \"clear values\" for a directive).\n            self._directives[d] = None\n            return self\n\n        tokens = [self._validate_token(v) for v in values]\n\n        existing = self._directives.get(d)\n        if existing is None:\n            existing_list: list[str] = []\n            self._directives[d] = existing_list\n        else:\n            existing_list = existing\n\n        # De-dupe while preserving insertion order.\n        seen = set(existing_list)\n        for t in tokens:\n            if t not in seen:\n                existing_list.append(t)\n                seen.add(t)\n        return self\n\n    # -------------------------------------------------------------------------\n    # Directive helpers (alphabetical by directive name)\n    # -------------------------------------------------------------------------\n\n    def base_uri(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for the document `<base>` element.\"\"\"\n        return self.custom_directive(\"base-uri\", *sources)\n\n    def block_all_mixed_content(self) -> ContentSecurityPolicy:\n        \"\"\"Prevent loading any assets using HTTP when the page is loaded using HTTPS.\n\n        Deprecated in MDN's reference; prefer modern HTTPS-only deployments and\n        consider `upgrade-insecure-requests` instead when appropriate.\n        \"\"\"\n        return self.custom_directive(\"block-all-mixed-content\")\n\n    def child_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for web workers and nested browsing contexts.\"\"\"\n        return self.custom_directive(\"child-src\", *sources)\n\n    def connect_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for script interfaces (for example, XHR, Fetch, WebSocket).\"\"\"\n        return self.custom_directive(\"connect-src\", *sources)\n\n    def default_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set the fallback policy for all fetch directives.\"\"\"\n        return self.custom_directive(\"default-src\", *sources)\n\n    def fenced_frame_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for nested browsing contexts loaded into `<fencedframe>`.\"\"\"\n        return self.custom_directive(\"fenced-frame-src\", *sources)\n\n    def font_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for fonts.\"\"\"\n        return self.custom_directive(\"font-src\", *sources)\n\n    def form_action(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Restrict the URLs which can be used as the target of form submissions.\"\"\"\n        return self.custom_directive(\"form-action\", *sources)\n\n    def frame_ancestors(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid parent sources that may embed the page in a frame.\"\"\"\n        return self.custom_directive(\"frame-ancestors\", *sources)\n\n    def frame_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for nested browsing contexts loaded into frames/iframes.\"\"\"\n        return self.custom_directive(\"frame-src\", *sources)\n\n    def img_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for images and favicons.\"\"\"\n        return self.custom_directive(\"img-src\", *sources)\n\n    def manifest_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for application manifests.\"\"\"\n        return self.custom_directive(\"manifest-src\", *sources)\n\n    def media_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for media (audio, video, track).\"\"\"\n        return self.custom_directive(\"media-src\", *sources)\n\n    def object_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for plugin-like objects (for example, `<object>`, `<embed>`).\"\"\"\n        return self.custom_directive(\"object-src\", *sources)\n\n    def prefetch_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources to be prefetched or prerendered.\n\n        Deprecated and non-standard in MDN's reference; use only if you have a\n        specific compatibility need.\n        \"\"\"\n        return self.custom_directive(\"prefetch-src\", *sources)\n\n    def report_to(self, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Configure reporting endpoints via `report-to` groups.\"\"\"\n        return self.custom_directive(\"report-to\", *values)\n\n    def report_uri(self, *uris: str) -> ContentSecurityPolicy:\n        \"\"\"Configure the legacy reporting endpoint(s) via `report-uri`.\n\n        Deprecated in MDN's reference. If you use `report-to`, note that browsers\n        that support `report-to` ignore `report-uri`.\n        \"\"\"\n        return self.custom_directive(\"report-uri\", *uris)\n\n    def require_trusted_types_for(self, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Enforce Trusted Types at specific DOM injection sinks.\"\"\"\n        return self.custom_directive(\"require-trusted-types-for\", *values)\n\n    def sandbox(self, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Enable a sandbox for the requested resource (similar to `<iframe sandbox>`).\"\"\"\n        return self.custom_directive(\"sandbox\", *values)\n\n    def script_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for JavaScript and WebAssembly resources.\"\"\"\n        return self.custom_directive(\"script-src\", *sources)\n\n    def script_src_attr(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for inline event handlers.\"\"\"\n        return self.custom_directive(\"script-src-attr\", *sources)\n\n    def script_src_elem(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for `<script>` elements.\"\"\"\n        return self.custom_directive(\"script-src-elem\", *sources)\n\n    def style_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for stylesheets.\"\"\"\n        return self.custom_directive(\"style-src\", *sources)\n\n    def style_src_attr(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for inline styles on individual elements.\"\"\"\n        return self.custom_directive(\"style-src-attr\", *sources)\n\n    def style_src_elem(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for `<style>` and stylesheet `<link>` elements.\"\"\"\n        return self.custom_directive(\"style-src-elem\", *sources)\n\n    def trusted_types(self, *values: str) -> ContentSecurityPolicy:\n        \"\"\"Specify an allowlist of Trusted Types policies.\"\"\"\n        return self.custom_directive(\"trusted-types\", *values)\n\n    def upgrade_insecure_requests(self) -> ContentSecurityPolicy:\n        \"\"\"Upgrade insecure HTTP requests to HTTPS.\"\"\"\n        return self.custom_directive(\"upgrade-insecure-requests\")\n\n    def worker_src(self, *sources: str) -> ContentSecurityPolicy:\n        \"\"\"Set valid sources for `Worker`, `SharedWorker`, and `ServiceWorker` scripts.\"\"\"\n        return self.custom_directive(\"worker-src\", *sources)\n\n    # -------------------------------------------------------------------------\n    # CSP value helpers\n    # -------------------------------------------------------------------------\n\n    @staticmethod\n    def keyword(name: str) -> str:\n        \"\"\"Return a quoted CSP keyword/source expression (for example, ``'self'``).\"\"\"\n        if not name:\n            raise ValueError(\"CSP keyword must be non-empty\")\n        if any(ch.isspace() for ch in name) or any(ch in name for ch in \"'\\\";\\r\\n\"):\n            raise ValueError(\"CSP keyword contains invalid characters\")\n        return f\"'{name}'\"\n\n    @staticmethod\n    def nonce(value: str) -> str:\n        \"\"\"Create a nonce source expression for inline scripts or styles.\n\n        The provided value should be Base64 or URL-safe Base64.\n        \"\"\"\n        if not value or not _NONCE_RE.fullmatch(value):\n            raise ValueError(\"nonce value must be Base64 (or URL-safe Base64) characters only\")\n        return f\"'nonce-{value}'\"\n\n    # -------------------------------------------------------------------------\n    # Internal helpers\n    # -------------------------------------------------------------------------\n\n    def _touch_structured(self) -> None:\n        \"\"\"Switch from raw override to structured directive building (if needed).\"\"\"\n        if self._raw_value is not None:\n            self._raw_value = None\n\n    @staticmethod\n    def _normalize_directive_name(directive: str) -> str:\n        if not directive:\n            raise ValueError(\"directive name must be non-empty\")\n        if any(ch.isspace() for ch in directive) or any(ch in directive for ch in \";\\r\\n\"):\n            raise ValueError(\"directive name contains invalid characters\")\n        if not _DIRECTIVE_NAME_RE.fullmatch(directive):\n            raise ValueError(f\"invalid directive name: {directive!r}\")\n        return directive\n\n    @staticmethod\n    def _validate_token(token: str) -> str:\n        if not token:\n            raise ValueError(\"directive value tokens must be non-empty\")\n        if any(ch.isspace() for ch in token) or any(ch in token for ch in \";\\r\\n\"):\n            raise ValueError(f\"directive token contains invalid characters: {token!r}\")\n        # Disallow other ASCII control characters.\n        if any(ord(ch) < _ASCII_SPACE or ord(ch) == _ASCII_DEL for ch in token):\n            raise ValueError(f\"directive token contains control characters: {token!r}\")\n        return token\n"
  },
  {
    "path": "secure/headers/cross_origin_embedder_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy\n# https://owasp.org/www-project-secure-headers/#cross-origin-embedder-policy\n#\n# Cross-Origin-Embedder-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\nfrom typing import Literal\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\nCOEPDirective = Literal[\"unsafe-none\", \"require-corp\", \"credentialless\"]\n\n\n@dataclass\nclass CrossOriginEmbedderPolicy(BaseHeader):\n    \"\"\"\n    Builder for the ``Cross-Origin-Embedder-Policy`` (COEP) HTTP response header.\n\n    COEP controls how the document embeds and loads cross-origin resources, with\n    directives that range from no isolation (``unsafe-none``) to strict isolation\n    (``require-corp``) or credentialless loading.\n\n    Default header value: ``require-corp``\n\n    Notes:\n        * Per MDN, omitting the header is equivalent to ``unsafe-none``.\n        * Each helper closes over canonical MDN directives while ``value(...)``\n          acts as an escape hatch for custom strings.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy\n        - https://owasp.org/www-project-secure-headers/#cross-origin-embedder-policy\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.CROSS_ORIGIN_EMBEDDER_POLICY.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value, repr=False)\n    _directive: str = field(default=HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value, repr=False)\n\n    def _normalize(self, value: str) -> str:\n        \"\"\"Normalize a directive value (trim + lowercase).\"\"\"\n        v = normalize_header_value(value, what=\"Cross-Origin-Embedder-Policy value\")\n\n        if not v:\n            return HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value\n        return v.lower()\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current ``Cross-Origin-Embedder-Policy`` header value.\"\"\"\n        return self._normalize(self._directive)\n\n    def set(self, value: COEPDirective | str) -> CrossOriginEmbedderPolicy:\n        \"\"\"Set a COEP directive.\n\n        This method accepts any string as an escape hatch. For MDN-defined values,\n        prefer :meth:`unsafe_none`, :meth:`require_corp`, or :meth:`credentialless`.\n\n        Args:\n            value: Directive value (e.g., ``\"require-corp\"``).\n\n        Returns:\n            This instance for method chaining.\n        \"\"\"\n        self._directive = self._normalize(str(value))\n        return self\n\n    def value(self, value: COEPDirective | str) -> CrossOriginEmbedderPolicy:\n        \"\"\"Alias for :meth:`set` to align with other headers.\"\"\"\n        return self.set(value)\n\n    def clear(self) -> CrossOriginEmbedderPolicy:\n        \"\"\"Reset to the library default directive.\"\"\"\n        self._directive = self._default_value\n        return self\n\n    def unsafe_none(self) -> CrossOriginEmbedderPolicy:\n        \"\"\"Set COEP to ``unsafe-none``.\n\n        ``unsafe-none`` allows the document to load cross-origin resources without\n        explicit CORP/CORS permission.\n        \"\"\"\n        self._directive = \"unsafe-none\"\n        return self\n\n    def require_corp(self) -> CrossOriginEmbedderPolicy:\n        \"\"\"Set COEP to ``require-corp``.\n\n        ``require-corp`` blocks cross-origin resource loading unless the resource\n        is explicitly permitted via CORP (for ``no-cors``) or via CORS (for ``cors``).\n        \"\"\"\n        self._directive = \"require-corp\"\n        return self\n\n    def credentialless(self) -> CrossOriginEmbedderPolicy:\n        \"\"\"Set COEP to ``credentialless``.\n\n        ``credentialless`` allows loading some cross-origin resources without\n        explicit CORP opt-in, but strips credentials (cookies are omitted on the\n        request and ignored in the response).\n        \"\"\"\n        self._directive = \"credentialless\"\n        return self\n"
  },
  {
    "path": "secure/headers/cross_origin_opener_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy\n# https://owasp.org/www-project-secure-headers/#cross-origin-opener-policy\n#\n# Cross-Origin-Opener-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\nfrom typing import Final, Literal\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\nCOOPDirective = Literal[\n    \"unsafe-none\",\n    \"same-origin-allow-popups\",\n    \"same-origin\",\n    \"noopener-allow-popups\",\n]\n\n# Library default (secure-header libs often default to isolation).\n# Note: per MDN/spec behavior, if the header is absent, the effective default is \"unsafe-none\".\nDEFAULT_VALUE: Final[str] = HeaderDefaultValue.CROSS_ORIGIN_OPENER_POLICY.value\n\n\n@dataclass\nclass CrossOriginOpenerPolicy(BaseHeader):\n    \"\"\"\n    Builder for the ``Cross-Origin-Opener-Policy`` (COOP) HTTP response header.\n\n    COOP lets a page opt into a dedicated browsing context group or share like with\n    its opener, helping protect against XS-Leaks.\n\n    Default header value: ``same-origin``\n\n    Notes:\n        * If this header is absent, browsers behave as if ``unsafe-none`` were set.\n        * Use the fluent helpers to pick MDN-defined directives; ``value(...)`` is\n          provided as an escape hatch.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy\n        - https://owasp.org/www-project-secure-headers/#cross-origin-opener-policy\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.CROSS_ORIGIN_OPENER_POLICY.value, repr=False)\n    _default_value: str = field(init=False, default=DEFAULT_VALUE, repr=False)\n    _directive: str = field(default=DEFAULT_VALUE, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `Cross-Origin-Opener-Policy` header value.\"\"\"\n        return self._directive\n\n    # ---------------------------------------------------------------------\n    # Escape hatches\n    # ---------------------------------------------------------------------\n\n    def value(self, directive: str) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Set a custom value for the `Cross-Origin-Opener-Policy` header.\n\n        This is an escape hatch. Prefer the explicit directive helpers when possible.\n\n        Safety:\n            Rejects CR/LF to avoid header-splitting. Additional validation (obs-text, etc.)\n            remains the responsibility of `Secure.validate_and_normalize_headers(...)`.\n\n        Args:\n            directive: Custom header value (usually one of the COOP directives).\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        v = normalize_header_value(directive, what=\"Cross-Origin-Opener-Policy value\")\n        self._directive = v\n        return self\n\n    def custom(self, directive: str) -> CrossOriginOpenerPolicy:\n        \"\"\"Alias for :meth:`value`.\"\"\"\n        return self.value(directive)\n\n    def set(self, value: str) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Backwards-compatible alias for :meth:`value`.\n\n        Prefer :meth:`value` or :meth:`custom` in v2+ for consistency.\n        \"\"\"\n        return self.value(value)\n\n    def clear(self) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Reset the `Cross-Origin-Opener-Policy` header to the library default value.\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        self._directive = self._default_value\n        return self\n\n    # ---------------------------------------------------------------------\n    # Directive helpers (fluent API)\n    # ---------------------------------------------------------------------\n\n    def unsafe_none(self) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Set the header to `'unsafe-none'`.\n\n        This opts out of COOP-based isolation.\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        self._directive = \"unsafe-none\"\n        return self\n\n    def same_origin_allow_popups(self) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Set the header to `'same-origin-allow-popups'`.\n\n        Similar to `same-origin`, but allows opening documents with COOP `unsafe-none`\n        in the same browsing context group for `Window.open()` integrations.\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        self._directive = \"same-origin-allow-popups\"\n        return self\n\n    def same_origin(self) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Set the header to `'same-origin'`.\n\n        Restricts browsing context group sharing to same-origin documents that also\n        use `same-origin`. Commonly used as part of cross-origin isolation.\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        self._directive = \"same-origin\"\n        return self\n\n    def noopener_allow_popups(self) -> CrossOriginOpenerPolicy:\n        \"\"\"\n        Set the header to `'noopener-allow-popups'`.\n\n        This severs opener relationships while still allowing popups, and is used to\n        isolate documents even from same-origin openers in some workflows.\n\n        Returns:\n            The `CrossOriginOpenerPolicy` instance for method chaining.\n        \"\"\"\n        self._directive = \"noopener-allow-popups\"\n        return self\n"
  },
  {
    "path": "secure/headers/cross_origin_resource_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy\n# https://owasp.org/www-project-secure-headers/#cross-origin-resource-policy\n#\n# Cross-Origin-Resource-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Final, Literal\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\nCorpDirective = Literal[\"same-site\", \"same-origin\", \"cross-origin\"]\n\n_ALLOWED: Final[frozenset[str]] = frozenset({\"same-site\", \"same-origin\", \"cross-origin\"})\n\n\n@dataclass\nclass CrossOriginResourcePolicy(BaseHeader):\n    \"\"\"\n    Builder for the ``Cross-Origin-Resource-Policy`` (CORP) HTTP response header.\n\n    CORP expresses the resource owner's intent for which origins may load this\n    resource, with MDN documenting ``same-site``, ``same-origin``, and\n    ``cross-origin`` directives.\n\n    Default header value: `same-origin`\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy\n        - https://resourcepolicy.fyi/\n        - https://owasp.org/www-project-secure-headers/#cross-origin-resource-policy\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.CROSS_ORIGIN_RESOURCE_POLICY.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.CROSS_ORIGIN_RESOURCE_POLICY.value, repr=False)\n    _value: str = field(default_factory=lambda: HeaderDefaultValue.CROSS_ORIGIN_RESOURCE_POLICY.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current header value.\"\"\"\n        return self._value\n\n    def clear(self) -> CrossOriginResourcePolicy:\n        \"\"\"\n        Reset this header to the library default value.\n\n        Returns:\n            The `CrossOriginResourcePolicy` instance for method chaining.\n        \"\"\"\n        self._value = self._default_value\n        return self\n\n    def value(self, value: str | CorpDirective) -> CrossOriginResourcePolicy:\n        \"\"\"\n        Set the header value.\n\n        This is the preferred \"escape hatch\" API. For known CORP directives, the\n        stored value is canonicalized to the standard lowercase token.\n\n        Args:\n            value:\n                Typically one of `same-origin`, `same-site`, or `cross-origin`.\n                Other values are accepted as-is (after trimming), but are not\n                described by MDN.\n\n        Returns:\n            The `CrossOriginResourcePolicy` instance for method chaining.\n\n        Raises:\n            ValueError: if the value contains CR/LF characters.\n        \"\"\"\n        self._value = self._normalize_value(str(value))\n        return self\n\n    # Backwards-compatible alias (keep for existing callers).\n    def set(self, value: str) -> CrossOriginResourcePolicy:\n        \"\"\"\n        Backwards-compatible alias for `value(...)`.\n\n        Prefer `value(...)` going forward.\n        \"\"\"\n        return self.value(value)\n\n    def same_origin(self) -> CrossOriginResourcePolicy:\n        \"\"\"Restrict resource loading to the same origin.\"\"\"\n        self._value = \"same-origin\"\n        return self\n\n    def same_site(self) -> CrossOriginResourcePolicy:\n        \"\"\"Allow resource loading from the same site.\"\"\"\n        self._value = \"same-site\"\n        return self\n\n    def cross_origin(self) -> CrossOriginResourcePolicy:\n        \"\"\"Allow resource loading from any origin.\"\"\"\n        self._value = \"cross-origin\"\n        return self\n\n    @staticmethod\n    def _normalize_value(value: str) -> str:\n        v = normalize_header_value(value, what=\"Cross-Origin-Resource-Policy value\")\n\n        # Canonicalize known directives (case-insensitive) to the MDN tokens.\n        lc = v.lower()\n        if lc in _ALLOWED:\n            return lc\n\n        # Unknown: keep trimmed string verbatim as an escape hatch.\n        return v\n"
  },
  {
    "path": "secure/headers/custom_header.py",
    "content": "from __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader\n\n\n@dataclass\nclass CustomHeader(BaseHeader):\n    \"\"\"\n    Wrapper for an arbitrary HTTP header.\n\n    Default header value: provided by the caller at initialization.\n\n    Notes:\n        * Header names and values are normalized via ``normalize_header_value`` to\n          prevent header injection.\n        * This class keeps parity with other builders via ``value``, ``set``, and\n          escape-hatch helpers so it plugs into the fluent API.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers\n    \"\"\"\n\n    header_name: str\n    _value: str = field(repr=False)\n\n    def __init__(self, header: str, value: str) -> None:\n        \"\"\"\n        Initialize a custom header name and value.\n\n        Args:\n            header: The header name (for example, ``\"X-Custom-Header\"``).\n            value: The header value to emit.\n        \"\"\"\n        self.header_name = normalize_header_value(header, what=\"custom header name\")\n        self._value = value\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"\n        Retrieve the current value of the custom header.\n\n        Returns:\n            str: The value of the custom header.\n        \"\"\"\n        return self._value\n\n    def set(self, value: str) -> CustomHeader:\n        \"\"\"\n        Update the value of the custom header.\n\n        This method allows the value of the custom header to be updated\n        and supports method chaining.\n\n        Args:\n            value: The new value to set for the custom header.\n\n        Returns:\n            CustomHeader: The current instance, allowing for method chaining.\n        \"\"\"\n        self._value = normalize_header_value(value, what=\"custom header value\")\n        return self\n\n    def value(self, value: str) -> CustomHeader:\n        \"\"\"\n        Alias for :meth:`set`, provided for parity with other headers.\n        \"\"\"\n        return self.set(value)\n"
  },
  {
    "path": "secure/headers/permissions_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy\n# https://owasp.org/www-project-secure-headers/#permissions-policy\n#\n# Permissions-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\ndef _normalize_token(raw: str, tokens_len: int) -> str | None:\n    \"\"\"Normalize a single allowlist token. Returns None for wildcard, empty string to skip.\"\"\"\n    token = normalize_header_value(str(raw), what=\"allowlist token\")\n    if not token:\n        return \"\"\n\n    # Map convenience aliases to their normalized forms\n    token_mappings = {\n        \"none\": \"()\",\n        \"'none'\": \"()\",\n        '\"none\"': \"()\",\n        \"()\": \"()\",\n        \"*\": None,\n        \"'*'\": None,\n        '\"*\"': None,\n        \"self\": \"self\",\n        \"'self'\": \"self\",\n        '\"self\"': \"self\",\n        \"src\": \"src\",\n        \"'src'\": \"src\",\n        '\"src\"': \"src\",\n    }\n\n    if token in token_mappings:\n        result = token_mappings[token]\n        if result == \"()\" and tokens_len != 1:\n            raise ValueError(\"() / none cannot be combined with other allowlist tokens\")\n        return result\n\n    # Origins: MDN uses double quotes\n    if token.startswith((\"http://\", \"https://\")):\n        if (token.startswith('\"') and token.endswith('\"')) or (token.startswith(\"'\") and token.endswith(\"'\")):\n            return f'\"{token[1:-1]}\"' if token.startswith(\"'\") else token\n        return f'\"{token}\"'\n\n    return token\n\n\ndef _normalize_allowlist(tokens: tuple[str, ...]) -> str:\n    \"\"\"\n    Normalize an allowlist according to MDN's header syntax.\n\n    - Empty allowlist disables the feature: ()\n    - Wildcard allows all origins: *\n    - Otherwise: (<token> <token> ...)\n\n    Notes\n    -----\n    MDN examples show `self` and `src` as bare tokens, and origins as double-quoted\n    strings (e.g. \"https://a.example.com\").\n    \"\"\"\n    if not tokens:\n        return \"()\"\n\n    cleaned: list[str] = []\n    saw_wildcard = False\n\n    for raw in tokens:\n        normalized = _normalize_token(raw, len(tokens))\n\n        if normalized == \"\":\n            continue\n        if normalized == \"()\":\n            return \"()\"\n        if normalized is None:\n            saw_wildcard = True\n            continue\n\n        cleaned.append(normalized)\n\n    if saw_wildcard:\n        if cleaned:\n            raise ValueError(\"Wildcard (*) must be used alone in a Permissions-Policy allowlist\")\n        return \"*\"\n\n    if not cleaned:\n        return \"()\"\n\n    for t in cleaned:\n        if any(ch.isspace() for ch in t):\n            raise ValueError(\"Allowlist tokens must not contain whitespace; pass each token separately\")\n        if \",\" in t:\n            raise ValueError(\"Allowlist tokens must not contain commas\")\n\n    return f\"({' '.join(cleaned)})\"\n\n\n@dataclass\nclass PermissionsPolicy(BaseHeader):\n    \"\"\"\n    Builder for the `Permissions-Policy` HTTP header.\n\n    Default header value: `geolocation=(), microphone=(), camera=()`\n\n    Notes:\n        * Directive helpers cover MDN features; use ``value(...)`` when you already\n          have a ready-made header string.\n        * Allowlists follow MDN syntax: ``()``, ``*``, ``self``, ``src``, or\n          double-quoted origins; ``()``/``none`` cannot be mixed with other tokens,\n          and wildcard must stand alone.\n        * Call helpers repeatedly without worrying about duplicates: each directive\n          is unique and re-assigning it keeps the order of the most recent write.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy\n        - https://owasp.org/www-project-secure-headers/#permissions-policy\n        - https://www.w3.org/TR/permissions-policy-1/\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.PERMISSION_POLICY.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.PERMISSION_POLICY.value, repr=False)\n\n    # Directive -> normalized allowlist string (e.g. \"()\", \"*\", '(self \"https://a.example.com\")')\n    _directives: dict[str, str] = field(default_factory=dict, repr=False)\n\n    # If set, overrides directive-building entirely.\n    _raw_value: str | None = field(default=None, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `Permissions-Policy` header value.\"\"\"\n        if self._raw_value is not None:\n            return self._raw_value\n\n        if not self._directives:\n            return self._default_value\n\n        # Dict preserves insertion order; updating an existing directive keeps its position.\n        return \", \".join(f\"{name}={allowlist}\" for name, allowlist in self._directives.items())\n\n    # ---------------------------------------------------------------------\n    # Escape hatches / lifecycle\n    # ---------------------------------------------------------------------\n\n    def value(self, value: str) -> PermissionsPolicy:\n        \"\"\"\n        Set a raw header value (escape hatch).\n\n        This bypasses directive-building and uses `value` verbatim after trimming.\n\n        Notes\n        -----\n        `Secure.validate_and_normalize_headers()` is responsible for final safety checks\n        (e.g., CR/LF handling). This method rejects CR/LF up front.\n        \"\"\"\n        value = normalize_header_value(str(value), what=\"Permissions-Policy value\")\n        if not value:\n            raise ValueError(\"Permissions-Policy value must not be empty\")\n\n        self._raw_value = value\n        return self\n\n    def set(self, value: str) -> PermissionsPolicy:\n        \"\"\"Alias for :meth:`value` (kept for backwards compatibility).\"\"\"\n        return self.value(value)\n\n    def clear(self) -> PermissionsPolicy:\n        \"\"\"Clear all configured directives and any raw override.\"\"\"\n        self._directives.clear()\n        self._raw_value = None\n        return self\n\n    # ---------------------------------------------------------------------\n    # Directive builder\n    # ---------------------------------------------------------------------\n\n    def add_directive(self, directive: str, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"\n        Add or replace a directive.\n\n        Parameters\n        ----------\n        directive:\n            The directive name (e.g. \"geolocation\", \"camera\", \"fullscreen\").\n        *allowlist:\n            Allowlist tokens. Examples:\n            - no tokens -> () (disabled)\n            - \"*\" -> * (allowed everywhere)\n            - \"self\", \"https://a.example.com\" -> (self \"https://a.example.com\")\n\n        Returns\n        -------\n        PermissionsPolicy\n            The instance (for chaining).\n        \"\"\"\n        directive = normalize_header_value(str(directive), what=\"directive\")\n        if not directive:\n            raise ValueError(\"Directive name must not be empty\")\n        if any(ch.isspace() for ch in directive) or any(ch in directive for ch in \",;=\"):\n            raise ValueError(f\"Invalid directive name: {directive!r}\")\n\n        # Directive-building and raw value are mutually exclusive.\n        self._raw_value = None\n\n        self._directives[directive] = _normalize_allowlist(tuple(allowlist))\n        return self\n\n    def directive(self, directive: str, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Alias for :meth:`add_directive`.\"\"\"\n        return self.add_directive(directive, *allowlist)\n\n    # ---------------------------------------------------------------------\n    # Directives (MDN list evolves; `add_directive()` remains the catch-all)\n    # ---------------------------------------------------------------------\n\n    def accelerometer(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Accelerometer sensor.\"\"\"\n        return self.add_directive(\"accelerometer\", *allowlist)\n\n    def ambient_light_sensor(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Ambient Light sensor.\"\"\"\n        return self.add_directive(\"ambient-light-sensor\", *allowlist)\n\n    def aria_notify(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use ARIA notifications (aria-notify).\"\"\"\n        return self.add_directive(\"aria-notify\", *allowlist)\n\n    def attribution_reporting(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use Attribution Reporting.\"\"\"\n        return self.add_directive(\"attribution-reporting\", *allowlist)\n\n    def autoplay(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether media is allowed to autoplay.\"\"\"\n        return self.add_directive(\"autoplay\", *allowlist)\n\n    def bluetooth(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Web Bluetooth API.\"\"\"\n        return self.add_directive(\"bluetooth\", *allowlist)\n\n    def browsing_topics(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use browsing-topics.\"\"\"\n        return self.add_directive(\"browsing-topics\", *allowlist)\n\n    def compute_pressure(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Compute Pressure API.\"\"\"\n        return self.add_directive(\"compute-pressure\", *allowlist)\n\n    def cross_origin_isolated(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the document can be cross-origin isolated.\"\"\"\n        return self.add_directive(\"cross-origin-isolated\", *allowlist)\n\n    def fullscreen(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Fullscreen API.\"\"\"\n        return self.add_directive(\"fullscreen\", *allowlist)\n\n    def gamepad(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Gamepad API.\"\"\"\n        return self.add_directive(\"gamepad\", *allowlist)\n\n    def geolocation(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Geolocation API.\"\"\"\n        return self.add_directive(\"geolocation\", *allowlist)\n\n    def gyroscope(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Gyroscope sensor.\"\"\"\n        return self.add_directive(\"gyroscope\", *allowlist)\n\n    def hid(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the WebHID API.\"\"\"\n        return self.add_directive(\"hid\", *allowlist)\n\n    def identity_credentials_get(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use identity credentials (identity-credentials-get).\"\"\"\n        return self.add_directive(\"identity-credentials-get\", *allowlist)\n\n    def idle_detection(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use idle detection.\"\"\"\n        return self.add_directive(\"idle-detection\", *allowlist)\n\n    def local_fonts(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can access local fonts.\"\"\"\n        return self.add_directive(\"local-fonts\", *allowlist)\n\n    def magnetometer(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Magnetometer sensor.\"\"\"\n        return self.add_directive(\"magnetometer\", *allowlist)\n\n    def microphone(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can access the microphone.\"\"\"\n        return self.add_directive(\"microphone\", *allowlist)\n\n    def on_device_speech_recognition(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use on-device speech recognition.\"\"\"\n        return self.add_directive(\"on-device-speech-recognition\", *allowlist)\n\n    def otp_credentials(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the WebOTP API.\"\"\"\n        return self.add_directive(\"otp-credentials\", *allowlist)\n\n    def publickey_credentials_create(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can create WebAuthn credentials.\"\"\"\n        return self.add_directive(\"publickey-credentials-create\", *allowlist)\n\n    def publickey_credentials_get(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use WebAuthn credential assertion.\"\"\"\n        return self.add_directive(\"publickey-credentials-get\", *allowlist)\n\n    def serial(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Web Serial API.\"\"\"\n        return self.add_directive(\"serial\", *allowlist)\n\n    def speaker_selection(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can select audio output devices.\"\"\"\n        return self.add_directive(\"speaker-selection\", *allowlist)\n\n    def storage_access(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can request storage access.\"\"\"\n        return self.add_directive(\"storage-access\", *allowlist)\n\n    def summarizer(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use summarizer.\"\"\"\n        return self.add_directive(\"summarizer\", *allowlist)\n\n    def translator(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use translator.\"\"\"\n        return self.add_directive(\"translator\", *allowlist)\n\n    def language_detector(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use language detection.\"\"\"\n        return self.add_directive(\"language-detector\", *allowlist)\n\n    def usb(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the WebUSB API.\"\"\"\n        return self.add_directive(\"usb\", *allowlist)\n\n    def web_share(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Web Share API.\"\"\"\n        return self.add_directive(\"web-share\", *allowlist)\n\n    def window_management(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use window management.\"\"\"\n        return self.add_directive(\"window-management\", *allowlist)\n\n    def xr_spatial_tracking(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use XR spatial tracking.\"\"\"\n        return self.add_directive(\"xr-spatial-tracking\", *allowlist)\n\n    # ---------------------------------------------------------------------\n    # Non-MDN / legacy directives (kept for backwards compatibility)\n    # ---------------------------------------------------------------------\n\n    def battery(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can access battery status.\"\"\"\n        return self.add_directive(\"battery\", *allowlist)\n\n    def camera(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can access the camera.\"\"\"\n        return self.add_directive(\"camera\", *allowlist)\n\n    def clipboard_read(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can read from the clipboard.\"\"\"\n        return self.add_directive(\"clipboard-read\", *allowlist)\n\n    def clipboard_write(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can write to the clipboard.\"\"\"\n        return self.add_directive(\"clipboard-write\", *allowlist)\n\n    def display_capture(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can capture the display (screen capture).\"\"\"\n        return self.add_directive(\"display-capture\", *allowlist)\n\n    def document_domain(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can use `document.domain`.\"\"\"\n        return self.add_directive(\"document-domain\", *allowlist)\n\n    def encrypted_media(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can use encrypted media.\"\"\"\n        return self.add_directive(\"encrypted-media\", *allowlist)\n\n    def execution_while_not_rendered(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can execute when not rendered.\"\"\"\n        return self.add_directive(\"execution-while-not-rendered\", *allowlist)\n\n    def execution_while_out_of_viewport(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can execute while out of the viewport.\"\"\"\n        return self.add_directive(\"execution-while-out-of-viewport\", *allowlist)\n\n    def midi(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Web MIDI API.\"\"\"\n        return self.add_directive(\"midi\", *allowlist)\n\n    def navigation_override(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can override navigation.\"\"\"\n        return self.add_directive(\"navigation-override\", *allowlist)\n\n    def payment(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Payment Request API.\"\"\"\n        return self.add_directive(\"payment\", *allowlist)\n\n    def picture_in_picture(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use Picture-in-Picture.\"\"\"\n        return self.add_directive(\"picture-in-picture\", *allowlist)\n\n    def screen_wake_lock(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Controls whether the page can use the Screen Wake Lock API.\"\"\"\n        return self.add_directive(\"screen-wake-lock\", *allowlist)\n\n    def sync_xhr(self, *allowlist: str) -> PermissionsPolicy:\n        \"\"\"Legacy/nonstandard: controls whether the page can use synchronous XHR.\"\"\"\n        return self.add_directive(\"sync-xhr\", *allowlist)\n"
  },
  {
    "path": "secure/headers/referrer_policy.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy\n# https://owasp.org/www-project-secure-headers/#referrer-policy\n#\n# Referrer-Policy by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\ndef _split_policies(value: str) -> list[str]:\n    \"\"\"Split a header value into individual policy tokens.\"\"\"\n    cleaned = normalize_header_value(value, what=\"Referrer-Policy value\")\n    if not cleaned:\n        return []\n\n    parts: list[str] = []\n    for raw in cleaned.split(\",\"):\n        token = raw.strip()\n        if not token:\n            continue\n\n        # Tokens should not contain internal whitespace; treat this as an error to\n        # help catch accidental pastes and prevent ambiguous serialization.\n        if any(ch in token for ch in (\" \", \"\\t\")):\n            raise ValueError(f\"Invalid Referrer-Policy token {token!r}\")\n\n        parts.append(token.lower())\n    return parts\n\n\n@dataclass\nclass ReferrerPolicy(BaseHeader):\n    \"\"\"\n    Builder for the ``Referrer-Policy`` HTTP response header.\n\n    Default header value: ``strict-origin-when-cross-origin``\n\n    Notes:\n        * ``Referrer-Policy`` controls how much of the ``Referer`` header is sent.\n        * The comma-separated fallback list should place the primary policy last.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy\n        - https://owasp.org/www-project-secure-headers/#referrer-policy\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.REFERRER_POLICY.value, repr=False)\n    _policies: list[str] = field(default_factory=list, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.REFERRER_POLICY.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current ``Referrer-Policy`` header value.\"\"\"\n        return \", \".join(self._policies) if self._policies else self._default_value\n\n    def _add_token(self, token: str) -> None:\n        if token not in self._policies:\n            self._policies.append(token)\n\n    def add(self, value: str) -> ReferrerPolicy:\n        \"\"\"Add one or more policy tokens.\n\n        Parameters\n        ----------\n        value:\n            A single policy token (e.g., ``\"no-referrer\"``) or a comma-separated\n            list (e.g., ``\"no-referrer, strict-origin-when-cross-origin\"``).\n\n        Returns\n        -------\n        ReferrerPolicy\n            The same instance, for fluent chaining.\n        \"\"\"\n        for token in _split_policies(value):\n            self._add_token(token)\n        return self\n\n    # Backwards-compatible alias: historically this method appended rather than replaced.\n    def set(self, value: str) -> ReferrerPolicy:\n        \"\"\"Alias of :meth:`add` (appends one or more policy tokens).\"\"\"\n        return self.add(value)\n\n    def value(self, value: str) -> ReferrerPolicy:\n        \"\"\"Replace the current policies with ``value``.\n\n        Use this when you want a single, explicit policy. For fallback lists,\n        call :meth:`add` repeatedly or pass a comma-separated list.\n        \"\"\"\n        self.clear()\n        return self.add(value)\n\n    def custom(self, value: str) -> ReferrerPolicy:\n        \"\"\"Escape hatch: same as :meth:`value`.\"\"\"\n        return self.value(value)\n\n    def fallback(self, *policies: str) -> ReferrerPolicy:\n        \"\"\"Replace the current policies with an explicit fallback list.\n\n        The desired (most modern) policy should be the **last** item.\n        \"\"\"\n        self.clear()\n        for p in policies:\n            self.add(p)\n        return self\n\n    def clear(self) -> ReferrerPolicy:\n        \"\"\"Clear all configured policies.\"\"\"\n        self._policies.clear()\n        return self\n\n    # --- Directive helpers (MDN-defined tokens) ---------------------------------\n\n    def no_referrer(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``no-referrer`` (omit the ``Referer`` header entirely).\"\"\"\n        return self.add(\"no-referrer\")\n\n    def no_referrer_when_downgrade(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``no-referrer-when-downgrade``.\n\n        Sends origin + path + query for same-or-more secure requests (HTTP→HTTP, HTTP→HTTPS, HTTPS→HTTPS),\n        but omits ``Referer`` for less secure destinations (HTTPS→HTTP, HTTPS→file).\n        \"\"\"\n        return self.add(\"no-referrer-when-downgrade\")\n\n    def origin(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``origin`` (send only the origin, e.g. ``https://example.com/``).\"\"\"\n        return self.add(\"origin\")\n\n    def origin_when_cross_origin(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``origin-when-cross-origin``.\n\n        Same-origin: send origin + path + query. Cross-origin (and HTTPS→HTTP): send only the origin.\n        \"\"\"\n        return self.add(\"origin-when-cross-origin\")\n\n    def same_origin(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``same-origin``.\n\n        Same-origin: send origin + path + query. Cross-origin: omit the ``Referer`` header.\n        \"\"\"\n        return self.add(\"same-origin\")\n\n    def strict_origin(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``strict-origin``.\n\n        Sends only the origin for same-security requests (HTTPS→HTTPS) and omits ``Referer`` on downgrade (HTTPS→HTTP).\n        \"\"\"\n        return self.add(\"strict-origin\")\n\n    def strict_origin_when_cross_origin(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``strict-origin-when-cross-origin`` (the modern default).\n\n        Same-origin: send origin + path + query. Cross-origin: send only the origin on HTTPS→HTTPS,\n        and omit on downgrade (HTTPS→HTTP).\n        \"\"\"\n        return self.add(\"strict-origin-when-cross-origin\")\n\n    def unsafe_url(self) -> ReferrerPolicy:\n        \"\"\"Set the policy to ``unsafe-url`` (send origin + path + query for all requests, regardless of security).\n\n        Warning: this can leak sensitive URL data from HTTPS pages to insecure origins.\n        \"\"\"\n        return self.add(\"unsafe-url\")\n"
  },
  {
    "path": "secure/headers/server.py",
    "content": "from __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\n@dataclass\nclass Server(BaseHeader):\n    \"\"\"\n    Builder for the ``Server`` HTTP response header.\n\n    Default header value: ``\"\"``\n\n    Notes:\n        * The default is intentionally empty to avoid leaking server details.\n        * Callers can override this value for compatibility with legacy tooling.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server\n        - https://owasp.org/www-project-secure-headers/\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.SERVER.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.SERVER.value, repr=False)\n    _value: str = field(default=HeaderDefaultValue.SERVER.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"\n        Retrieve the current value of the `Server` header.\n\n        Returns:\n            str: The current value of the `Server` header.\n        \"\"\"\n        return self._value\n\n    def set(self, value: str) -> Server:\n        \"\"\"\n        Set a custom value for the `Server` header.\n\n        This allows you to override the default `Server` header value with a custom value\n        that will be included in HTTP responses.\n\n        Args:\n            value: The custom value to set for the `Server` header.\n\n        Returns:\n            Server: The current instance, allowing for method chaining.\n        \"\"\"\n        self._value = normalize_header_value(value, what=\"Server value\")\n        return self\n\n    def value(self, value: str) -> Server:\n        \"\"\"Alias for :meth:`set` (kept for feature parity with other headers).\"\"\"\n        return self.set(value)\n\n    def clear(self) -> Server:\n        \"\"\"\n        Reset the `Server` header value to its default (an empty string).\n\n        This method clears any custom value that has been set for the `Server`\n        header and reverts it to the default, which hides server details.\n\n        Returns:\n            Server: The current instance, allowing for method chaining.\n        \"\"\"\n        self._value = self._default_value\n        return self\n"
  },
  {
    "path": "secure/headers/strict_transport_security.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security\n# https://owasp.org/www-project-secure-headers/\n#\n# Strict-Transport-Security by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n_PRELOAD_MIN_MAX_AGE_SECONDS = 31_536_000  # 1 year\n\n\n@dataclass\nclass StrictTransportSecurity(BaseHeader):\n    \"\"\"\n    Builder for the ``Strict-Transport-Security`` (HSTS) HTTP response header.\n\n    Default header value: ``max-age=31536000``\n\n    Notes:\n        * Only send this header over HTTPS; browsers ignore it otherwise.\n        * ``preload`` requires ``includeSubDomains`` and at least one year ``max-age``.\n        * ``max-age`` is required by the HSTS specification.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security\n        - https://hstspreload.org/\n        - https://owasp.org/www-project-secure-headers/\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.STRICT_TRANSPORT_SECURITY.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.STRICT_TRANSPORT_SECURITY.value, repr=False)\n\n    # Structured directives\n    _max_age: int | None = None\n    _include_subdomains: bool = False\n    _preload: bool = False\n\n    # Escape hatch: if set, emitted exactly as provided (after basic safety checks).\n    _raw_value: str | None = None\n\n    # ------------------------------------------------------------------\n    # Serialization\n    # ------------------------------------------------------------------\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the serialized ``Strict-Transport-Security`` header value.\"\"\"\n        if self._raw_value is not None:\n            return self._raw_value\n\n        # If nothing was explicitly configured, emit the library default.\n        if self._max_age is None and not self._include_subdomains and not self._preload:\n            return self._default_value\n\n        max_age = self._max_age if self._max_age is not None else self._default_max_age_seconds()\n\n        parts: list[str] = [f\"max-age={max_age}\"]\n\n        if self._preload:\n            # MDN: preload requires includeSubDomains and min max-age.\n            if max_age < _PRELOAD_MIN_MAX_AGE_SECONDS:\n                raise ValueError(\n                    \"preload requires max-age to be at least 31536000 seconds (1 year). \"\n                    \"Increase max-age or remove preload().\"\n                )\n            parts.append(\"includeSubDomains\")\n            parts.append(\"preload\")\n            return \"; \".join(parts)\n\n        if self._include_subdomains:\n            parts.append(\"includeSubDomains\")\n\n        return \"; \".join(parts)\n\n    def _default_max_age_seconds(self) -> int:\n        \"\"\"Extract the integer max-age from the library default (fallback: 31536000).\"\"\"\n        prefix = \"max-age=\"\n        if self._default_value.startswith(prefix):\n            rest = self._default_value[len(prefix) :].split(\";\", 1)[0].strip()\n            try:\n                return int(rest)\n            except ValueError:\n                pass\n        return _PRELOAD_MIN_MAX_AGE_SECONDS\n\n    @staticmethod\n    def _ensure_no_newlines(value: str) -> str:\n        \"\"\"Reject values that could enable header injection via CR/LF.\"\"\"\n        return normalize_header_value(value, what=\"Strict-Transport-Security value\")\n\n    def clear(self) -> StrictTransportSecurity:\n        \"\"\"Clear configured directives and reset back to the library default.\"\"\"\n        self._max_age = None\n        self._include_subdomains = False\n        self._preload = False\n        self._raw_value = None\n        return self\n\n    # ------------------------------------------------------------------\n    # Directive builders\n    # ------------------------------------------------------------------\n\n    def value(self, value: str) -> StrictTransportSecurity:\n        \"\"\"Set a raw header value (escape hatch), replacing any configured directives.\"\"\"\n        value = self._ensure_no_newlines(value).strip()\n        self._raw_value = value\n        self._max_age = None\n        self._include_subdomains = False\n        self._preload = False\n        return self\n\n    # Backwards-compatible alias used by other header modules in this codebase.\n    set = value\n\n    def max_age(self, seconds: int) -> StrictTransportSecurity:\n        \"\"\"Set ``max-age``: how long (in seconds) the browser should remember to use HTTPS only.\"\"\"\n        if seconds < 0:\n            raise ValueError(\"max-age must be a non-negative integer (use 0 to disable HSTS).\")\n\n        if self._preload and seconds < _PRELOAD_MIN_MAX_AGE_SECONDS:\n            raise ValueError(\n                \"preload requires max-age to be at least 31536000 seconds (1 year). \"\n                \"Increase max-age or remove preload().\"\n            )\n\n        self._raw_value = None\n        self._max_age = int(seconds)\n        return self\n\n    def include_subdomains(self) -> StrictTransportSecurity:\n        \"\"\"Add ``includeSubDomains``: apply the HSTS policy to all subdomains as well.\"\"\"\n        self._raw_value = None\n        self._include_subdomains = True\n        return self\n\n    def preload(self) -> StrictTransportSecurity:\n        \"\"\"Add ``preload``: enable HSTS preload list requirements (requires includeSubDomains and 1y+ max-age).\"\"\"\n        self._raw_value = None\n        self._preload = True\n        self._include_subdomains = True  # required when preload is used (per MDN)\n\n        if self._max_age is not None and self._max_age < _PRELOAD_MIN_MAX_AGE_SECONDS:\n            raise ValueError(\n                \"preload requires max-age to be at least 31536000 seconds (1 year). \"\n                \"Increase max-age or remove preload().\"\n            )\n\n        return self\n"
  },
  {
    "path": "secure/headers/x_content_type_options.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n# https://owasp.org/www-project-secure-headers/#x-content-type-options\n#\n# X-Content-Type-Options by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\n@dataclass\nclass XContentTypeOptions(BaseHeader):\n    \"\"\"\n    Builder for the `X-Content-Type-Options` HTTP header.\n\n    Default header value: `nosniff`\n\n    Notes:\n        * The only standardized directive is `nosniff`; other values are allowed but discouraged.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n        - https://owasp.org/www-project-secure-headers/#x-content-type-options\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.X_CONTENT_TYPE_OPTIONS.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.X_CONTENT_TYPE_OPTIONS.value, repr=False)\n    _value: str = field(default=HeaderDefaultValue.X_CONTENT_TYPE_OPTIONS.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `X-Content-Type-Options` header value.\n\n        Returns:\n            The current `X-Content-Type-Options` header value as a string.\n        \"\"\"\n        return self._value\n\n    def set(self, value: str) -> XContentTypeOptions:\n        \"\"\"\n        Set a custom value for the `X-Content-Type-Options` header.\n\n        Args:\n            value: The custom header value.\n\n        Returns:\n            The `XContentTypeOptions` instance for method chaining.\n        \"\"\"\n        self._value = normalize_header_value(value, what=\"X-Content-Type-Options value\")\n        return self\n\n    def value(self, value: str) -> XContentTypeOptions:\n        \"\"\"Alias for :meth:`set` to match other headers.\"\"\"\n        return self.set(value)\n\n    def clear(self) -> XContentTypeOptions:\n        \"\"\"\n        Reset the `X-Content-Type-Options` header to its default value.\n\n        Returns:\n            The `XContentTypeOptions` instance for method chaining.\n        \"\"\"\n        self._value = self._default_value\n        return self\n\n    def nosniff(self) -> XContentTypeOptions:\n        \"\"\"\n        Set the `X-Content-Type-Options` header to `nosniff`.\n\n        This value tells the browser to block requests for certain content types and prevents MIME-sniffing attacks.\n\n        Resources:\n            https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n\n        Returns:\n            The `XContentTypeOptions` instance for method chaining.\n        \"\"\"\n        self._value = \"nosniff\"\n        return self\n"
  },
  {
    "path": "secure/headers/x_dns_prefetch_control.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-DNS-Prefetch-Control\n# https://owasp.org/www-project-secure-headers/#x-dns-prefetch-control\n#\n# X-DNS-Prefetch-Control by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\nfrom typing import Final\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n_VALID_VALUES: Final[frozenset[str]] = frozenset({\"on\", \"off\"})\n\n\n@dataclass\nclass XDnsPrefetchControl(BaseHeader):\n    \"\"\"\n    Builder for the non-standard `X-DNS-Prefetch-Control` HTTP header.\n\n    Default header value: `off`\n\n    Notes:\n        * Browsers may ignore this header as it is non-standard, but it documents\n          the desired behavior for DNS prefetching.\n        * Normalization keeps ``on``/``off`` lowercase while permitting other values unchanged.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-DNS-Prefetch-Control\n        - https://owasp.org/www-project-secure-headers/#x-dns-prefetch-control\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.X_DNS_PREFETCH_CONTROL.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.X_DNS_PREFETCH_CONTROL.value, repr=False)\n    _value: str = field(default_factory=lambda: HeaderDefaultValue.X_DNS_PREFETCH_CONTROL.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current header value.\"\"\"\n        return self._value\n\n    def clear(self) -> XDnsPrefetchControl:\n        \"\"\"Reset to the library default value (`off`).\"\"\"\n        self._value = self._default_value\n        return self\n\n    def set(self, value: str) -> XDnsPrefetchControl:\n        \"\"\"\n        Set a custom value for the `X-DNS-Prefetch-Control` header.\n\n        Typical values are `on` or `off`. If `value` is `on`/`off` (case-insensitive),\n        it will be normalized to lowercase for deterministic output.\n        \"\"\"\n        cleaned = normalize_header_value(value, what=\"X-DNS-Prefetch-Control value\")\n        self._value = self._normalize(cleaned)\n        return self\n\n    def value(self, value: str) -> XDnsPrefetchControl:\n        \"\"\"Alias for :meth:`set`.\"\"\"\n        return self.set(value)\n\n    def custom(self, value: str) -> XDnsPrefetchControl:\n        \"\"\"Alias for :meth:`set` (escape hatch).\"\"\"\n        return self.set(value)\n\n    def on(self) -> XDnsPrefetchControl:\n        \"\"\"\n        Enable DNS prefetching.\n\n        This is what browsers do (when supported) if the header is not present.\n        \"\"\"\n        self._value = \"on\"\n        return self\n\n    def off(self) -> XDnsPrefetchControl:\n        \"\"\"\n        Disable DNS prefetching.\n\n        Useful if you don't control the links on the page or don't want to leak\n        information to these domains.\n        \"\"\"\n        self._value = \"off\"\n        return self\n\n    # Backwards-compatible aliases (keep existing public API)\n    def allow(self) -> XDnsPrefetchControl:\n        \"\"\"Alias for :meth:`on`.\"\"\"\n        return self.on()\n\n    def disable(self) -> XDnsPrefetchControl:\n        \"\"\"Alias for :meth:`off`.\"\"\"\n        return self.off()\n\n    @staticmethod\n    def _normalize(value: str) -> str:\n        \"\"\"Trim whitespace and canonicalize `on`/`off` to lowercase when recognized.\"\"\"\n        v = value.strip()\n        v_lc = v.lower()\n        return v_lc if v_lc in _VALID_VALUES else v\n"
  },
  {
    "path": "secure/headers/x_frame_options.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options\n# https://owasp.org/www-project-secure-headers/#x-frame-options\n#\n# X-Frame-Options by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\n@dataclass\nclass XFrameOptions(BaseHeader):\n    \"\"\"\n    Builder for the `X-Frame-Options` HTTP response header.\n\n    Default header value: `SAMEORIGIN`\n\n    Notes:\n        * Consider CSP `frame-ancestors` for richer framing controls.\n        * This header is only processed when sent as an HTTP response header.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options\n        - https://owasp.org/www-project-secure-headers/#x-frame-options\n    \"\"\"\n\n    header_name: str = field(init=False, default=HeaderName.X_FRAME_OPTIONS.value, repr=False)\n    _default_value: str = field(init=False, default=HeaderDefaultValue.X_FRAME_OPTIONS.value, repr=False)\n    _value: str = field(default=HeaderDefaultValue.X_FRAME_OPTIONS.value, repr=False)\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current `X-Frame-Options` header value.\"\"\"\n        return self._value\n\n    # ---------------------------------------------------------------------\n    # Escape hatches\n    # ---------------------------------------------------------------------\n\n    def value(self, value: str) -> XFrameOptions:\n        \"\"\"Set a custom header value.\n\n        Use this when you already have a fully-formed header value and want to bypass\n        directive helpers.\n\n        Notes:\n            This method rejects CR/LF characters to avoid header injection. Any further\n            validation/normalization belongs in `Secure.validate_and_normalize_headers(...)`.\n\n        Args:\n            value: The complete header value.\n\n        Returns:\n            The `XFrameOptions` instance for method chaining.\n        \"\"\"\n        self._value = normalize_header_value(value, what=\"X-Frame-Options value\")\n        return self\n\n    def set(self, value: str) -> XFrameOptions:\n        \"\"\"Alias for `value(...)` (backwards-compatible).\"\"\"\n        return self.value(value)\n\n    def custom(self, value: str) -> XFrameOptions:\n        \"\"\"Alias for `value(...)`.\"\"\"\n        return self.value(value)\n\n    def clear(self) -> XFrameOptions:\n        \"\"\"Reset the `X-Frame-Options` header to its default value (`SAMEORIGIN`).\"\"\"\n        self._value = self._default_value\n        return self\n\n    # ---------------------------------------------------------------------\n    # Directives\n    # ---------------------------------------------------------------------\n\n    def deny(self) -> XFrameOptions:\n        \"\"\"Set the directive to `DENY`.\n\n        The page cannot be displayed in a frame, regardless of the site attempting to do so.\n        \"\"\"\n        self._value = \"DENY\"\n        return self\n\n    def sameorigin(self) -> XFrameOptions:\n        \"\"\"Set the directive to `SAMEORIGIN`.\n\n        The page can only be displayed if all ancestor frames have the same origin as the page.\n        \"\"\"\n        self._value = \"SAMEORIGIN\"\n        return self\n\n    def allow_from(self, origin: str) -> XFrameOptions:\n        \"\"\"Set the (obsolete) `ALLOW-FROM <origin>` directive.\n\n        Warning:\n            This is an obsolete directive. Modern browsers that encounter response headers\n            with this directive will ignore the header completely. Use CSP `frame-ancestors`\n            instead.\n\n        Args:\n            origin: An origin value (for example, `https://example.com`).\n\n        Returns:\n            The `XFrameOptions` instance for method chaining.\n        \"\"\"\n        # Keep construction minimal; validity of the origin is out-of-scope for this module.\n        return self.value(f\"ALLOW-FROM {origin.strip()}\")\n"
  },
  {
    "path": "secure/headers/x_permitted_cross_domain_policies.py",
    "content": "# Security header recommendations and information from the MDN Web Docs and the OWASP Secure Headers Project\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Permitted-Cross-Domain-Policies\n# https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies\n#\n# X-Permitted-Cross-Domain-Policies by Mozilla Contributors is licensed under CC-BY-SA 2.5.\n# https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor\n# https://creativecommons.org/licenses/by-sa/2.5/\n\nfrom __future__ import annotations  # type: ignore\n\nfrom dataclasses import dataclass, field\nfrom typing import Final, Literal\n\nfrom secure.headers._validation import normalize_header_value\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\nPermittedCrossDomainPolicy = Literal[\n    \"none\",\n    \"master-only\",\n    \"by-content-type\",\n    \"by-ftp-filename\",\n    \"all\",\n    \"none-this-response\",\n]\n\n_ALLOWED_POLICIES: Final[set[str]] = {\n    \"none\",\n    \"master-only\",\n    \"by-content-type\",\n    \"by-ftp-filename\",\n    \"all\",\n    \"none-this-response\",\n}\n\n\n@dataclass\nclass XPermittedCrossDomainPolicies(BaseHeader):\n    \"\"\"\n    Builder for the `X-Permitted-Cross-Domain-Policies` HTTP response header.\n\n    Default header value: `none`\n\n    Notes:\n        * This header governs which cross-domain policy files legacy clients (Flash,\n          Silverlight, etc.) may load.\n        * Use helper methods for MDN-defined directives; ``value`` is an escape hatch.\n\n    Resources:\n        - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Permitted-Cross-Domain-Policies\n        - https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies\n    \"\"\"\n\n    header_name: str = field(\n        init=False,\n        default=HeaderName.X_PERMITTED_CROSS_DOMAIN_POLICIES.value,\n        repr=False,\n    )\n    _default_value: str = field(\n        init=False,\n        default=HeaderDefaultValue.X_PERMITTED_CROSS_DOMAIN_POLICIES.value,\n        repr=False,\n    )\n    _value: str = field(\n        default_factory=lambda: HeaderDefaultValue.X_PERMITTED_CROSS_DOMAIN_POLICIES.value,\n        repr=False,\n    )\n\n    @property\n    def header_value(self) -> str:\n        \"\"\"Return the current header value.\"\"\"\n        return self._value\n\n    # --- Escape hatches / lifecycle -------------------------------------------------\n\n    def clear(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Reset the header to the default value (`none`).\"\"\"\n        self._value = self._default_value\n        return self\n\n    def value(self, value: str) -> XPermittedCrossDomainPolicies:\n        \"\"\"\n        Set a custom header value.\n\n        Prefer the directive helper methods (e.g., :meth:`none`, :meth:`master_only`)\n        when you want a well-known policy.\n        \"\"\"\n        self._value = normalize_header_value(value, what=\"X-Permitted-Cross-Domain-Policies value\")\n        return self\n\n    def custom(self, value: str) -> XPermittedCrossDomainPolicies:\n        \"\"\"Alias for :meth:`value`.\"\"\"\n        return self.value(value)\n\n    def set(self, value: str) -> XPermittedCrossDomainPolicies:\n        \"\"\"Backwards-compatible alias for :meth:`value`.\"\"\"\n        return self.value(value)\n\n    def policy(self, policy: PermittedCrossDomainPolicy) -> XPermittedCrossDomainPolicies:\n        \"\"\"Set the header to one of the known directive values.\"\"\"\n        if policy not in _ALLOWED_POLICIES:\n            raise ValueError(f\"Unsupported X-Permitted-Cross-Domain-Policies value: {policy!r}\")\n        self._value = policy\n        return self\n\n    # --- Directive helpers ----------------------------------------------------------\n\n    def none(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Disallow policy files anywhere on the target server, including a master policy file.\"\"\"\n        return self.policy(\"none\")\n\n    def master_only(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Allow cross-domain access to the master policy file defined on the same domain.\"\"\"\n        return self.policy(\"master-only\")\n\n    def by_content_type(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Allow only policy files served with `Content-Type: text/x-cross-domain-policy` (HTTP/HTTPS only).\"\"\"\n        return self.policy(\"by-content-type\")\n\n    def by_ftp_filename(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Allow only policy files named `crossdomain.xml` (FTP only).\"\"\"\n        return self.policy(\"by-ftp-filename\")\n\n    def all(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Allow all policy files on this target domain.\"\"\"\n        return self.policy(\"all\")\n\n    def none_this_response(self) -> XPermittedCrossDomainPolicies:\n        \"\"\"Indicate the current document should not be used as a policy file.\"\"\"\n        return self.policy(\"none-this-response\")\n"
  },
  {
    "path": "secure/middleware/__init__.py",
    "content": "from __future__ import annotations\n\nfrom .asgi import SecureASGIMiddleware\nfrom .wsgi import SecureWSGIMiddleware\n\n__all__ = [\n    \"SecureASGIMiddleware\",\n    \"SecureWSGIMiddleware\",\n]\n"
  },
  {
    "path": "secure/middleware/asgi.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable, Iterable, MutableMapping\nfrom typing import Protocol, TypeAlias, cast\n\nfrom ..secure import MULTI_OK, Secure\n\n# ---------------------------------------------------------------------------\n# ASGI typing aliases\n# ---------------------------------------------------------------------------\n\nScope: TypeAlias = object\nMessage: TypeAlias = object\n\nReceive: TypeAlias = Callable[[], Awaitable[Message]]\nSend: TypeAlias = Callable[[Message], Awaitable[None]]\n\n\nclass ASGIApp(Protocol):\n    def __call__(self, scope: Scope, receive: Receive, send: Send) -> Awaitable[None]: ...\n\n\n# ``http.response.start`` stores headers as a list of (name: bytes, value: bytes).\nHeaderList: TypeAlias = list[tuple[bytes, bytes]]\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _normalize_header_name(name: str) -> str:\n    \"\"\"Normalize a header field-name for case-insensitive comparison.\"\"\"\n    return name.strip().lower()\n\n\ndef _normalize_header_name_bytes(name: bytes) -> bytes:\n    \"\"\"Normalize a header field-name (bytes) for case-insensitive comparison.\"\"\"\n    return name.strip().lower()\n\n\ndef _encode_header_name(name: str) -> bytes:\n    \"\"\"\n    Encode an HTTP header field-name as ASCII bytes.\n\n    ASGI requires header field-names to be ``bytes``. Per RFC 9110, header\n    field-names are ASCII.\n    \"\"\"\n    return name.encode(\"ascii\")\n\n\ndef _encode_header_value(value: str) -> bytes:\n    \"\"\"\n    Encode an HTTP header field-value as latin-1 bytes.\n\n    ASGI transports header values as ``bytes``. The de-facto convention for ASGI\n    servers is latin-1 encoding, matching common implementations and avoiding\n    accidental Unicode transformations.\n    \"\"\"\n    return value.encode(\"latin-1\")\n\n\n# ---------------------------------------------------------------------------\n# Middleware\n# ---------------------------------------------------------------------------\n\n\nclass SecureASGIMiddleware:\n    \"\"\"\n    Apply Secure's configured HTTP security headers to an ASGI application.\n\n    This middleware wraps an ASGI app and injects headers by intercepting the\n    ``http.response.start`` message for HTTP requests.\n\n    Parameters\n    ----------\n    app:\n        The ASGI application to wrap.\n    secure:\n        A configured :class:`~secure.Secure` instance. If omitted, uses\n        :meth:`~secure.Secure.with_default_headers`.\n    multi_ok:\n        Header names allowed to appear multiple times in a response. For these,\n        Secure's value is appended instead of overwriting. Defaults to\n        :data:`secure.MULTI_OK`.\n\n    Behavior\n    --------\n    - Only applies to HTTP scopes (``scope[\"type\"] == \"http\"``).\n    - For most headers, existing values are removed (case-insensitive) and\n      Secure's value is added to avoid duplicates.\n    - For headers listed in ``multi_ok``, existing values are preserved and\n      Secure's value is appended.\n\n    Notes\n    -----\n    This middleware is intentionally \"response-object free\": it does not require\n    a framework's response type, so it can be used with any ASGI-compliant stack.\n    \"\"\"\n\n    def __init__(\n        self,\n        app: object,\n        *,\n        secure: Secure | None = None,\n        multi_ok: Iterable[str] | None = None,\n    ) -> None:\n        # Cast once: callers may pass function apps or callable objects.\n        self.app: ASGIApp = cast(\"ASGIApp\", app)\n        self.secure = secure or Secure.with_default_headers()\n\n        provided = MULTI_OK if multi_ok is None else multi_ok\n        # Normalize once during init; comparisons during request handling are bytes-based.\n        self._multi_ok: frozenset[bytes] = frozenset(\n            _normalize_header_name_bytes(_encode_header_name(_normalize_header_name(name))) for name in provided\n        )\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        scope_map = cast(\"MutableMapping[str, object]\", scope)\n        if scope_map.get(\"type\") != \"http\":\n            await self.app(scope, receive, send)\n            return\n\n        async def send_wrapper(message: Message) -> None:\n            msg = cast(\"MutableMapping[str, object]\", message)\n\n            if msg.get(\"type\") == \"http.response.start\":\n                raw_headers = msg.get(\"headers\", [])\n                headers: HeaderList = list(cast(\"Iterable[tuple[bytes, bytes]]\", raw_headers))\n\n                # Track existing occurrences by normalized key.\n                positions: dict[bytes, list[int]] = {}\n                for i, (k, _v) in enumerate(headers):\n                    positions.setdefault(_normalize_header_name_bytes(k), []).append(i)\n\n                # Apply Secure headers.\n                for name_str, value_str in self.secure.headers.items():\n                    name_b = _encode_header_name(name_str)\n                    norm_name_b = _normalize_header_name_bytes(name_b)\n                    value_b = _encode_header_value(value_str)\n\n                    if norm_name_b in self._multi_ok:\n                        headers.append((name_b, value_b))\n                        continue\n\n                    # Remove all existing values for this header (if present).\n                    if norm_name_b in positions:\n                        for idx in reversed(positions[norm_name_b]):\n                            headers.pop(idx)\n                        positions.pop(norm_name_b, None)\n\n                    headers.append((name_b, value_b))\n                    positions[norm_name_b] = [len(headers) - 1]\n\n                msg[\"headers\"] = headers\n\n            await send(message)\n\n        await self.app(scope, receive, send_wrapper)\n"
  },
  {
    "path": "secure/middleware/wsgi.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom types import TracebackType\nfrom typing import Any, TypeAlias\n\nfrom ..secure import MULTI_OK, Secure\n\n# ---------------------------------------------------------------------------\n# WSGI typing aliases\n# ---------------------------------------------------------------------------\n\nHeaderList: TypeAlias = list[tuple[str, str]]\n\n# PEP 3333: exc_info may be a 3-tuple, or None. In practice (and in wsgiref types),\n# it's also allowed to be (None, None, None) as a sentinel.\nExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType] | tuple[None, None, None] | None\n\n# PEP 3333: start_response(status, headers[, exc_info]) -> write(body_bytes)\nWriteCallable: TypeAlias = Any  # write callable exists, but is rarely used in modern apps\n\ntry:\n    # Python 3.11+ provides useful WSGI types in the stdlib.\n    from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment\nexcept Exception:  # pragma: no cover\n    from collections.abc import Callable\n\n    WSGIEnvironment = dict[str, Any]\n    StartResponse = Callable[[str, HeaderList, ExcInfo], WriteCallable]\n    WSGIApplication = Callable[[WSGIEnvironment, StartResponse], Iterable[bytes]]\n\nWSGIApp: TypeAlias = WSGIApplication\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _normalize_header_name(name: str) -> str:\n    \"\"\"Normalize a header field-name for case-insensitive comparison.\"\"\"\n    return name.strip().lower()\n\n\n# ---------------------------------------------------------------------------\n# Middleware\n# ---------------------------------------------------------------------------\n\n\nclass SecureWSGIMiddleware:\n    \"\"\"\n    Apply Secure's configured HTTP security headers to a WSGI application.\n\n    This middleware wraps a WSGI app and injects headers by wrapping the WSGI\n    ``start_response`` callable. It does not require a framework-specific\n    response object.\n\n    Parameters\n    ----------\n    app:\n        The WSGI application to wrap.\n    secure:\n        A configured :class:`~secure.Secure` instance. If omitted, uses\n        :meth:`~secure.Secure.with_default_headers`.\n    multi_ok:\n        Header names allowed to appear multiple times in a response. For these,\n        Secure's value is appended instead of overwriting. Defaults to\n        :data:`secure.MULTI_OK`.\n\n    Behavior\n    --------\n    - Overwrites existing headers by default (case-insensitive) to avoid duplicates\n      for single-value headers (e.g., ``X-Frame-Options``).\n    - For header names in ``multi_ok``, existing values are preserved and\n      Secure's value is appended.\n\n    Notes\n    -----\n    WSGI technically allows calling ``start_response`` multiple times in error\n    scenarios (via ``exc_info``). This middleware preserves that behavior by\n    forwarding ``exc_info`` unchanged.\n    \"\"\"\n\n    def __init__(\n        self,\n        app: WSGIApp,\n        *,\n        secure: Secure | None = None,\n        multi_ok: Iterable[str] | None = None,\n    ) -> None:\n        self.app = app\n        self.secure = secure or Secure.with_default_headers()\n\n        provided = MULTI_OK if multi_ok is None else multi_ok\n        self._multi_ok: frozenset[str] = frozenset(_normalize_header_name(h) for h in provided)\n\n    def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]:\n        \"\"\"\n        Invoke the wrapped WSGI app, injecting configured security headers.\n\n        Parameters\n        ----------\n        environ:\n            The WSGI environment for the request.\n        start_response:\n            The WSGI ``start_response`` callable provided by the server.\n\n        Returns\n        -------\n        Iterable[bytes]\n            The response body iterable returned by the wrapped application.\n        \"\"\"\n\n        def custom_start_response(\n            status: str,\n            headers: HeaderList,\n            exc_info: ExcInfo = None,\n        ) -> WriteCallable:\n            out: HeaderList = list(headers)\n\n            # Track existing occurrences by normalized key.\n            positions: dict[str, list[int]] = {}\n            for i, (k, _v) in enumerate(out):\n                positions.setdefault(_normalize_header_name(k), []).append(i)\n\n            # Apply Secure headers.\n            for k, v in self.secure.headers.items():\n                nk = _normalize_header_name(k)\n\n                if nk in self._multi_ok:\n                    out.append((k, v))\n                    continue\n\n                if nk in positions:\n                    for idx in reversed(positions[nk]):\n                        out.pop(idx)\n                    positions.pop(nk, None)\n\n                out.append((k, v))\n                positions[nk] = [len(out) - 1]\n\n            return start_response(status, out, exc_info)\n\n        return self.app(environ, custom_start_response)\n"
  },
  {
    "path": "secure/py.typed",
    "content": ""
  },
  {
    "path": "secure/secure.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Mapping\n\n    from ._internal.types import DeduplicateAction, HeaderItems, OnInvalidPolicy, OnUnexpectedPolicy, ResponseProtocol\n    from .headers import (\n        BaseHeader,\n        CacheControl,\n        ContentSecurityPolicy,\n        CrossOriginEmbedderPolicy,\n        CrossOriginOpenerPolicy,\n        CrossOriginResourcePolicy,\n        CustomHeader,\n        PermissionsPolicy,\n        ReferrerPolicy,\n        Server,\n        StrictTransportSecurity,\n        XContentTypeOptions,\n        XDnsPrefetchControl,\n        XFrameOptions,\n        XPermittedCrossDomainPolicies,\n    )\n\nfrom ._internal import emit as _emit\nfrom ._internal.configured_headers import ConfiguredHeaders, header_items_from_objects, header_mapping_from_items\nfrom ._internal.constants import COMMA_JOIN_OK, DEFAULT_ALLOWED_HEADERS, MULTI_OK\nfrom ._internal.normalize import normalize_header_items\nfrom ._internal.policy import allowlist_header_objects, deduplicate_header_objects\nfrom ._internal.presets import Preset, preset_kwargs\n\nHeaderSetError = _emit.HeaderSetError\n\n# ---------------------------------------------------------------------------\n# Core API\n# ---------------------------------------------------------------------------\n\n\nclass Secure:\n    \"\"\"\n    Configure and apply HTTP security headers for web applications.\n\n    A :class:`Secure` instance is the library's public facade. It encapsulates a\n    set of typed header builders and applies them to response objects from common\n    Python web frameworks (FastAPI, Starlette, Flask, Django, etc.).\n\n    Typical pipeline:\n\n    >>> secure = (\n    ...     Secure.with_default_headers().allowlist_headers().deduplicate_headers().validate_and_normalize_headers()\n    ... )\n\n    Then, inside your framework integration:\n\n    >>> secure.set_headers(response)\n    >>> # or in async contexts:\n    >>> await secure.set_headers_async(response)\n\n    Attributes\n    ----------\n    headers_list :\n        Ordered list of header objects representing the configured headers.\n        Methods like :meth:`allowlist_headers` and :meth:`deduplicate_headers`\n        operate on this list in place and return ``self`` for chaining.\n    \"\"\"\n\n    def __init__(  # noqa: PLR0913\n        self,\n        *,\n        cache: CacheControl | None = None,\n        coep: CrossOriginEmbedderPolicy | None = None,\n        coop: CrossOriginOpenerPolicy | None = None,\n        corp: CrossOriginResourcePolicy | None = None,\n        csp: ContentSecurityPolicy | None = None,\n        custom: list[CustomHeader] | None = None,\n        hsts: StrictTransportSecurity | None = None,\n        permissions: PermissionsPolicy | None = None,\n        referrer: ReferrerPolicy | None = None,\n        xpcdp: XPermittedCrossDomainPolicies | None = None,\n        xdfc: XDnsPrefetchControl | None = None,\n        server: Server | None = None,\n        xcto: XContentTypeOptions | None = None,\n        xfo: XFrameOptions | None = None,\n    ) -> None:\n        \"\"\"\n        Initialize a :class:`Secure` instance with the specified security headers.\n\n        Parameters\n        ----------\n        cache :\n            Cache-Control header configuration.\n        coep :\n            Cross-Origin-Embedder-Policy header configuration.\n        coop :\n            Cross-Origin-Opener-Policy header configuration.\n        corp :\n            Cross-Origin-Resource-Policy header configuration.\n        csp :\n            Content-Security-Policy header configuration.\n        custom :\n            Additional custom headers to include (app-specific).\n        hsts :\n            Strict-Transport-Security header configuration.\n        permissions :\n            Permissions-Policy header configuration.\n        referrer :\n            Referrer-Policy header configuration.\n        xpcdp :\n            X-Permitted-Cross-Domain-Policies header configuration.\n        xdfc :\n            X-DNS-Prefetch-Control header configuration.\n        server :\n            Server header configuration.\n        xcto :\n            X-Content-Type-Options header configuration.\n        xfo :\n            X-Frame-Options header configuration.\n        \"\"\"\n        params: tuple[BaseHeader | None, ...] = (\n            cache,\n            coep,\n            coop,\n            corp,\n            csp,\n            hsts,\n            permissions,\n            referrer,\n            xpcdp,\n            xdfc,\n            server,\n            xcto,\n            xfo,\n        )\n        configured_headers = [header for header in params if header is not None]\n\n        if custom:\n            configured_headers.extend(custom)\n\n        self._headers = ConfiguredHeaders(configured_headers, on_change=self._discard_normalized_headers)\n        self._normalized_headers: Mapping[str, str] | None = None\n        self._normalized_source_items: HeaderItems | None = None\n\n    @property\n    def headers_list(self) -> list[BaseHeader]:\n        \"\"\"Mutable ordered list of configured header builders.\"\"\"\n        return self._headers\n\n    @headers_list.setter\n    def headers_list(self, headers: Iterable[BaseHeader]) -> None:\n        self._headers.replace_all(headers)\n\n    def _discard_normalized_headers(self) -> None:\n        self._normalized_headers = None\n        self._normalized_source_items = None\n\n    def _normalized_headers_for(\n        self,\n        header_items: HeaderItems,\n    ) -> Mapping[str, str] | None:\n        if self._normalized_headers is None:\n            return None\n\n        if self._normalized_source_items != header_items:\n            self._discard_normalized_headers()\n            return None\n\n        return self._normalized_headers\n\n    @classmethod\n    def with_default_headers(cls) -> Secure:\n        \"\"\"\n        Create a :class:`Secure` instance with a sensible default set of headers.\n\n        This configuration is suitable for many modern applications and can be\n        customized with methods like :meth:`allowlist_headers` or by adding\n        additional header builder objects.\n\n        Returns\n        -------\n        Secure\n            Instance preconfigured with :data:`Preset.BALANCED`, the recommended\n            default profile.\n        \"\"\"\n        return cls.from_preset(Preset.BALANCED)\n\n    @classmethod\n    def from_preset(cls, preset: Preset) -> Secure:\n        \"\"\"\n        Create a :class:`Secure` instance using a predefined security preset.\n\n        Parameters\n        ----------\n        preset :\n            The security preset to use, for example :data:`Preset.BALANCED` for the\n            recommended default profile, :data:`Preset.BASIC` for compatibility-\n            oriented behavior, or :data:`Preset.STRICT` for a hardened configuration with\n            stronger guarantees.\n\n        Returns\n        -------\n        Secure\n            Instance configured with the selected preset.\n\n        Raises\n        ------\n        ValueError\n            If an unknown preset is provided.\n        \"\"\"\n        return cls(**preset_kwargs(preset))\n\n    def __str__(self) -> str:\n        \"\"\"Return a human-readable listing of headers and their effective values.\"\"\"\n        return \"\\n\".join(f\"{name}: {value}\" for name, value in self._resolved_header_items())\n\n    def __repr__(self) -> str:\n        \"\"\"Return a detailed representation of the :class:`Secure` instance.\"\"\"\n        return f\"{self.__class__.__name__}(headers_list={self.headers_list!r})\"\n\n    # ------------------------------------------------------------------\n    # Header normalization / safety helpers\n    # ------------------------------------------------------------------\n\n    def validate_and_normalize_headers(\n        self,\n        *,\n        on_invalid: OnInvalidPolicy = \"drop\",\n        strict: bool = False,\n        allow_obs_text: bool = False,\n        logger: logging.Logger | None = None,\n    ) -> Secure:\n        \"\"\"\n        Validate and normalize the current header items and cache an immutable mapping.\n\n        This operates on :meth:`header_items` (not ``headers_list`` directly) to\n        preserve ordering, multi-valued behavior, and any prior deduplication.\n\n        The resulting mapping is stored as a normalized snapshot that is returned\n        by :attr:`headers` until the configured headers change.\n\n        Parameters\n        ----------\n        on_invalid :\n            Policy for invalid headers:\n            - ``\"drop\"``: silently drop invalid entries (default).\n            - ``\"warn\"``: log a warning and drop invalid entries.\n            - ``\"raise\"``: raise :class:`ValueError` on invalid entries.\n        strict :\n            If true, treat CR/LF and disallowed characters as hard errors.\n            Other invalid cases (name/value) are governed by ``on_invalid``.\n        allow_obs_text :\n            If true, allow \"obs-text\" (bytes 0x80-0xFF) as per older RFCs.\n        logger :\n            Optional :class:`logging.Logger` used when ``on_invalid=\"warn\"`` or\n            when dropping headers with ``on_invalid=\"drop\"`` but logging is desired.\n\n        Returns\n        -------\n        Secure\n            The same instance, for call chaining.\n\n        Raises\n        ------\n        ValueError\n            If a header name is invalid (when ``on_invalid=\"raise\"``),\n            if duplicates are found when building the single-valued mapping,\n            or if ``strict=True`` and CR/LF or disallowed characters are present.\n        \"\"\"\n        header_items = self.header_items()\n        self._normalized_headers = normalize_header_items(\n            header_items,\n            on_invalid=on_invalid,\n            strict=strict,\n            allow_obs_text=allow_obs_text,\n            logger=logger or logging.getLogger(__name__),\n        )\n        self._normalized_source_items = header_items\n        return self\n\n    def deduplicate_headers(\n        self,\n        *,\n        action: DeduplicateAction = \"raise\",\n        comma_join_ok: frozenset[str] = COMMA_JOIN_OK,\n        multi_ok: frozenset[str] = MULTI_OK,\n        logger: logging.Logger | None = None,\n    ) -> Secure:\n        \"\"\"\n        Deduplicate headers in :attr:`headers_list` according to the chosen policy.\n\n        Parameters\n        ----------\n        action :\n            Policy when encountering disallowed duplicates:\n            - ``\"raise\"``: raise :class:`ValueError` (default).\n            - ``\"first\"``: keep the first instance and drop others.\n            - ``\"last\"``: keep the last instance and drop others.\n            - ``\"concat\"``: join values with commas when safe.\n        comma_join_ok :\n            Names (lowercased) for which RFC 7230-style comma joining is safe.\n        multi_ok :\n            Names (lowercased) that are allowed to appear multiple times\n            (for example Content-Security-Policy).\n        logger :\n            Optional :class:`logging.Logger` used for warning messages when\n            dropping duplicates in non-``\"raise\"`` modes.\n\n        Returns\n        -------\n        Secure\n            The same instance, for call chaining.\n\n        Raises\n        ------\n        ValueError\n            If duplicates are found for headers that are not in ``multi_ok``\n            and the action is ``\"raise\"`` or ``\"concat\"`` for unsafe headers.\n        \"\"\"\n        self.headers_list = deduplicate_header_objects(\n            self._headers,\n            action=action,\n            comma_join_ok=comma_join_ok,\n            multi_ok=multi_ok,\n            logger=logger or logging.getLogger(__name__),\n        )\n        return self\n\n    def allowlist_headers(\n        self,\n        *,\n        allowed: Iterable[str] = DEFAULT_ALLOWED_HEADERS,\n        allow_extra: Iterable[str] | None = None,\n        on_unexpected: OnUnexpectedPolicy = \"raise\",\n        allow_x_prefixed: bool = False,\n        logger: logging.Logger | None = None,\n    ) -> Secure:\n        \"\"\"\n        Enforce a case-insensitive allowlist for header names in :attr:`headers_list`.\n\n        Parameters\n        ----------\n        allowed :\n            Base allowlist of header names (case-insensitive).\n        allow_extra :\n            Additional names to allow, for example app-specific headers.\n        on_unexpected :\n            Policy for headers not in the allowlist:\n            - ``\"raise\"``: error on any name not in the allowlist (default).\n            - ``\"drop\"``: remove unexpected headers (logs if logger is set).\n            - ``\"warn\"``: keep unexpected headers but log a warning.\n        allow_x_prefixed :\n            If true, allows any header starting with ``\"x-\"``.\n        logger :\n            Optional :class:`logging.Logger` used for warnings in ``\"drop\"`` and\n            ``\"warn\"`` modes.\n\n        Returns\n        -------\n        Secure\n            The same instance, for call chaining.\n\n        Raises\n        ------\n        ValueError\n            If ``on_unexpected=\"raise\"`` and any header is not in the allowlist.\n        \"\"\"\n        self.headers_list = allowlist_header_objects(\n            self._headers,\n            allowed=allowed,\n            allow_extra=allow_extra,\n            on_unexpected=on_unexpected,\n            allow_x_prefixed=allow_x_prefixed,\n            logger=logger or logging.getLogger(__name__),\n        )\n        return self\n\n    # ------------------------------------------------------------------\n    # Serialization / access\n    # ------------------------------------------------------------------\n\n    def header_items(self) -> HeaderItems:\n        \"\"\"\n        Serialize the current headers into ``(name, value)`` pairs.\n\n        It does not enforce uniqueness. Use :meth:`deduplicate_headers` or\n        :meth:`validate_and_normalize_headers` when you need a single-valued\n        mapping.\n\n        Returns\n        -------\n        tuple[tuple[str, str], ...]\n            Immutable sequence of ``(name, value)`` pairs.\n        \"\"\"\n        return header_items_from_objects(self._headers)\n\n    def _resolved_header_items(self) -> HeaderItems:\n        \"\"\"\n        Return the list of header items honoring any normalized override.\n        \"\"\"\n        header_items = self.header_items()\n        normalized_headers = self._normalized_headers_for(header_items)\n        if normalized_headers is not None:\n            return tuple(normalized_headers.items())\n        return header_items\n\n    @property\n    def headers(self) -> Mapping[str, str]:\n        \"\"\"\n        Single-valued, immutable mapping of headers.\n\n        By default, this is derived from :meth:`header_items`. If\n        :meth:`validate_and_normalize_headers` has been called, the mapping\n        returned here is the normalized snapshot produced by that method.\n\n        Returns\n        -------\n        Mapping[str, str]\n            Immutable mapping of header names to header values.\n\n        Raises\n        ------\n        ValueError\n            If any header name appears more than once (case-insensitive) when\n            building the mapping and no override is set. This includes headers\n            in :data:`MULTI_OK`. Use :meth:`header_items` to emit multi-valued\n            headers or call :meth:`deduplicate_headers` first.\n        \"\"\"\n        header_items = self.header_items()\n        normalized_headers = self._normalized_headers_for(header_items)\n        if normalized_headers is not None:\n            return normalized_headers\n\n        return header_mapping_from_items(header_items)\n\n    # ------------------------------------------------------------------\n    # Application to framework responses\n    # ------------------------------------------------------------------\n\n    def set_headers(self, response: ResponseProtocol) -> None:\n        \"\"\"\n        Apply configured headers synchronously to ``response``.\n\n        This method is strictly sync-only. It is suitable for synchronous\n        frameworks or sync response objects in async frameworks.\n\n        Supported patterns\n        ------------------\n        * ``response.set_header(name, value)`` (synchronous).\n        * ``response.headers.set(name, value)`` (Werkzeug-style headers container).\n        * ``response.headers[name] = value`` (mapping interface).\n\n        Parameters\n        ----------\n        response :\n            Response object implementing either :class:`SetHeaderProtocol` or\n            :class:`HeadersProtocol`.\n\n        Raises\n        ------\n        RuntimeError\n            If an async setter is detected (for example an async method is used\n            in a sync context).\n        AttributeError\n            If the response lacks both ``.set_header`` and ``.headers``, or if\n            ``.headers`` does not support setting values.\n        HeaderSetError\n            If setting an individual header fails.\n        \"\"\"\n        _emit.set_headers_sync(response, self._resolved_header_items())\n\n    async def set_headers_async(self, response: ResponseProtocol) -> None:\n        \"\"\"\n        Apply configured headers asynchronously to ``response``.\n\n        This method is designed for async frameworks such as FastAPI and\n        Starlette. It transparently supports sync or async setters.\n\n        Supported patterns\n        ------------------\n        * ``await response.set_header(name, value)`` for async setters.\n        * ``response.set_header(name, value)`` for sync setters returning ``None``.\n        * ``await response.headers.set(name, value)`` for async headers containers.\n        * ``response.headers.set(name, value)`` for sync headers containers.\n        * ``await response.headers.__setitem__(name, value)`` for async mappings.\n        * ``response.headers[name] = value`` for sync mappings.\n\n        Parameters\n        ----------\n        response :\n            Response object implementing either :class:`SetHeaderProtocol` or\n            :class:`HeadersProtocol`.\n\n        Raises\n        ------\n        AttributeError\n            If the response lacks both ``.set_header`` and ``.headers``, or if\n            ``.headers`` does not support setting values.\n        HeaderSetError\n            If setting an individual header fails.\n        \"\"\"\n        await _emit.set_headers_async(response, self._resolved_header_items())\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/headers/test_cache_control.py",
    "content": "import unittest\n\nfrom secure.headers import CacheControl\n\n\nclass TestCacheControl(unittest.TestCase):\n    def test_default_cache_control(self):\n        \"\"\"Test default Cache-Control value (no-store, max-age=0).\"\"\"\n        cache_control = CacheControl()\n        self.assertEqual(cache_control.header_value, \"no-store, max-age=0\")\n\n    def test_set_no_cache(self):\n        \"\"\"Test adding the no-cache directive to Cache-Control.\"\"\"\n        cache_control = CacheControl().no_cache()\n        self.assertIn(\"no-cache\", cache_control.header_value)\n\n    def test_set_max_age(self):\n        \"\"\"Test setting a max-age directive in Cache-Control.\"\"\"\n        cache_control = CacheControl().max_age(3600)\n        self.assertIn(\"max-age=3600\", cache_control.header_value)\n\n    def test_clear_cache_control(self):\n        \"\"\"Test clearing Cache-Control directives.\"\"\"\n        cache_control = CacheControl().no_cache().clear()\n        self.assertEqual(cache_control.header_value, \"no-store, max-age=0\")\n\n    def test_multiple_directives(self):\n        \"\"\"Test adding multiple Cache-Control directives.\"\"\"\n        cache_control = CacheControl().no_cache().must_revalidate().max_age(3600)\n        self.assertEqual(cache_control.header_value, \"no-cache, max-age=3600, must-revalidate\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_content_security_policy.py",
    "content": "import unittest\n\nfrom secure.headers import ContentSecurityPolicy\nfrom secure.headers.base_header import HeaderDefaultValue\n\n\nclass TestContentSecurityPolicy(unittest.TestCase):\n    def test_default_csp(self):\n        \"\"\"Test default Content-Security-Policy header.\"\"\"\n        csp = ContentSecurityPolicy()\n        self.assertEqual(\n            csp.header_value,\n            HeaderDefaultValue.CONTENT_SECURITY_POLICY.value,\n        )\n\n    def test_custom_policy(self):\n        \"\"\"Test setting custom directives for CSP.\"\"\"\n        csp = ContentSecurityPolicy().default_src(\"'self'\").img_src(\"'self'\", \"cdn.example.com\")\n        self.assertEqual(csp.header_value, \"default-src 'self'; img-src 'self' cdn.example.com\")\n\n    def test_add_script_src(self):\n        \"\"\"Test adding script-src directive.\"\"\"\n        csp = ContentSecurityPolicy().script_src(\"'self'\", \"'unsafe-inline'\")\n        self.assertIn(\"script-src 'self' 'unsafe-inline'\", csp.header_value)\n\n    def test_clear_policy(self):\n        \"\"\"Test clearing CSP directives.\"\"\"\n        csp = ContentSecurityPolicy().default_src(\"'self'\").clear()\n        self.assertEqual(\n            csp.header_value,\n            HeaderDefaultValue.CONTENT_SECURITY_POLICY.value,\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_cross_origin_embedder_policy.py",
    "content": "import unittest\n\nfrom secure.headers import CrossOriginEmbedderPolicy\nfrom secure.headers.base_header import HeaderDefaultValue\n\n\nclass TestCrossOriginEmbedderPolicy(unittest.TestCase):\n    def test_default_value(self) -> None:\n        \"\"\"Default COEP should be `require-corp`.\"\"\"\n        policy = CrossOriginEmbedderPolicy()\n        self.assertEqual(policy.header_value, HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value)\n\n    def test_builder_helpers(self) -> None:\n        \"\"\"Helper methods should set predictable directives.\"\"\"\n        self.assertEqual(CrossOriginEmbedderPolicy().require_corp().header_value, \"require-corp\")\n        self.assertEqual(CrossOriginEmbedderPolicy().credentialless().header_value, \"credentialless\")\n        self.assertEqual(CrossOriginEmbedderPolicy().unsafe_none().header_value, \"unsafe-none\")\n\n    def test_value_accepts_custom(self) -> None:\n        \"\"\"`value()` should normalize and accept arbitrary strings.\"\"\"\n        policy = CrossOriginEmbedderPolicy().value(\"CUSTOM-Policy \")\n        self.assertEqual(policy.header_value, \"custom-policy\")\n\n    def test_invalid_inputs_raise(self) -> None:\n        \"\"\"CR/LF should be rejected when setting a value.\"\"\"\n        with self.assertRaises(ValueError):\n            CrossOriginEmbedderPolicy().value(\"bad\\rvalue\")\n\n    def test_deterministic_output(self) -> None:\n        \"\"\"Calling helpers repeatedly should yield the same header text.\"\"\"\n        first = CrossOriginEmbedderPolicy().credentialless()\n        second = CrossOriginEmbedderPolicy().credentialless().credentialless()\n        self.assertEqual(first.header_value, second.header_value)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_cross_origin_opener_policy.py",
    "content": "import unittest\n\nfrom secure.headers import CrossOriginOpenerPolicy\n\n\nclass TestCrossOriginOpenerPolicy(unittest.TestCase):\n    def test_default_coop(self):\n        \"\"\"Test default Cross-Origin-Opener-Policy header.\"\"\"\n        coop = CrossOriginOpenerPolicy()\n        self.assertEqual(coop.header_value, \"same-origin\")\n\n    def test_set_custom_policy(self):\n        \"\"\"Test setting a custom value for COOP.\"\"\"\n        coop = CrossOriginOpenerPolicy().set(\"custom-policy\")\n        self.assertEqual(coop.header_value, \"custom-policy\")\n\n    def test_same_origin(self):\n        \"\"\"Test setting the COOP to 'same-origin'.\"\"\"\n        coop = CrossOriginOpenerPolicy().same_origin()\n        self.assertEqual(coop.header_value, \"same-origin\")\n\n    def test_same_origin_allow_popups(self):\n        \"\"\"Test setting the COOP to 'same-origin-allow-popups'.\"\"\"\n        coop = CrossOriginOpenerPolicy().same_origin_allow_popups()\n        self.assertEqual(coop.header_value, \"same-origin-allow-popups\")\n\n    def test_unsafe_none(self):\n        \"\"\"Test setting the COOP to 'unsafe-none'.\"\"\"\n        coop = CrossOriginOpenerPolicy().unsafe_none()\n        self.assertEqual(coop.header_value, \"unsafe-none\")\n\n    def test_clear_policy(self):\n        \"\"\"Test resetting the COOP to its default value.\"\"\"\n        coop = CrossOriginOpenerPolicy().set(\"custom-policy\").clear()\n        self.assertEqual(coop.header_value, \"same-origin\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_custom_header.py",
    "content": "import unittest\n\nfrom secure.headers import CustomHeader\n\n\nclass TestCustomHeader(unittest.TestCase):\n    def test_custom_header_initialization(self):\n        \"\"\"Test initialization of a custom header with name and value.\"\"\"\n        custom_header = CustomHeader(\"X-Custom-Header\", \"custom-value\")\n        self.assertEqual(custom_header.header_name, \"X-Custom-Header\")\n        self.assertEqual(custom_header.header_value, \"custom-value\")\n\n    def test_set_custom_header_value(self):\n        \"\"\"Test setting a new value for the custom header.\"\"\"\n        custom_header = CustomHeader(\"X-Custom-Header\", \"initial-value\")\n        custom_header.set(\"updated-value\")\n        self.assertEqual(custom_header.header_value, \"updated-value\")\n\n    def test_method_chaining(self):\n        \"\"\"Test method chaining while setting a new value for the custom header.\"\"\"\n        custom_header = CustomHeader(\"X-Custom-Header\", \"initial-value\").set(\"new-value\")\n        self.assertEqual(custom_header.header_value, \"new-value\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_header_contracts_extended.py",
    "content": "from collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import ClassVar\nimport unittest\n\nfrom secure.headers import (\n    CacheControl,\n    ContentSecurityPolicy,\n    CrossOriginEmbedderPolicy,\n    CrossOriginOpenerPolicy,\n    CrossOriginResourcePolicy,\n    CustomHeader,\n    PermissionsPolicy,\n    ReferrerPolicy,\n    Server,\n    StrictTransportSecurity,\n    XContentTypeOptions,\n    XDnsPrefetchControl,\n    XFrameOptions,\n    XPermittedCrossDomainPolicies,\n)\nfrom secure.headers.base_header import BaseHeader, HeaderDefaultValue, HeaderName\n\n\n@dataclass(frozen=True)\nclass HeaderSpec:\n    name: str\n    factory: Callable[[], BaseHeader]\n    default: str\n    expected_header_name: str\n    builder: Callable[[], BaseHeader]\n    builder_expected: str\n    invalid: Callable[[], BaseHeader]\n    deterministic: tuple[Callable[[], BaseHeader], Callable[[], BaseHeader]]\n    supports_clear: bool = True\n\n\nclass TestHeaderContracts(unittest.TestCase):\n    HEADER_SPECS: ClassVar[tuple[HeaderSpec, ...]] = (\n        HeaderSpec(\n            name=\"CacheControl\",\n            factory=lambda: CacheControl(),\n            default=HeaderDefaultValue.CACHE_CONTROL.value,\n            expected_header_name=HeaderName.CACHE_CONTROL.value,\n            builder=lambda: CacheControl().no_cache().max_age(60),\n            builder_expected=\"no-cache, max-age=60\",\n            invalid=lambda: CacheControl().max_age(-1),\n            deterministic=(\n                lambda: CacheControl().no_cache().max_age(60),\n                lambda: CacheControl().no_cache().max_age(60).no_cache(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"ContentSecurityPolicy\",\n            factory=lambda: ContentSecurityPolicy(),\n            default=HeaderDefaultValue.CONTENT_SECURITY_POLICY.value,\n            expected_header_name=HeaderName.CONTENT_SECURITY_POLICY.value,\n            builder=lambda: ContentSecurityPolicy().default_src(\"'none'\").script_src(\"'self'\").img_src(\"'self'\"),\n            builder_expected=\"default-src 'none'; script-src 'self'; img-src 'self'\",\n            invalid=lambda: ContentSecurityPolicy().custom_directive(\"invalid name!\", \"'self'\"),\n            deterministic=(\n                lambda: ContentSecurityPolicy().script_src(\"'self'\"),\n                lambda: ContentSecurityPolicy().script_src(\"'self'\").script_src(\"'self'\"),\n            ),\n        ),\n        HeaderSpec(\n            name=\"CrossOriginEmbedderPolicy\",\n            factory=lambda: CrossOriginEmbedderPolicy(),\n            default=HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value,\n            expected_header_name=HeaderName.CROSS_ORIGIN_EMBEDDER_POLICY.value,\n            builder=lambda: CrossOriginEmbedderPolicy().credentialless(),\n            builder_expected=\"credentialless\",\n            invalid=lambda: CrossOriginEmbedderPolicy().set(\"bad\\nvalue\"),\n            deterministic=(\n                lambda: CrossOriginEmbedderPolicy().require_corp(),\n                lambda: CrossOriginEmbedderPolicy().require_corp().require_corp(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"CrossOriginOpenerPolicy\",\n            factory=lambda: CrossOriginOpenerPolicy(),\n            default=HeaderDefaultValue.CROSS_ORIGIN_OPENER_POLICY.value,\n            expected_header_name=HeaderName.CROSS_ORIGIN_OPENER_POLICY.value,\n            builder=lambda: CrossOriginOpenerPolicy().same_origin_allow_popups(),\n            builder_expected=\"same-origin-allow-popups\",\n            invalid=lambda: CrossOriginOpenerPolicy().value(\"bad\\rvalue\"),\n            deterministic=(\n                lambda: CrossOriginOpenerPolicy().same_origin(),\n                lambda: CrossOriginOpenerPolicy().same_origin().same_origin(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"CrossOriginResourcePolicy\",\n            factory=lambda: CrossOriginResourcePolicy(),\n            default=HeaderDefaultValue.CROSS_ORIGIN_RESOURCE_POLICY.value,\n            expected_header_name=HeaderName.CROSS_ORIGIN_RESOURCE_POLICY.value,\n            builder=lambda: CrossOriginResourcePolicy().same_site(),\n            builder_expected=\"same-site\",\n            invalid=lambda: CrossOriginResourcePolicy().value(\"bad\\nvalue\"),\n            deterministic=(\n                lambda: CrossOriginResourcePolicy().same_origin(),\n                lambda: CrossOriginResourcePolicy().same_origin().same_origin(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"CustomHeader\",\n            factory=lambda: CustomHeader(\"X-Test\", \"initial\"),\n            default=\"initial\",\n            expected_header_name=\"X-Test\",\n            builder=lambda: CustomHeader(\"X-Test\", \"initial\").set(\"updated\"),\n            builder_expected=\"updated\",\n            invalid=lambda: CustomHeader(\"Bad\\rName\", \"value\"),\n            deterministic=(\n                lambda: CustomHeader(\"X-Test\", \"value\"),\n                lambda: CustomHeader(\"X-Test\", \"value\").set(\"value\"),\n            ),\n            supports_clear=False,\n        ),\n        HeaderSpec(\n            name=\"PermissionsPolicy\",\n            factory=lambda: PermissionsPolicy(),\n            default=HeaderDefaultValue.PERMISSION_POLICY.value,\n            expected_header_name=HeaderName.PERMISSION_POLICY.value,\n            builder=lambda: PermissionsPolicy().camera(\"'self'\"),\n            builder_expected=\"camera=(self)\",\n            invalid=lambda: PermissionsPolicy().add_directive(\"bad name\", \"'self'\"),\n            deterministic=(\n                lambda: PermissionsPolicy().camera(\"'self'\"),\n                lambda: PermissionsPolicy().camera(\"'self'\").camera(\"'self'\"),\n            ),\n        ),\n        HeaderSpec(\n            name=\"ReferrerPolicy\",\n            factory=lambda: ReferrerPolicy(),\n            default=HeaderDefaultValue.REFERRER_POLICY.value,\n            expected_header_name=HeaderName.REFERRER_POLICY.value,\n            builder=lambda: ReferrerPolicy().fallback(\"no-referrer\", \"origin\"),\n            builder_expected=\"no-referrer, origin\",\n            invalid=lambda: ReferrerPolicy().add(\"no referrer\"),\n            deterministic=(\n                lambda: ReferrerPolicy().add(\"no-referrer\"),\n                lambda: ReferrerPolicy().add(\"no-referrer\").add(\"no-referrer\"),\n            ),\n        ),\n        HeaderSpec(\n            name=\"Server\",\n            factory=lambda: Server(),\n            default=HeaderDefaultValue.SERVER.value,\n            expected_header_name=HeaderName.SERVER.value,\n            builder=lambda: Server().set(\"CustomServer\"),\n            builder_expected=\"CustomServer\",\n            invalid=lambda: Server().set(\"bad\\nvalue\"),\n            deterministic=(\n                lambda: Server().set(\"CustomServer\"),\n                lambda: Server().set(\"CustomServer\").set(\"CustomServer\"),\n            ),\n        ),\n        HeaderSpec(\n            name=\"StrictTransportSecurity\",\n            factory=lambda: StrictTransportSecurity(),\n            default=HeaderDefaultValue.STRICT_TRANSPORT_SECURITY.value,\n            expected_header_name=HeaderName.STRICT_TRANSPORT_SECURITY.value,\n            builder=lambda: StrictTransportSecurity().max_age(172800).include_subdomains(),\n            builder_expected=\"max-age=172800; includeSubDomains\",\n            invalid=lambda: StrictTransportSecurity().max_age(-1),\n            deterministic=(\n                lambda: StrictTransportSecurity().max_age(60).include_subdomains(),\n                lambda: StrictTransportSecurity().max_age(60).include_subdomains().include_subdomains(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"XContentTypeOptions\",\n            factory=lambda: XContentTypeOptions(),\n            default=HeaderDefaultValue.X_CONTENT_TYPE_OPTIONS.value,\n            expected_header_name=HeaderName.X_CONTENT_TYPE_OPTIONS.value,\n            builder=lambda: XContentTypeOptions().nosniff(),\n            builder_expected=\"nosniff\",\n            invalid=lambda: XContentTypeOptions().set(\"bad\\rvalue\"),\n            deterministic=(\n                lambda: XContentTypeOptions().nosniff(),\n                lambda: XContentTypeOptions().nosniff().nosniff(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"XDnsPrefetchControl\",\n            factory=lambda: XDnsPrefetchControl(),\n            default=HeaderDefaultValue.X_DNS_PREFETCH_CONTROL.value,\n            expected_header_name=HeaderName.X_DNS_PREFETCH_CONTROL.value,\n            builder=lambda: XDnsPrefetchControl().on(),\n            builder_expected=\"on\",\n            invalid=lambda: XDnsPrefetchControl().set(\"bad\\nvalue\"),\n            deterministic=(\n                lambda: XDnsPrefetchControl().on(),\n                lambda: XDnsPrefetchControl().on().on(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"XFrameOptions\",\n            factory=lambda: XFrameOptions(),\n            default=HeaderDefaultValue.X_FRAME_OPTIONS.value,\n            expected_header_name=HeaderName.X_FRAME_OPTIONS.value,\n            builder=lambda: XFrameOptions().allow_from(\"https://example.com\"),\n            builder_expected=\"ALLOW-FROM https://example.com\",\n            invalid=lambda: XFrameOptions().value(\"bad\\rvalue\"),\n            deterministic=(\n                lambda: XFrameOptions().deny(),\n                lambda: XFrameOptions().deny().deny(),\n            ),\n        ),\n        HeaderSpec(\n            name=\"XPermittedCrossDomainPolicies\",\n            factory=lambda: XPermittedCrossDomainPolicies(),\n            default=HeaderDefaultValue.X_PERMITTED_CROSS_DOMAIN_POLICIES.value,\n            expected_header_name=HeaderName.X_PERMITTED_CROSS_DOMAIN_POLICIES.value,\n            builder=lambda: XPermittedCrossDomainPolicies().all(),\n            builder_expected=\"all\",\n            invalid=lambda: XPermittedCrossDomainPolicies().policy(\"unsupported\"),  # type: ignore[arg-type]\n            deterministic=(\n                lambda: XPermittedCrossDomainPolicies().none(),\n                lambda: XPermittedCrossDomainPolicies().none().none(),\n            ),\n        ),\n    )\n\n    def test_default_values(self) -> None:\n        for spec in self.HEADER_SPECS:\n            with self.subTest(header=spec.name):\n                header = spec.factory()\n                self.assertEqual(header.header_value, spec.default)\n\n    def test_header_names(self) -> None:\n        for spec in self.HEADER_SPECS:\n            with self.subTest(header=spec.name):\n                header = spec.factory()\n                self.assertEqual(header.header_name, spec.expected_header_name)\n\n    def test_clear_resets_default(self) -> None:\n        for spec in self.HEADER_SPECS:\n            if not spec.supports_clear:\n                continue\n            with self.subTest(header=spec.name):\n                header = spec.builder()\n                header.clear()\n                self.assertEqual(header.header_value, spec.default)\n\n    def test_builder_helpers(self) -> None:\n        for spec in self.HEADER_SPECS:\n            with self.subTest(header=spec.name):\n                header = spec.builder()\n                self.assertEqual(header.header_value, spec.builder_expected)\n\n    def test_invalid_inputs_raise(self) -> None:\n        for spec in self.HEADER_SPECS:\n            with self.subTest(header=spec.name), self.assertRaises(ValueError):\n                spec.invalid()\n\n    def test_deterministic_output(self) -> None:\n        for spec in self.HEADER_SPECS:\n            first_fn, second_fn = spec.deterministic\n            with self.subTest(header=spec.name):\n                first = first_fn()\n                second = second_fn()\n                self.assertEqual(first.header_value, second.header_value)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_permissions_policy.py",
    "content": "import unittest\n\nfrom secure.headers import PermissionsPolicy\n\n\nclass TestPermissionsPolicy(unittest.TestCase):\n    def test_default_permissions_policy(self):\n        \"\"\"Test the default Permissions-Policy header.\"\"\"\n        policy = PermissionsPolicy()\n        self.assertEqual(\n            policy.header_value,\n            \"geolocation=(), microphone=(), camera=()\",\n        )\n\n    def test_custom_permissions_policy(self):\n        \"\"\"Test setting custom directives in Permissions-Policy.\"\"\"\n        policy = PermissionsPolicy().camera(\"'self'\").geolocation(\"'none'\")\n        self.assertEqual(policy.header_value, \"camera=(self), geolocation=()\")\n\n    def test_clear_permissions_policy(self):\n        \"\"\"Test clearing all directives in Permissions-Policy.\"\"\"\n        policy = PermissionsPolicy().camera(\"'self'\").clear()\n        self.assertEqual(\n            policy.header_value,\n            \"geolocation=(), microphone=(), camera=()\",\n        )\n\n    def test_add_directive(self):\n        \"\"\"Test adding a specific directive to Permissions-Policy.\"\"\"\n        policy = PermissionsPolicy().add_directive(\"microphone\", \"'self'\")\n        self.assertIn(\"microphone=(self)\", policy.header_value)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_referrer_policy.py",
    "content": "import unittest\n\nfrom secure.headers import ReferrerPolicy\n\n\nclass TestReferrerPolicy(unittest.TestCase):\n    def test_default_referrer_policy(self):\n        \"\"\"Test default Referrer-Policy header.\"\"\"\n        referrer_policy = ReferrerPolicy()\n        self.assertEqual(referrer_policy.header_value, \"strict-origin-when-cross-origin\")\n\n    def test_set_custom_policy(self):\n        \"\"\"Test setting a custom referrer policy.\"\"\"\n        referrer_policy = ReferrerPolicy().set(\"no-referrer\")\n        self.assertEqual(referrer_policy.header_value, \"no-referrer\")\n\n    def test_no_referrer(self):\n        \"\"\"Test setting the Referrer-Policy to 'no-referrer'.\"\"\"\n        referrer_policy = ReferrerPolicy().no_referrer()\n        self.assertEqual(referrer_policy.header_value, \"no-referrer\")\n\n    def test_no_referrer_when_downgrade(self):\n        \"\"\"Test setting the Referrer-Policy to 'no-referrer-when-downgrade'.\"\"\"\n        referrer_policy = ReferrerPolicy().no_referrer_when_downgrade()\n        self.assertEqual(referrer_policy.header_value, \"no-referrer-when-downgrade\")\n\n    def test_origin(self):\n        \"\"\"Test setting the Referrer-Policy to 'origin'.\"\"\"\n        referrer_policy = ReferrerPolicy().origin()\n        self.assertEqual(referrer_policy.header_value, \"origin\")\n\n    def test_origin_when_cross_origin(self):\n        \"\"\"Test setting the Referrer-Policy to 'origin-when-cross-origin'.\"\"\"\n        referrer_policy = ReferrerPolicy().origin_when_cross_origin()\n        self.assertEqual(referrer_policy.header_value, \"origin-when-cross-origin\")\n\n    def test_same_origin(self):\n        \"\"\"Test setting the Referrer-Policy to 'same-origin'.\"\"\"\n        referrer_policy = ReferrerPolicy().same_origin()\n        self.assertEqual(referrer_policy.header_value, \"same-origin\")\n\n    def test_strict_origin(self):\n        \"\"\"Test setting the Referrer-Policy to 'strict-origin'.\"\"\"\n        referrer_policy = ReferrerPolicy().strict_origin()\n        self.assertEqual(referrer_policy.header_value, \"strict-origin\")\n\n    def test_strict_origin_when_cross_origin(self):\n        \"\"\"Test setting the Referrer-Policy to 'strict-origin-when-cross-origin'.\"\"\"\n        referrer_policy = ReferrerPolicy().strict_origin_when_cross_origin()\n        self.assertEqual(referrer_policy.header_value, \"strict-origin-when-cross-origin\")\n\n    def test_unsafe_url(self):\n        \"\"\"Test setting the Referrer-Policy to 'unsafe-url'.\"\"\"\n        referrer_policy = ReferrerPolicy().unsafe_url()\n        self.assertEqual(referrer_policy.header_value, \"unsafe-url\")\n\n    def test_clear_policy(self):\n        \"\"\"Test clearing the referrer policy directives and resetting to default.\"\"\"\n        referrer_policy = ReferrerPolicy().set(\"custom-policy\").clear()\n        self.assertEqual(referrer_policy.header_value, \"strict-origin-when-cross-origin\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_server.py",
    "content": "import unittest\n\nfrom secure.headers import Server\n\n\nclass TestServerHeader(unittest.TestCase):\n    def test_default_server(self):\n        \"\"\"Test default Server header value.\"\"\"\n        server_header = Server()\n        self.assertEqual(server_header.header_value, \"\")\n\n    def test_set_custom_server(self):\n        \"\"\"Test setting a custom Server header value.\"\"\"\n        server_header = Server().set(\"CustomServer\")\n        self.assertEqual(server_header.header_value, \"CustomServer\")\n\n    def test_clear_server(self):\n        \"\"\"Test clearing the Server header to default value (NULL).\"\"\"\n        server_header = Server().set(\"CustomServer\").clear()\n        self.assertEqual(server_header.header_value, \"\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_strict_transport_security.py",
    "content": "import unittest\n\nfrom secure.headers import StrictTransportSecurity\n\n\nclass TestStrictTransportSecurity(unittest.TestCase):\n    def test_default_hsts(self):\n        \"\"\"Test default Strict-Transport-Security header.\"\"\"\n        hsts = StrictTransportSecurity()\n        self.assertEqual(hsts.header_name, \"Strict-Transport-Security\")\n        self.assertEqual(hsts.header_value, \"max-age=31536000\")\n\n    def test_custom_max_age(self):\n        \"\"\"Test setting a custom max-age for HSTS.\"\"\"\n        hsts = StrictTransportSecurity().max_age(31536000)\n        self.assertEqual(hsts.header_value, \"max-age=31536000\")\n\n    def test_preload(self):\n        \"\"\"Test adding preload directive to HSTS.\"\"\"\n        hsts = StrictTransportSecurity().preload()\n        self.assertIn(\"preload\", hsts.header_value)\n\n    def test_include_subdomains(self):\n        \"\"\"Test adding includeSubDomains directive to HSTS.\"\"\"\n        hsts = StrictTransportSecurity().max_age(31536000).include_subdomains()\n        self.assertIn(\"includeSubDomains\", hsts.header_value)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_x_content_type_options.py",
    "content": "import unittest\n\nfrom secure.headers import XContentTypeOptions\n\n\nclass TestXContentTypeOptions(unittest.TestCase):\n    def test_default_x_content_type_options(self):\n        \"\"\"Test default X-Content-Type-Options header.\"\"\"\n        x_content_type_options = XContentTypeOptions()\n        self.assertEqual(x_content_type_options.header_value, \"nosniff\")\n\n    def test_set_custom_value(self):\n        \"\"\"Test setting a custom value for X-Content-Type-Options.\"\"\"\n        x_content_type_options = XContentTypeOptions().set(\"custom-value\")\n        self.assertEqual(x_content_type_options.header_value, \"custom-value\")\n\n    def test_nosniff(self):\n        \"\"\"Test setting the X-Content-Type-Options to 'nosniff'.\"\"\"\n        x_content_type_options = XContentTypeOptions().nosniff()\n        self.assertEqual(x_content_type_options.header_value, \"nosniff\")\n\n    def test_clear(self):\n        \"\"\"Test clearing and resetting the X-Content-Type-Options to default.\"\"\"\n        x_content_type_options = XContentTypeOptions().set(\"custom-value\").clear()\n        self.assertEqual(x_content_type_options.header_value, \"nosniff\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/headers/test_x_frame_options.py",
    "content": "import unittest\n\nfrom secure.headers import XFrameOptions\n\n\nclass TestXFrameOptions(unittest.TestCase):\n    def test_default_x_frame_options(self):\n        \"\"\"Test default X-Frame-Options value (SAMEORIGIN).\"\"\"\n        xfo = XFrameOptions()\n        self.assertEqual(xfo.header_value, \"SAMEORIGIN\")\n\n    def test_set_deny(self):\n        \"\"\"Test setting X-Frame-Options to DENY.\"\"\"\n        xfo = XFrameOptions().deny()\n        self.assertEqual(xfo.header_value, \"DENY\")\n\n    def test_set_sameorigin(self):\n        \"\"\"Test explicitly setting X-Frame-Options to SAMEORIGIN.\"\"\"\n        xfo = XFrameOptions().sameorigin()\n        self.assertEqual(xfo.header_value, \"SAMEORIGIN\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/middleware/test_middleware.py",
    "content": "import asyncio\nfrom typing import Any, Literal, TypedDict, cast\n\nfrom secure import CustomHeader, Secure\nfrom secure.middleware import SecureASGIMiddleware, SecureWSGIMiddleware\n\n\ndef _find_header_values(headers: list[tuple[str, str]], name: str) -> list[str]:\n    lc = name.lower()\n    return [value for key, value in headers if key.lower() == lc]\n\n\nclass _CapturedWSGI(TypedDict):\n    status: str\n    headers: list[tuple[str, str]]\n    exc_info: Any\n    body: bytes\n\n\ndef _run_wsgi_with_middleware(middleware) -> _CapturedWSGI:\n    captured: _CapturedWSGI = {\n        \"status\": \"\",\n        \"headers\": [],\n        \"exc_info\": None,\n        \"body\": b\"\",\n    }\n\n    def start_response(\n        status: str,\n        headers: list[tuple[str, str]],\n        exc_info: Any = None,\n    ):\n        captured[\"status\"] = status\n        captured[\"headers\"] = headers\n        captured[\"exc_info\"] = exc_info\n        return lambda _: None\n\n    body = list(middleware({}, start_response))\n    captured[\"body\"] = b\"\".join(body)\n    return captured\n\n\ndef test_wsgi_overwrites_existing_header():\n    secure_headers = Secure.with_default_headers()\n    app_headers = [(\"X-Frame-Options\", \"DENY\")]\n\n    def app(environ, start_response):\n        start_response(\"200 OK\", app_headers)\n        return [b\"ok\"]\n\n    middleware = SecureWSGIMiddleware(app, secure=secure_headers)\n    captured = _run_wsgi_with_middleware(middleware)\n\n    xfo_values = _find_header_values(captured[\"headers\"], \"x-frame-options\")\n    assert xfo_values == [secure_headers.headers[\"X-Frame-Options\"]]\n\n\ndef test_wsgi_multi_ok_appends_existing_header():\n    secure_headers = Secure.with_default_headers()\n    app_headers = [(\"Content-Security-Policy\", \"app-csp\")]\n\n    def app(environ, start_response):\n        start_response(\"200 OK\", app_headers)\n        return [b\"ok\"]\n\n    middleware = SecureWSGIMiddleware(app, secure=secure_headers)\n    captured = _run_wsgi_with_middleware(middleware)\n\n    csp_values = _find_header_values(captured[\"headers\"], \"content-security-policy\")\n    assert csp_values == [\"app-csp\", secure_headers.headers[\"Content-Security-Policy\"]]\n\n\ndef test_wsgi_preserves_status_and_extra_headers():\n    secure_headers = Secure.with_default_headers()\n    app_headers = [(\"X-Custom\", \"keep\")]\n\n    def app(environ, start_response):\n        start_response(\"200 OK\", app_headers)\n        return [b\"ok\"]\n\n    middleware = SecureWSGIMiddleware(app, secure=secure_headers)\n    captured = _run_wsgi_with_middleware(middleware)\n\n    assert captured[\"status\"] == \"200 OK\"\n    assert _find_header_values(captured[\"headers\"], \"x-custom\") == [\"keep\"]\n\n\ndef _headers_by_name_bytes(headers: list[tuple[bytes, bytes]], name: bytes) -> list[bytes]:\n    lc = name.lower()\n    return [value for key, value in headers if key.lower() == lc]\n\n\ndef _http_app(response_headers: list[tuple[bytes, bytes]]):\n    async def app(scope, receive, send):\n        await send(\n            {\n                \"type\": \"http.response.start\",\n                \"status\": 200,\n                \"headers\": response_headers,\n            }\n        )\n        await send({\"type\": \"http.response.body\", \"body\": b\"\", \"more_body\": False})\n\n    return app\n\n\nclass _HTTPResponseStart(TypedDict):\n    type: Literal[\"http.response.start\"]\n    status: int\n    headers: list[tuple[bytes, bytes]]\n\n\nasync def _run_asgi(scope: dict[str, Any], app) -> list[dict[str, Any]]:\n    messages: list[dict[str, Any]] = []\n\n    async def send(message: dict[str, Any]):\n        messages.append(message)\n\n    async def receive() -> dict[str, Any]:\n        return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n\n    await app(scope, receive, send)\n    return messages\n\n\ndef test_asgi_overwrites_numeric_headers():\n    secure_headers = Secure.with_default_headers()\n    default_xfo = secure_headers.headers[\"X-Frame-Options\"].encode(\"latin-1\")\n    app = _http_app(\n        [\n            (b\"x-frame-options\", b\"DENY\"),\n            (b\"x-custom\", b\"keep\"),\n        ]\n    )\n\n    middleware = SecureASGIMiddleware(app, secure=secure_headers)\n    messages = asyncio.run(_run_asgi({\"type\": \"http\"}, middleware))\n    start = cast(\n        \"_HTTPResponseStart\",\n        next(m for m in messages if m[\"type\"] == \"http.response.start\"),\n    )\n    headers = start[\"headers\"]\n\n    xfo_values = _headers_by_name_bytes(headers, b\"x-frame-options\")\n    assert xfo_values == [default_xfo]\n    assert _headers_by_name_bytes(headers, b\"x-custom\") == [b\"keep\"]\n\n\ndef test_asgi_multi_ok_appends_default_csp():\n    secure_headers = Secure.with_default_headers()\n    expected_csp = secure_headers.headers[\"Content-Security-Policy\"].encode(\"latin-1\")\n    app = _http_app([(b\"content-security-policy\", b\"app-csp\")])\n\n    middleware = SecureASGIMiddleware(app, secure=secure_headers)\n    messages = asyncio.run(_run_asgi({\"type\": \"http\"}, middleware))\n    start = cast(\n        \"_HTTPResponseStart\",\n        next(m for m in messages if m[\"type\"] == \"http.response.start\"),\n    )\n    headers = start[\"headers\"]\n\n    csp_values = _headers_by_name_bytes(headers, b\"content-security-policy\")\n    assert csp_values == [b\"app-csp\", expected_csp]\n\n\ndef test_asgi_ignores_non_http_scopes():\n    async def websocket_app(scope, receive, send):\n        await send({\"type\": \"websocket.accept\"})\n\n    middleware = SecureASGIMiddleware(websocket_app)\n    messages = asyncio.run(_run_asgi({\"type\": \"websocket\"}, middleware))\n\n    assert messages == [{\"type\": \"websocket.accept\"}]\n\n\ndef test_asgi_multi_ok_custom_header():\n    secure_headers = Secure(custom=[CustomHeader(\"X-Extra\", \"new\")])\n    app = _http_app([(b\"x-extra\", b\"existing\")])\n\n    middleware = SecureASGIMiddleware(app, secure=secure_headers, multi_ok=[\"x-extra\"])\n    messages = asyncio.run(_run_asgi({\"type\": \"http\"}, middleware))\n    start = cast(\n        \"_HTTPResponseStart\",\n        next(m for m in messages if m[\"type\"] == \"http.response.start\"),\n    )\n    headers = start[\"headers\"]\n\n    extra_values = _headers_by_name_bytes(headers, b\"x-extra\")\n    assert extra_values == [b\"existing\", b\"new\"]\n"
  },
  {
    "path": "tests/middleware/test_secure_protocols.py",
    "content": "import asyncio\nfrom typing import TYPE_CHECKING\nimport unittest\n\nfrom secure import CustomHeader, Secure\n\nif TYPE_CHECKING:\n    from collections.abc import MutableMapping\n\n\nclass HeadersOnlyResponse:\n    def __init__(self) -> None:\n        self.headers: MutableMapping[str, str] = {}\n\n\nclass SetHeaderResponse:\n    def __init__(self) -> None:\n        self.calls: list[tuple[str, str]] = []\n\n    def set_header(self, key: str, value: str) -> None:\n        self.calls.append((key, value))\n\n\nclass AsyncSetHeaderResponse:\n    def __init__(self) -> None:\n        self.calls: list[tuple[str, str]] = []\n\n    async def set_header(self, key: str, value: str) -> None:\n        self.calls.append((key, value))\n\n\nclass AsyncOnlySetHeader:\n    async def set_header(self, key: str, value: str) -> None:\n        pass\n\n\nclass TestSetHeaders(unittest.TestCase):\n    def test_headers_mapping_path_applies_headers(self) -> None:\n        secure_headers = Secure(custom=[CustomHeader(\"X-Test\", \"value\")])\n        response = HeadersOnlyResponse()\n\n        secure_headers.set_headers(response)\n\n        self.assertEqual(response.headers[\"X-Test\"], \"value\")\n\n    def test_set_headers_prefers_set_header_method(self) -> None:\n        secure_headers = Secure(custom=[CustomHeader(\"X-Method\", \"value\")])\n        response = SetHeaderResponse()\n\n        secure_headers.set_headers(response)\n\n        self.assertEqual(response.calls, [(\"X-Method\", \"value\")])\n\n    def test_set_headers_async_accepts_async_set_header(self) -> None:\n        secure_headers = Secure(custom=[CustomHeader(\"X-Async\", \"value\")])\n        response = AsyncSetHeaderResponse()\n\n        asyncio.run(secure_headers.set_headers_async(response))\n\n        self.assertEqual(response.calls, [(\"X-Async\", \"value\")])\n\n    def test_set_headers_rejects_async_method_in_sync_context(self) -> None:\n        secure_headers = Secure(custom=[CustomHeader(\"X-AsyncOnly\", \"value\")])\n\n        with self.assertRaises(RuntimeError):\n            secure_headers.set_headers(AsyncOnlySetHeader())\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/secure_tests/__init__.py",
    "content": "\"\"\"Test suite for the top-level ``secure`` package.\"\"\"\n"
  },
  {
    "path": "tests/secure_tests/test_exports.py",
    "content": "import unittest\n\nimport secure\nimport secure.headers\nimport secure.middleware\n\n\nclass TestExportSurface(unittest.TestCase):\n    def test_secure_package_exports(self) -> None:\n        expected = {\n            \"COMMA_JOIN_OK\",\n            \"DEFAULT_ALLOWED_HEADERS\",\n            \"MULTI_OK\",\n            \"CacheControl\",\n            \"ContentSecurityPolicy\",\n            \"CrossOriginEmbedderPolicy\",\n            \"CrossOriginOpenerPolicy\",\n            \"CrossOriginResourcePolicy\",\n            \"CustomHeader\",\n            \"PermissionsPolicy\",\n            \"Preset\",\n            \"ReferrerPolicy\",\n            \"Secure\",\n            \"Server\",\n            \"StrictTransportSecurity\",\n            \"XDnsPrefetchControl\",\n            \"XContentTypeOptions\",\n            \"XFrameOptions\",\n            \"XPermittedCrossDomainPolicies\",\n        }\n\n        self.assertEqual(set(secure.__all__), expected)\n        for name in expected:\n            with self.subTest(export=name):\n                self.assertTrue(hasattr(secure, name))\n\n    def test_headers_package_exports(self) -> None:\n        expected = {\n            \"BaseHeader\",\n            \"CacheControl\",\n            \"ContentSecurityPolicy\",\n            \"CrossOriginEmbedderPolicy\",\n            \"CrossOriginOpenerPolicy\",\n            \"CrossOriginResourcePolicy\",\n            \"CustomHeader\",\n            \"PermissionsPolicy\",\n            \"ReferrerPolicy\",\n            \"Server\",\n            \"StrictTransportSecurity\",\n            \"XContentTypeOptions\",\n            \"XDnsPrefetchControl\",\n            \"XFrameOptions\",\n            \"XPermittedCrossDomainPolicies\",\n        }\n\n        self.assertEqual(set(secure.headers.__all__), expected)\n        for name in expected:\n            with self.subTest(header=name):\n                self.assertTrue(hasattr(secure.headers, name))\n\n    def test_middleware_package_exports(self) -> None:\n        expected = {\"SecureASGIMiddleware\", \"SecureWSGIMiddleware\"}\n\n        self.assertEqual(set(secure.middleware.__all__), expected)\n        for name in expected:\n            with self.subTest(middleware=name):\n                self.assertTrue(hasattr(secure.middleware, name))\n"
  },
  {
    "path": "tests/secure_tests/test_headers.py",
    "content": "import unittest\n\nfrom secure.headers import (\n    CacheControl,\n    ContentSecurityPolicy,\n    CrossOriginEmbedderPolicy,\n    CrossOriginOpenerPolicy,\n    CrossOriginResourcePolicy,\n    CustomHeader,\n    PermissionsPolicy,\n    ReferrerPolicy,\n    Server,\n    StrictTransportSecurity,\n    XContentTypeOptions,\n    XDnsPrefetchControl,\n    XFrameOptions,\n    XPermittedCrossDomainPolicies,\n)\nfrom secure.headers.base_header import HeaderDefaultValue\n\n\nclass TestHeaderConsistency(unittest.TestCase):\n    def test_value_methods_reject_crlf(self) -> None:\n        \"\"\"Ensure every header's `value()` method rejects CR/LF injections.\"\"\"\n        factories = [\n            (\"CacheControl\", lambda: CacheControl()),\n            (\"ContentSecurityPolicy\", lambda: ContentSecurityPolicy()),\n            (\"CrossOriginEmbedderPolicy\", lambda: CrossOriginEmbedderPolicy()),\n            (\"CrossOriginOpenerPolicy\", lambda: CrossOriginOpenerPolicy()),\n            (\"CrossOriginResourcePolicy\", lambda: CrossOriginResourcePolicy()),\n            (\"PermissionsPolicy\", lambda: PermissionsPolicy()),\n            (\"ReferrerPolicy\", lambda: ReferrerPolicy()),\n            (\"Server\", lambda: Server()),\n            (\"StrictTransportSecurity\", lambda: StrictTransportSecurity()),\n            (\"XContentTypeOptions\", lambda: XContentTypeOptions()),\n            (\"XDnsPrefetchControl\", lambda: XDnsPrefetchControl()),\n            (\"XFrameOptions\", lambda: XFrameOptions()),\n            (\"XPermittedCrossDomainPolicies\", lambda: XPermittedCrossDomainPolicies()),\n            (\"CustomHeader\", lambda: CustomHeader(\"X-Test\", \"value\")),\n        ]\n\n        for name, factory in factories:\n            header = factory()\n            with self.subTest(header=name), self.assertRaises(ValueError):\n                header.value(\"bad\\rvalue\")\n\n    def test_clear_restores_default_values(self) -> None:\n        \"\"\"Each header should return to its library default after `clear()`.\"\"\"\n        cases = [\n            (CacheControl(), \"public, max-age=60\", HeaderDefaultValue.CACHE_CONTROL.value),\n            (\n                ContentSecurityPolicy(),\n                \"default-src 'self'\",\n                HeaderDefaultValue.CONTENT_SECURITY_POLICY.value,\n            ),\n            (CrossOriginEmbedderPolicy(), \"unsafe-none\", HeaderDefaultValue.CROSS_ORIGIN_EMBEDDER_POLICY.value),\n            (CrossOriginOpenerPolicy(), \"unsafe-none\", HeaderDefaultValue.CROSS_ORIGIN_OPENER_POLICY.value),\n            (CrossOriginResourcePolicy(), \"cross-origin\", HeaderDefaultValue.CROSS_ORIGIN_RESOURCE_POLICY.value),\n            (PermissionsPolicy(), \"geolocation=()\", HeaderDefaultValue.PERMISSION_POLICY.value),\n            (ReferrerPolicy(), \"no-referrer\", HeaderDefaultValue.REFERRER_POLICY.value),\n            (Server(), \"Custom\", HeaderDefaultValue.SERVER.value),\n            (StrictTransportSecurity(), \"max-age=0\", HeaderDefaultValue.STRICT_TRANSPORT_SECURITY.value),\n            (XContentTypeOptions(), \"detect\", HeaderDefaultValue.X_CONTENT_TYPE_OPTIONS.value),\n            (XDnsPrefetchControl(), \"on\", HeaderDefaultValue.X_DNS_PREFETCH_CONTROL.value),\n            (XFrameOptions(), \"DENY\", HeaderDefaultValue.X_FRAME_OPTIONS.value),\n            (XPermittedCrossDomainPolicies(), \"all\", HeaderDefaultValue.X_PERMITTED_CROSS_DOMAIN_POLICIES.value),\n        ]\n\n        for header, value, expected in cases:\n            header.value(value)\n            header.clear()\n            with self.subTest(header=header.__class__.__name__):\n                self.assertEqual(header.header_value, expected)\n\n    def test_cache_control_canonical_order(self) -> None:\n        \"\"\"Canonical ordering should be stable regardless of helper call order.\"\"\"\n        cc = CacheControl().public().max_age(60).no_cache()\n        self.assertEqual(cc.header_value, \"no-cache, public, max-age=60\")\n\n    def test_content_security_policy_values_deduplicate(self) -> None:\n        \"\"\"Duplicate tokens for the same directive should be ignored.\"\"\"\n        csp = ContentSecurityPolicy().default_src(\"'self'\").custom_directive(\"default-src\", \"'self'\", \"https:\")\n        self.assertEqual(csp.header_value, \"default-src 'self' https:\")\n\n    def test_permissions_policy_wildcard_must_be_alone(self) -> None:\n        policy = PermissionsPolicy().camera(\"*\")\n        with self.assertRaises(ValueError):\n            policy.camera(\"*\", \"self\")\n\n    def test_referrer_policy_add_is_idempotent(self) -> None:\n        rp = ReferrerPolicy().add(\"no-referrer\").add(\"no-referrer\").add(\"same-origin\")\n        self.assertEqual(rp.header_value, \"no-referrer, same-origin\")\n\n    def test_xdns_prefetch_control_normalizes_values(self) -> None:\n        xdfc = XDnsPrefetchControl().set(\"ON\").set(\"OFF\")\n        self.assertEqual(xdfc.header_value, \"off\")\n        xdfc.on()\n        self.assertEqual(xdfc.header_value, \"on\")\n        xdfc.off()\n        self.assertEqual(xdfc.header_value, \"off\")\n\n    def test_x_permitted_policy_rejects_unknown(self) -> None:\n        with self.assertRaises(ValueError):\n            XPermittedCrossDomainPolicies().policy(\"unsupported\")  # type: ignore\n"
  },
  {
    "path": "tests/secure_tests/test_internal_helpers.py",
    "content": "import asyncio\nimport unittest\nfrom unittest import mock\n\nfrom secure import DEFAULT_ALLOWED_HEADERS, MULTI_OK\nfrom secure._internal.emit import set_headers_async\nfrom secure._internal.normalize import normalize_header_items\nfrom secure._internal.policy import allowlist_header_objects, deduplicate_header_objects\nfrom secure.headers import CustomHeader\n\n\nclass _AsyncHeadersMapping:\n    def __init__(self) -> None:\n        self.storage: dict[str, str] = {}\n\n    async def __setitem__(self, key: str, value: str) -> None:\n        self.storage[key] = value\n\n\nclass _AsyncHeadersResponse:\n    def __init__(self) -> None:\n        self.headers = _AsyncHeadersMapping()\n\n\nclass TestInternalHelpers(unittest.TestCase):\n    def test_normalize_header_items_warns_and_drops_invalid_names(self) -> None:\n        logger = mock.Mock()\n\n        items = normalize_header_items(\n            ((\"Bad Header\", \"value\"),),\n            on_invalid=\"warn\",\n            logger=logger,\n        )\n\n        self.assertEqual(dict(items), {})\n        logger.warning.assert_called_once_with(\"Invalid header name 'Bad Header' (RFC 7230 token required)\")\n\n    def test_normalize_header_items_preserves_obs_text_when_allowed(self) -> None:\n        items = normalize_header_items(\n            ((\"X-Obs-Text\", \"caf\\xe9\"),),\n            allow_obs_text=True,\n        )\n\n        self.assertEqual(items[\"X-Obs-Text\"], \"caf\\xe9\")\n\n    def test_allowlist_header_objects_warn_keeps_unexpected_headers(self) -> None:\n        logger = mock.Mock()\n        headers = [CustomHeader(\"X-App-Header\", \"value\")]\n\n        kept = allowlist_header_objects(\n            headers,\n            allowed=DEFAULT_ALLOWED_HEADERS,\n            on_unexpected=\"warn\",\n            logger=logger,\n        )\n\n        self.assertEqual([header.header_name for header in kept], [\"X-App-Header\"])\n        logger.warning.assert_called_once_with(\"Unexpected header %r kept (not in allowlist)\", \"X-App-Header\")\n\n    def test_deduplicate_header_objects_preserves_multi_ok_order(self) -> None:\n        headers = [\n            CustomHeader(\"Content-Security-Policy\", \"default-src 'self'\"),\n            CustomHeader(\"Content-Security-Policy\", \"report-uri /csp\"),\n        ]\n\n        deduplicated = deduplicate_header_objects(\n            headers,\n            action=\"raise\",\n            comma_join_ok=frozenset(),\n            multi_ok=MULTI_OK,\n        )\n\n        self.assertEqual(\n            [(header.header_name, header.header_value) for header in deduplicated],\n            [\n                (\"Content-Security-Policy\", \"default-src 'self'\"),\n                (\"Content-Security-Policy\", \"report-uri /csp\"),\n            ],\n        )\n\n    def test_set_headers_async_helper_awaits_async_headers_mapping(self) -> None:\n        response = _AsyncHeadersResponse()\n\n        asyncio.run(set_headers_async(response, ((\"X-Test\", \"value\"),)))\n\n        self.assertEqual(response.headers.storage, {\"X-Test\": \"value\"})\n"
  },
  {
    "path": "tests/secure_tests/test_secure.py",
    "content": "import asyncio\nfrom collections.abc import Awaitable, Callable, Generator\nimport unittest\n\nimport secure as secure_pkg\nfrom secure import (\n    ContentSecurityPolicy,\n    CustomHeader,\n    Preset,\n    Secure,\n    Server,\n    StrictTransportSecurity,\n)\nfrom secure.secure import (\n    COMMA_JOIN_OK,\n    DEFAULT_ALLOWED_HEADERS,\n    MULTI_OK,\n    HeaderSetError,\n)\n\n\nclass MockResponse:\n    def __init__(self) -> None:\n        self.headers: dict[str, str] = {}\n\n    def set_header(self, key: str, value: str) -> None:\n        \"\"\"A simple method to simulate the set_header method.\"\"\"\n        self.headers[key] = value\n\n\nclass MockResponseWithSetHeader:\n    def __init__(self) -> None:\n        self.headers: dict[str, str] = {}\n        self.header_storage: dict[str, str] = {}\n\n    def set_header(self, key: str, value: str) -> None:\n        \"\"\"Simulate set_header method.\"\"\"\n        self.header_storage[key] = value\n\n\nclass MockResponseAsyncSetHeader:\n    def __init__(self) -> None:\n        self.headers: dict[str, str] = {}\n        self.header_storage: dict[str, str] = {}\n\n    async def set_header(self, key: str, value: str) -> None:\n        \"\"\"Simulate async set_header method.\"\"\"\n        self.header_storage[key] = value\n\n\nclass MockResponseNoHeaders:\n    pass\n\n\nclass _AsyncHeadersMapping:\n    def __init__(self) -> None:\n        self.storage: dict[str, str] = {}\n\n    async def __setitem__(self, key: str, value: str) -> None:\n        self.storage[key] = value\n\n\nclass MockAsyncHeadersResponse:\n    def __init__(self) -> None:\n        self.headers = _AsyncHeadersMapping()\n\n    async def set_header(self, key: str, value: str) -> None:\n        \"\"\"Async set_header method for protocol compliance.\"\"\"\n        await self.headers.__setitem__(key, value)\n\n\nclass MockResponseRaiseSetHeader:\n    def set_header(self, key: str, value: str) -> None:\n        raise ValueError(\"boom\")\n\n\nclass MockResponseAwaitableSetHeader:\n    def __init__(self) -> None:\n        self.calls: list[tuple[str, str]] = []\n\n    def set_header(self, key: str, value: str) -> Awaitable[None]:\n        class _Awaitable:\n            def __init__(self, callback: Callable[[], None]) -> None:\n                self._callback = callback\n\n            def __await__(self) -> Generator[None, None, None]:\n                async def _run() -> None:\n                    self._callback()\n\n                return _run().__await__()\n\n        return _Awaitable(lambda: self.calls.append((key, value)))\n\n\ndef _expected_basic_csp_value() -> str:\n    \"\"\"Builder matching the CSP used by the BASIC preset.\"\"\"\n    return (\n        ContentSecurityPolicy()\n        .default_src(\"'self'\")\n        .base_uri(\"'self'\")\n        .font_src(\"'self'\", \"https:\", \"data:\")\n        .form_action(\"'self'\")\n        .frame_ancestors(\"'self'\")\n        .img_src(\"'self'\", \"data:\")\n        .object_src(\"'none'\")\n        .script_src(\"'self'\")\n        .script_src_attr(\"'none'\")\n        .style_src(\"'self'\", \"https:\", \"'unsafe-inline'\")\n        .upgrade_insecure_requests()\n    ).header_value\n\n\nclass TestSecure(unittest.TestCase):\n    def setUp(self) -> None:\n        # Initialize Secure with some test headers\n        self.secure = Secure(\n            custom=[\n                CustomHeader(\"X-Test-Header-1\", \"Value1\"),\n                CustomHeader(\"X-Test-Header-2\", \"Value2\"),\n            ]\n        )\n\n    def test_with_default_headers(self) -> None:\n        \"\"\"Test that the Balanced defaults are correctly applied.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponse()\n\n        secure_headers.set_headers(response)\n\n        self.assertNotIn(\"Cache-Control\", response.headers)\n\n        self.assertIn(\"Content-Security-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Content-Security-Policy\"],\n            _expected_basic_csp_value(),\n        )\n\n        self.assertIn(\"Cross-Origin-Opener-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Opener-Policy\"], \"same-origin\")\n\n        self.assertIn(\"Cross-Origin-Resource-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Resource-Policy\"], \"same-origin\")\n\n        self.assertIn(\"Permissions-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Permissions-Policy\"],\n            \"geolocation=(), microphone=(), camera=()\",\n        )\n\n        self.assertIn(\"Referrer-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Referrer-Policy\"], \"strict-origin-when-cross-origin\")\n\n        self.assertIn(\"Server\", response.headers)\n        self.assertEqual(response.headers[\"Server\"], \"\")\n\n        self.assertIn(\"Strict-Transport-Security\", response.headers)\n        self.assertEqual(\n            response.headers[\"Strict-Transport-Security\"],\n            \"max-age=31536000; includeSubDomains\",\n        )\n\n        self.assertIn(\"X-Content-Type-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Content-Type-Options\"], \"nosniff\")\n\n        self.assertIn(\"X-Frame-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Frame-Options\"], \"SAMEORIGIN\")\n\n        self.assertNotIn(\"Origin-Agent-Cluster\", response.headers)\n        self.assertNotIn(\"X-Download-Options\", response.headers)\n        self.assertNotIn(\"X-XSS-Protection\", response.headers)\n        self.assertNotIn(\"X-Permitted-Cross-Domain-Policies\", response.headers)\n        self.assertNotIn(\"X-DNS-Prefetch-Control\", response.headers)\n\n    def test_with_default_headers_matches_balanced_preset(self) -> None:\n        \"\"\"with_default_headers() should mirror the BALANCED preset.\"\"\"\n        balanced = Secure.from_preset(Preset.BALANCED)\n\n        self.assertEqual(Secure.with_default_headers().headers, balanced.headers)\n\n    def test_balanced_preset_omits_cache_control(self) -> None:\n        \"\"\"Balanced preset purposely excludes Cache-Control.\"\"\"\n        balanced_headers = Secure.from_preset(Preset.BALANCED).headers\n\n        self.assertNotIn(\"Cache-Control\", balanced_headers)\n\n    def test_from_preset_basic(self) -> None:\n        \"\"\"Test that the BASIC preset is applied correctly.\"\"\"\n        secure_headers = Secure.from_preset(Preset.BASIC)\n        response = MockResponse()\n\n        # Apply the headers to the response object\n        secure_headers.set_headers(response)\n\n        self.assertNotIn(\"Cache-Control\", response.headers)\n        self.assertNotIn(\"Permissions-Policy\", response.headers)\n        self.assertNotIn(\"Server\", response.headers)\n\n        self.assertIn(\"Content-Security-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Content-Security-Policy\"],\n            _expected_basic_csp_value(),\n        )\n\n        self.assertIn(\"Cross-Origin-Opener-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Opener-Policy\"], \"same-origin\")\n\n        self.assertIn(\"Cross-Origin-Resource-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Resource-Policy\"], \"same-origin\")\n\n        self.assertIn(\"Referrer-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Referrer-Policy\"], \"no-referrer\")\n\n        self.assertIn(\"Strict-Transport-Security\", response.headers)\n        self.assertEqual(\n            response.headers[\"Strict-Transport-Security\"],\n            \"max-age=31536000; includeSubDomains\",\n        )\n\n        self.assertIn(\"X-Content-Type-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Content-Type-Options\"], \"nosniff\")\n\n        self.assertIn(\"X-Frame-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Frame-Options\"], \"SAMEORIGIN\")\n\n        self.assertIn(\"X-Permitted-Cross-Domain-Policies\", response.headers)\n        self.assertEqual(response.headers[\"X-Permitted-Cross-Domain-Policies\"], \"none\")\n\n        self.assertIn(\"X-DNS-Prefetch-Control\", response.headers)\n        self.assertEqual(response.headers[\"X-DNS-Prefetch-Control\"], \"off\")\n\n        self.assertIn(\"Origin-Agent-Cluster\", response.headers)\n        self.assertEqual(response.headers[\"Origin-Agent-Cluster\"], \"?1\")\n\n        self.assertIn(\"X-Download-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Download-Options\"], \"noopen\")\n\n        self.assertIn(\"X-XSS-Protection\", response.headers)\n        self.assertEqual(response.headers[\"X-XSS-Protection\"], \"0\")\n\n    def test_from_preset_strict(self) -> None:\n        \"\"\"Test that the STRICT preset is applied correctly.\"\"\"\n        secure_headers = Secure.from_preset(Preset.STRICT)\n        response = MockResponse()\n\n        # Apply the headers to the response object\n        secure_headers.set_headers(response)\n\n        # Strict preset headers\n        self.assertIn(\"Cache-Control\", response.headers)\n        self.assertEqual(response.headers[\"Cache-Control\"], \"no-store, max-age=0\")\n\n        self.assertIn(\"Content-Security-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Content-Security-Policy\"],\n            (\n                \"default-src 'self'; script-src 'self'; style-src 'self'; \"\n                \"object-src 'none'; base-uri 'none'; frame-ancestors 'none'\"\n            ),\n        )\n\n        self.assertIn(\"Cross-Origin-Embedder-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Embedder-Policy\"], \"require-corp\")\n\n        self.assertIn(\"Cross-Origin-Opener-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Cross-Origin-Opener-Policy\"], \"same-origin\")\n\n        self.assertIn(\"Permissions-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Permissions-Policy\"],\n            \"geolocation=(), microphone=(), camera=()\",\n        )\n\n        self.assertIn(\"Referrer-Policy\", response.headers)\n        self.assertEqual(response.headers[\"Referrer-Policy\"], \"no-referrer\")\n\n        self.assertIn(\"Server\", response.headers)\n        self.assertEqual(response.headers[\"Server\"], \"\")\n\n        self.assertIn(\"Strict-Transport-Security\", response.headers)\n        self.assertEqual(\n            response.headers[\"Strict-Transport-Security\"],\n            \"max-age=63072000; includeSubDomains\",\n        )\n\n        self.assertIn(\"X-Content-Type-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Content-Type-Options\"], \"nosniff\")\n\n        self.assertIn(\"X-Frame-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Frame-Options\"], \"DENY\")\n\n    def test_custom_headers(self) -> None:\n        \"\"\"Test that custom headers are applied correctly.\"\"\"\n        custom_server = Server().set(\"SecureServer\")\n        custom_csp = ContentSecurityPolicy().default_src(\"'none'\").img_src(\"'self'\")\n\n        secure_headers = Secure(server=custom_server, csp=custom_csp)\n        response = MockResponse()\n\n        # Apply the custom headers\n        secure_headers.set_headers(response)\n\n        self.assertIn(\"Server\", response.headers)\n        self.assertEqual(response.headers[\"Server\"], \"SecureServer\")\n\n        self.assertIn(\"Content-Security-Policy\", response.headers)\n        self.assertEqual(\n            response.headers[\"Content-Security-Policy\"],\n            \"default-src 'none'; img-src 'self'\",\n        )\n\n    def test_async_set_headers(self) -> None:\n        \"\"\"Test that async setting headers works correctly.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponse()\n\n        async def mock_set_headers() -> None:\n            await secure_headers.set_headers_async(response)\n\n        asyncio.run(mock_set_headers())\n\n        # Verify that headers are set asynchronously\n        self.assertIn(\"Strict-Transport-Security\", response.headers)\n        self.assertEqual(\n            response.headers[\"Strict-Transport-Security\"],\n            \"max-age=31536000; includeSubDomains\",\n        )\n\n        self.assertIn(\"X-Content-Type-Options\", response.headers)\n        self.assertEqual(response.headers[\"X-Content-Type-Options\"], \"nosniff\")\n\n        # Additional assertions for other headers\n        self.assertIn(\"Content-Security-Policy\", response.headers)\n        self.assertIn(\"Cross-Origin-Opener-Policy\", response.headers)\n        self.assertIn(\"Permissions-Policy\", response.headers)\n        self.assertIn(\"Referrer-Policy\", response.headers)\n        self.assertIn(\"Server\", response.headers)\n        self.assertIn(\"X-Frame-Options\", response.headers)\n        self.assertNotIn(\"Cache-Control\", response.headers)\n\n    def test_set_headers_with_set_header_method(self) -> None:\n        \"\"\"Test setting headers on a response object with set_header method.\"\"\"\n        response = MockResponseWithSetHeader()\n        self.secure.set_headers(response)\n\n        # Verify that headers are set using set_header method\n        self.assertEqual(response.header_storage, self.secure.headers)\n        # Ensure set_header was called correct number of times\n        self.assertEqual(len(response.header_storage), len(self.secure.headers))\n\n    def test_set_headers_with_headers_dict(self) -> None:\n        \"\"\"Test set_headers with a response object that has a headers dictionary.\"\"\"\n        response = MockResponse()\n        self.secure.set_headers(response)\n\n        # Verify that headers are set\n        self.assertEqual(response.headers, self.secure.headers)\n\n    def test_set_headers_async_with_async_set_header(self) -> None:\n        \"\"\"Test set_headers_async with a response object that has an asynchronous set_header method.\"\"\"\n        response = MockResponseAsyncSetHeader()\n\n        async def test_async() -> None:\n            await self.secure.set_headers_async(response)\n\n        asyncio.run(test_async())\n\n        # Verify that headers are set using async set_header method\n        self.assertEqual(response.header_storage, self.secure.headers)\n        # Ensure set_header was called correct number of times\n        self.assertEqual(len(response.header_storage), len(self.secure.headers))\n\n    def test_set_headers_async_with_headers_dict(self) -> None:\n        \"\"\"Test set_headers_async with a response object that has a headers dictionary.\"\"\"\n        response = MockResponse()\n        asyncio.run(self.secure.set_headers_async(response))\n\n        # Verify that headers are set\n        self.assertEqual(response.headers, self.secure.headers)\n\n    def test_validate_and_normalize_headers_drops_invalid_entries(self) -> None:\n        \"\"\"Test that invalid headers are removed before emission.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Invalid-Header\", \"\\n\"),\n                CustomHeader(\"X-Valid-Header\", \"value\"),\n            ]\n        )\n        secure_headers.validate_and_normalize_headers()\n\n        response = MockResponse()\n        secure_headers.set_headers(response)\n\n        self.assertNotIn(\"X-Invalid-Header\", response.headers)\n        self.assertEqual(response.headers[\"X-Valid-Header\"], \"value\")\n\n    def test_validate_and_normalize_headers_applies_normalized_values(self) -> None:\n        \"\"\"Test that normalized headers drive both sync and async setters.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Test-Header\", \"value\\nwith\\r\\nbad\"),\n            ]\n        )\n        secure_headers.validate_and_normalize_headers()\n\n        response_sync = MockResponse()\n        secure_headers.set_headers(response_sync)\n        self.assertEqual(response_sync.headers[\"X-Test-Header\"], \"value with bad\")\n\n        response_async = MockResponse()\n        asyncio.run(secure_headers.set_headers_async(response_async))\n        self.assertEqual(response_async.headers[\"X-Test-Header\"], \"value with bad\")\n\n        self.assertEqual(secure_headers.headers[\"X-Test-Header\"], \"value with bad\")\n\n    def test_set_headers_missing_interface(self) -> None:\n        \"\"\"Test that an error is raised when response object lacks required methods.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponseNoHeaders()\n\n        with self.assertRaises(AttributeError) as context:\n            secure_headers.set_headers(response)  # type: ignore\n\n        self.assertIn(\n            \"does not support setting headers\",\n            str(context.exception),\n        )\n\n    def test_set_headers_with_async_set_header_in_sync_context(self) -> None:\n        \"\"\"Test set_headers raises RuntimeError when encountering async set_header in sync context.\"\"\"\n        response = MockResponseAsyncSetHeader()\n        with self.assertRaises(RuntimeError):\n            self.secure.set_headers(response)\n\n    def test_set_headers_overwrites_existing_headers(self) -> None:\n        \"\"\"Test that existing headers are overwritten by Secure.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponse()\n        response.headers[\"Referrer-Policy\"] = \"unsafe-url\"\n\n        # Apply the headers to the response object\n        secure_headers.set_headers(response)\n\n        # Verify that the header has been overwritten\n        self.assertEqual(response.headers[\"Referrer-Policy\"], \"strict-origin-when-cross-origin\")\n\n    def test_custom_header_inclusion(self) -> None:\n        \"\"\"Test that custom headers are included and applied.\"\"\"\n        custom_header = CustomHeader(\"X-Custom-Header\", \"CustomValue\")\n        secure_headers = Secure(custom=[custom_header])\n        response = MockResponse()\n\n        # Apply the headers to the response object\n        secure_headers.set_headers(response)\n\n        self.assertIn(\"X-Custom-Header\", response.headers)\n        self.assertEqual(response.headers[\"X-Custom-Header\"], \"CustomValue\")\n\n    def test_headers_property(self) -> None:\n        \"\"\"Test that the headers property returns the correct headers.\"\"\"\n        secure_headers = Secure.with_default_headers()\n\n        expected_headers = {header.header_name: header.header_value for header in secure_headers.headers_list}\n\n        self.assertEqual(secure_headers.headers, expected_headers)\n\n    def test_str_representation(self) -> None:\n        \"\"\"Test the __str__ method of Secure class.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        headers_str = str(secure_headers)\n\n        for header in secure_headers.headers_list:\n            header_line = f\"{header.header_name}: {header.header_value}\"\n            self.assertIn(header_line, headers_str)\n\n    def test_repr_representation(self) -> None:\n        \"\"\"Test the __repr__ method of Secure class.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        repr_str = repr(secure_headers)\n\n        self.assertIn(\"Secure(headers_list=\", repr_str)\n        self.assertIn(\"headers_list=\", repr_str)\n\n    def test_str_representation_uses_normalized_values(self) -> None:\n        \"\"\"str() should reflect the normalized output used when setting headers.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Test-Normalized\", \"value\\nwith\\r\\nspaces\"),\n            ]\n        )\n        secure_headers.validate_and_normalize_headers()\n        self.assertIn(\"X-Test-Normalized: value with spaces\", str(secure_headers))\n\n    def test_package_exports_header_constants(self) -> None:\n        \"\"\"The public package API should re-export the pipeline helpers the docs mention.\"\"\"\n        self.assertIs(secure_pkg.DEFAULT_ALLOWED_HEADERS, DEFAULT_ALLOWED_HEADERS)\n        self.assertIs(secure_pkg.COMMA_JOIN_OK, COMMA_JOIN_OK)\n        self.assertIs(secure_pkg.MULTI_OK, MULTI_OK)\n\n    def test_invalid_preset(self) -> None:\n        \"\"\"Test that an invalid preset raises a ValueError.\"\"\"\n        with self.assertRaises(ValueError) as context:\n            Secure.from_preset(\"invalid_preset\")  # type: ignore\n\n        self.assertIn(\"Unknown preset\", str(context.exception))\n\n    def test_empty_secure_instance(self) -> None:\n        \"\"\"Test that an empty Secure instance does not set any headers.\"\"\"\n        self.secure = Secure()\n        response = MockResponse()\n\n        self.secure.set_headers(response)\n        self.assertEqual(len(response.headers), 0)\n\n    def test_multiple_custom_headers(self) -> None:\n        \"\"\"Test that multiple custom headers are applied correctly.\"\"\"\n        custom_headers = [\n            CustomHeader(\"X-Custom-Header-1\", \"Value1\"),\n            CustomHeader(\"X-Custom-Header-2\", \"Value2\"),\n        ]\n        secure_headers = Secure(custom=custom_headers)\n        response = MockResponse()\n\n        secure_headers.set_headers(response)\n\n        self.assertIn(\"X-Custom-Header-1\", response.headers)\n        self.assertEqual(response.headers[\"X-Custom-Header-1\"], \"Value1\")\n\n        self.assertIn(\"X-Custom-Header-2\", response.headers)\n        self.assertEqual(response.headers[\"X-Custom-Header-2\"], \"Value2\")\n\n    def test_custom_strict_transport_security(self) -> None:\n        \"\"\"Test setting a custom Strict-Transport-Security header.\"\"\"\n        custom_hsts = StrictTransportSecurity().max_age(123456).include_subdomains()\n        secure_headers = Secure(hsts=custom_hsts)\n        response = MockResponse()\n\n        secure_headers.set_headers(response)\n\n        self.assertIn(\"Strict-Transport-Security\", response.headers)\n        self.assertEqual(\n            response.headers[\"Strict-Transport-Security\"],\n            \"max-age=123456; includeSubDomains\",\n        )\n\n    def test_setting_headers_on_response_with_both_headers_and_set_header(self) -> None:\n        \"\"\"Test that headers are set on response object with both headers dict and set_header method.\"\"\"\n\n        class MockResponseWithBoth:\n            def __init__(self) -> None:\n                self.headers: dict[str, str] = {}\n                self.header_storage: dict[str, str] = {}\n\n            def set_header(self, key: str, value: str) -> None:\n                self.header_storage[key] = value\n\n        secure_headers = Secure.with_default_headers()\n        response = MockResponseWithBoth()\n\n        # Apply the headers to the response object\n        secure_headers.set_headers(response)\n\n        # Verify that headers are set using set_header\n        self.assertIn(\"Strict-Transport-Security\", response.header_storage)\n        self.assertEqual(\n            response.header_storage[\"Strict-Transport-Security\"],\n            \"max-age=31536000; includeSubDomains\",\n        )\n\n        # Verify that headers dict was not used\n        self.assertNotIn(\"Strict-Transport-Security\", response.headers)\n\n    def test_header_order(self) -> None:\n        \"\"\"Test that headers are applied in the order they are in headers_list.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponse()\n\n        secure_headers.set_headers(response)\n\n        expected_order = [header.header_name for header in secure_headers.headers_list]\n        actual_order = list(response.headers.keys())\n\n        self.assertEqual(expected_order, actual_order)\n\n    def test_set_headers_async_with_sync_set_header(self) -> None:\n        \"\"\"Test async set_headers when response has a synchronous set_header method.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = MockResponseWithSetHeader()\n\n        async def mock_set_headers() -> None:\n            await secure_headers.set_headers_async(response)\n\n        asyncio.run(mock_set_headers())\n\n        # Verify that headers are set using set_header method\n        self.assertEqual(response.header_storage, secure_headers.headers)\n\n    def test_set_headers_with_no_headers_or_set_header(self) -> None:\n        \"\"\"Test that an error is raised when response lacks both headers and set_header.\"\"\"\n        secure_headers = Secure.with_default_headers()\n        response = object()  # An object with neither headers nor set_header\n\n        with self.assertRaises(AttributeError) as context:\n            secure_headers.set_headers(response)  # type: ignore\n\n        self.assertIn(\n            \"does not support setting headers\",\n            str(context.exception),\n        )\n\n    def test_headers_list_property(self) -> None:\n        \"\"\"Test that headers_list contains the correct headers.\"\"\"\n        custom_server = Server().set(\"CustomServer\")\n        custom_csp = ContentSecurityPolicy().default_src(\"'self'\")\n        custom_headers = [CustomHeader(\"X-Test-Header\", \"TestValue\")]\n\n        secure_headers = Secure(server=custom_server, csp=custom_csp, custom=custom_headers)\n\n        # Adjust the expected order based on how Secure initializes headers\n        expected_headers_list = [custom_csp, custom_server, *custom_headers]\n\n        self.assertEqual(secure_headers.headers_list, expected_headers_list)\n\n    def test_headers_property_with_no_headers(self) -> None:\n        \"\"\"Test that headers property returns an empty dict when no headers are set.\"\"\"\n        secure_headers = Secure()\n        self.assertEqual(secure_headers.headers, {})\n\n    def test_headers_property_tracks_builder_mutation_after_access(self) -> None:\n        \"\"\"Header mapping should reflect later builder updates instead of staying cached.\"\"\"\n        server = Server().set(\"Initial\")\n        secure_headers = Secure(server=server)\n\n        self.assertEqual(secure_headers.headers[\"Server\"], \"Initial\")\n\n        server.set(\"Updated\")\n\n        self.assertEqual(secure_headers.headers[\"Server\"], \"Updated\")\n\n    def test_allowlist_headers_drop_unexpected(self) -> None:\n        \"\"\"Headers not on the allowlist are removed when using drop policy.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Not-Allowed\", \"value\")])\n        secure_headers.allowlist_headers(on_unexpected=\"drop\")\n\n        header_names = [h.header_name for h in secure_headers.headers_list]\n        self.assertNotIn(\"X-Not-Allowed\", header_names)\n\n    def test_allowlist_headers_raises_on_unexpected(self) -> None:\n        \"\"\"Allowlist should raise when encountering unexpected names under the default policy.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Not-Allowed\", \"value\")])\n\n        with self.assertRaises(ValueError):\n            secure_headers.allowlist_headers()  # default on_unexpected is \"raise\"\n\n    def test_allowlist_accepts_default_balanced_headers(self) -> None:\n        \"\"\"The default allowlist should accept the default preset without extra configuration.\"\"\"\n        secure_headers = Secure.with_default_headers().allowlist_headers()\n\n        header_names = [h.header_name for h in secure_headers.headers_list]\n        self.assertIn(\"Server\", header_names)\n\n    def test_allowlist_respects_allow_x_prefixed(self) -> None:\n        \"\"\"Allowlist can be relaxed to accept any `X-` header when requested.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Extra-Header\", \"ok\")])\n        secure_headers.allowlist_headers(allow_x_prefixed=True)\n        header_names = [h.header_name for h in secure_headers.headers_list]\n        self.assertIn(\"X-Extra-Header\", header_names)\n\n    def test_deduplicate_concat_merges_cache_control(self) -> None:\n        \"\"\"Comma-joinable headers can be concatenated via concat action.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"Cache-Control\", \"max-age=0\"),\n                CustomHeader(\"Cache-Control\", \"no-cache\"),\n            ]\n        )\n        secure_headers.deduplicate_headers(action=\"concat\")\n\n        self.assertEqual(len(secure_headers.headers_list), 1)\n        only_header = secure_headers.headers_list[0]\n        self.assertEqual(only_header.header_name, \"Cache-Control\")\n        self.assertEqual(only_header.header_value, \"max-age=0, no-cache\")\n\n    def test_deduplicate_headers_raise_on_duplicate(self) -> None:\n        \"\"\"Duplicates without a merge policy still surface as errors.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Test-Header\", \"a\"),\n                CustomHeader(\"X-Test-Header\", \"b\"),\n            ]\n        )\n\n        with self.assertRaises(ValueError):\n            secure_headers.deduplicate_headers()\n\n    def test_validate_and_normalize_headers_strict_rejects_crlf(self) -> None:\n        \"\"\"Strict mode in validation treats CR/LF as configuration errors.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Strict\", \"bad\\rvalue\"),\n            ]\n        )\n\n        with self.assertRaises(ValueError):\n            secure_headers.validate_and_normalize_headers(strict=True)\n\n    def test_validate_and_normalize_headers_is_cleared_by_headers_list_mutation(self) -> None:\n        \"\"\"Mutating `headers_list` should discard any normalized snapshot.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Test\", \"value\\nwith\\r\\nspaces\")])\n        secure_headers.validate_and_normalize_headers()\n\n        secure_headers.headers_list.append(CustomHeader(\"X-New\", \"fresh\"))\n\n        self.assertEqual(\n            secure_headers.header_items(),\n            (\n                (\"X-Test\", \"value\\nwith\\r\\nspaces\"),\n                (\"X-New\", \"fresh\"),\n            ),\n        )\n\n    def test_validate_and_normalize_headers_is_cleared_by_builder_mutation(self) -> None:\n        \"\"\"Mutating an existing builder should invalidate stale normalized output.\"\"\"\n        server = Server().set(\"Initial\")\n        secure_headers = Secure(server=server)\n        secure_headers.validate_and_normalize_headers()\n\n        server.set(\"Updated\")\n\n        self.assertEqual(secure_headers.headers[\"Server\"], \"Updated\")\n\n    def test_headers_property_raises_on_duplicates(self) -> None:\n        \"\"\"Accessing `headers` should fail when duplicates are configured.\"\"\"\n        secure_headers = Secure(\n            custom=[\n                CustomHeader(\"X-Dupe\", \"a\"),\n                CustomHeader(\"X-Dupe\", \"b\"),\n            ]\n        )\n\n        with self.assertRaises(ValueError):\n            _ = secure_headers.headers\n\n    def test_set_headers_wraps_setter_errors(self) -> None:\n        \"\"\"Synchronous setter errors are surfaced as HeaderSetError.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Test\", \"value\")])\n        response = MockResponseRaiseSetHeader()\n\n        with self.assertRaises(HeaderSetError):\n            secure_headers.set_headers(response)\n\n    def test_set_headers_async_wraps_setter_errors(self) -> None:\n        \"\"\"Async setter errors propagate as HeaderSetError as well.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Test\", \"value\")])\n        response = MockResponseRaiseSetHeader()\n\n        async def run() -> None:\n            with self.assertRaises(HeaderSetError):\n                await secure_headers.set_headers_async(response)\n\n        asyncio.run(run())\n\n    def test_set_headers_runtime_error_on_async_setter(self) -> None:\n        \"\"\"Sync set_headers should detect awaitables returned from set_header.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Test\", \"value\")])\n        response = MockResponseAwaitableSetHeader()\n\n        with self.assertRaises(RuntimeError):\n            secure_headers.set_headers(response)\n\n    def test_set_headers_async_handles_async_headers_mapping(self) -> None:\n        \"\"\"Async header mappings are awaited to completion.\"\"\"\n        secure_headers = Secure(custom=[CustomHeader(\"X-Async\", \"value\")])\n        response = MockAsyncHeadersResponse()\n\n        async def run() -> None:\n            await secure_headers.set_headers_async(response)\n\n        asyncio.run(run())\n\n        self.assertEqual(response.headers.storage, secure_headers.headers)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  }
]