fastapi-m8 3.1.0__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.github/workflows/CI.yaml +26 -9
  2. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.github/workflows/PiPy.yml +5 -7
  3. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/CHANGELOG.md +90 -0
  4. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/PKG-INFO +35 -15
  5. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/README.md +33 -13
  6. fastapi_m8-3.3.0/constraints-all.txt +237 -0
  7. fastapi_m8-3.3.0/constraints.txt +114 -0
  8. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_app.py +7 -6
  9. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_compat.py +16 -0
  10. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_internal_auth.py +5 -2
  11. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_version.py +1 -1
  12. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/config.py +68 -7
  13. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/pyproject.toml +2 -2
  14. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_app.py +107 -7
  15. fastapi_m8-3.3.0/tests/test_ci_policy.py +136 -0
  16. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_config.py +80 -0
  17. fastapi_m8-3.1.0/.github/workflows/ci.yml +0 -54
  18. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.codacy.yml +0 -0
  19. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.dockerignore +0 -0
  20. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.env.example +0 -0
  21. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.gitattributes +0 -0
  22. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.github/FUNDING.yml +0 -0
  23. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.github/dependabot.yml +0 -0
  24. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.gitignore +0 -0
  25. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/.pydocstyle +0 -0
  26. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/LICENSE +0 -0
  27. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/SECURITY.md +0 -0
  28. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/__init__.py +0 -0
  29. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_async_stub.py +0 -0
  30. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_deps.py +0 -0
  31. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_engine.py +0 -0
  32. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_events.py +0 -0
  33. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_health.py +0 -0
  34. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/_revocation.py +0 -0
  35. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/scripts/__init__.py +0 -0
  36. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/scripts/docker_start.sh +0 -0
  37. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/fastapi_m8/scripts/pre_start.py +0 -0
  38. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/__init__.py +0 -0
  39. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/conftest.py +0 -0
  40. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_app_extra.py +0 -0
  41. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_async_stub.py +0 -0
  42. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_compat.py +0 -0
  43. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_config_file_secrets.py +0 -0
  44. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_deps.py +0 -0
  45. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_engine.py +0 -0
  46. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_event_signing_gate.py +0 -0
  47. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_events.py +0 -0
  48. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_health.py +0 -0
  49. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_host_header_routing.py +0 -0
  50. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_internal_auth.py +0 -0
  51. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_meta.py +0 -0
  52. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_pre_start.py +0 -0
  53. {fastapi_m8-3.1.0 → fastapi_m8-3.3.0}/tests/test_revocation.py +0 -0
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: ["main"]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  env:
10
13
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
11
14
 
@@ -13,10 +16,10 @@ jobs:
13
16
  lint:
14
17
  runs-on: ubuntu-latest
15
18
  steps:
16
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
17
20
 
18
21
  - name: Set up Python
19
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
22
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
20
23
  with:
21
24
  python-version: "3.14"
22
25
  cache: "pip"
@@ -33,10 +36,10 @@ jobs:
33
36
  typecheck:
34
37
  runs-on: ubuntu-latest
35
38
  steps:
36
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
39
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37
40
 
38
41
  - name: Set up Python
39
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
42
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
40
43
  with:
41
44
  python-version: "3.14"
42
45
  cache: "pip"
@@ -50,10 +53,10 @@ jobs:
50
53
  security:
51
54
  runs-on: ubuntu-latest
52
55
  steps:
53
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
56
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
54
57
 
55
58
  - name: Set up Python
56
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
59
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
57
60
  with:
58
61
  python-version: "3.14"
59
62
 
@@ -84,10 +87,10 @@ jobs:
84
87
  python-version: ["3.11", "3.12", "3.13", "3.14"]
85
88
 
86
89
  steps:
87
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
90
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
88
91
 
89
92
  - name: Set up Python ${{ matrix.python-version }}
90
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
93
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
91
94
  with:
92
95
  python-version: ${{ matrix.python-version }}
93
96
  cache: "pip"
@@ -100,7 +103,7 @@ jobs:
100
103
 
101
104
  - name: Upload coverage to Codecov
102
105
  if: matrix.python-version == '3.11' && github.actor != 'dependabot[bot]'
103
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
106
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
104
107
  with:
105
108
  token: ${{ secrets.CODECOV_TOKEN }}
106
109
  files: coverage.xml
@@ -112,3 +115,17 @@ jobs:
112
115
  with:
113
116
  project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
114
117
  coverage-reports: coverage.xml
118
+
119
+ secret-scan:
120
+ name: Secret scan (gitleaks)
121
+ runs-on: ubuntu-latest
122
+
123
+ steps:
124
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
125
+ with:
126
+ fetch-depth: 0
127
+
128
+ - uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
129
+ env:
130
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
131
+ GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
@@ -12,9 +12,9 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
16
 
17
- - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
17
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
18
18
  with:
19
19
  python-version: "3.x"
20
20
 
@@ -52,8 +52,6 @@ jobs:
52
52
 
53
53
  - name: Publish release distributions to PyPI
54
54
  uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
55
- with:
56
- password: ${{ secrets.PYPI_API_TOKEN }}
57
55
 
58
56
  sbom:
59
57
  name: Generate & attach SBOM
@@ -65,16 +63,16 @@ jobs:
65
63
  id-token: write
66
64
 
67
65
  steps:
68
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
66
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
69
67
 
70
- - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
68
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
71
69
  with:
72
70
  python-version: "3.x"
73
71
 
74
72
  - name: Install package
75
73
  run: pip install -e ".[all]"
76
74
 
77
- - uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
75
+ - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
78
76
  with:
79
77
  artifact-name: fastapi-m8-${{ github.event.release.tag_name }}.sbom.spdx.json
80
78
  format: spdx-json
@@ -5,6 +5,96 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
5
5
 
6
6
  ---
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ---
11
+
12
+ ## [3.3.0] — 2026-07-02 · legacy consumer private-auth is development-only (item 11.2b) + supply-chain hardening (11.6/11.7/11.8)
13
+
14
+ > **MINOR — security remediation, no public API removed.** The only runtime behaviour change
15
+ > is a new fail-closed boot check: a production/strict consumer (`ENVIRONMENT=production` or
16
+ > `STRICT_PRODUCTION_MODE=true`) that still runs legacy single-shared-secret private auth
17
+ > (`INTROSPECTION_URL` set, `INTERNAL_CLIENT_ID` unset) now raises at settings construction.
18
+ > Such a consumer already could not authenticate against a hardened `fa-auth-m8` issuer
19
+ > (item 11.2a retired the shared-secret fallback), so this converts a latent deployment fault
20
+ > into an explicit error rather than changing any working configuration. Local/development is
21
+ > unaffected. `auth-sdk-m8` floor raised to `>=2.1.1,<3.0.0` (carries the SDK's latest
22
+ > dependency/CI maintenance release; no new SDK API is required).
23
+
24
+ ### Security
25
+
26
+ - **Legacy single-shared-secret private-auth mode is now development-only (OWASP remediation
27
+ item 11.2b).** `ConsumerServiceSettings` gained a `model_validator` that fails settings
28
+ construction when a production/strict consumer (`ENVIRONMENT=production` or
29
+ `STRICT_PRODUCTION_MODE=true`) has `INTROSPECTION_URL` configured but no
30
+ `INTERNAL_CLIENT_ID`. `fa-auth-m8` has retired the issuer's legacy single-shared-secret
31
+ private-API fallback (item 11.2a), so a production/strict consumer left in legacy
32
+ token-only mode is guaranteed to fail against a hardened issuer — this turns that latent
33
+ deployment fault into an explicit, fail-closed boot error. Legacy mode remains valid for
34
+ local/development.
35
+ - **Coherent per-consumer private-auth group validation.** The same validator now rejects
36
+ (in every environment) `SERVICE_TOKEN_EXCHANGE_ENABLED=true` without `INTERNAL_CLIENT_ID`
37
+ (exchange has no identity to present and would silently downgrade to legacy) and
38
+ `INTERNAL_CLIENT_ID` set without `PRIVATE_API_SECRET` (per-consumer mode needs its
39
+ bootstrap secret). No validation message echoes a secret value.
40
+ - **PyPI Trusted Publishing is now the sole publish mechanism (item 11.6).** The long-lived
41
+ `PYPI_API_TOKEN` secret was removed from `PiPy.yml`; the release job publishes via OIDC
42
+ (`id-token: write`) into a protected `pypi` environment. No API token is stored in the repo
43
+ or referenced by any workflow.
44
+ - **CI policy tests (item 11.7).** New `tests/test_ci_policy.py` asserts the supply-chain
45
+ posture in CI: no `PYPI_API_TOKEN` reference, `id-token: write` present, the protected
46
+ `pypi` environment is used, the duplicate `ci.yml` is gone, the secret-scan job is present,
47
+ every action is SHA-pinned in both workflows, and key runtime packages are pinned in
48
+ `constraints-all.txt`.
49
+ - **Locked dependency constraints (item 11.8).** `constraints.txt` and `constraints-all.txt`
50
+ are generated via `pip-compile` from public PyPI only (no custom index), giving CI a
51
+ reproducible, fully-pinned install closure.
52
+
53
+ ### Changed
54
+
55
+ - CI workflows consolidated — the secret-scan job was absorbed into `CI.yaml` and the
56
+ duplicate `ci.yml` removed; dependabot SHA bumps for `checkout`, `setup-python`,
57
+ `sbom-action`, `codecov-action`, and `gitleaks-action` applied across both workflows.
58
+ - `COMPAT_MATRIX` gains a `3.3` entry (`auth-sdk-m8 >=2.1.0,<3.0.0`, unchanged floor).
59
+
60
+ ### Docs
61
+
62
+ - README *Per-consumer internal auth* section and the defaults-by-layer table now document
63
+ that legacy mode is development-only and fatal under production/strict; removed wording
64
+ that implied the issuer still offers a shared-secret fallback. Compatibility table updated
65
+ with the `3.1.0`–`3.3.0` rows.
66
+
67
+ ---
68
+
69
+ ## [3.2.0] — 2026-06-28 · ungated `/health` constant liveness body (item 9.4 Design B)
70
+
71
+ > **MINOR — additive, backward-compatible.** The only behaviour change is in the ungated
72
+ > `/health` response: callers without a valid `HEALTH_DETAIL_CREDENTIAL` now always
73
+ > receive `200 {"status":"ok"}` instead of the real aggregate status. Callers that
74
+ > already present the credential are unaffected — they still get the real status, per-check
75
+ > detail, and the 503 HTTP code when checks fail. `auth-sdk-m8` floor unchanged at
76
+ > `>=2.1.0,<3.0.0`.
77
+
78
+ ### Changed
79
+
80
+ - **Ungated `/health` body is now a constant liveness response (item 9.4 Design B).**
81
+ Callers without a valid `HEALTH_DETAIL_CREDENTIAL` (or `detail_public=True`) always
82
+ receive `200 {"status":"ok"}`, regardless of the real aggregate health. Previously the
83
+ ungated branch returned `{"status": overall.value}`, which leaked `degraded` — a
84
+ public attack-timing oracle that signals when fail-open degradation is active.
85
+
86
+ Health checks are **not run** for ungated requests (pure liveness, no dependency
87
+ probing); caching is unaffected for credentialed callers. Credentialed callers
88
+ (valid `X-Internal-Token` matching `HEALTH_DETAIL_CREDENTIAL`, or `detail_public=True`)
89
+ continue to receive the real aggregate status, the per-check body, and the correct
90
+ HTTP status code (200 or 503).
91
+
92
+ - **README `Health Detail Gating` section updated** to document the constant ungated
93
+ body and the Design B rationale; the defaults-by-layer table row for
94
+ `HEALTH_DETAIL_CREDENTIAL` updated accordingly.
95
+
96
+ ---
97
+
8
98
  ## [3.1.0] — 2026-06-24/25 · consumer-side post-1.0 remediation (9.1 / 5.5 / 7.x.1 / 9.2 / 10.1) + event-stream 9.1 follow-on
9
99
 
10
100
  > **MINOR — additive, backward-compatible.** Default behaviour is unchanged: a consumer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-m8
3
- Version: 3.1.0
3
+ Version: 3.3.0
4
4
  Summary: FastAPI application framework for m8 consumer microservices.
5
5
  Author-email: Eli Serra <e.serra173@gmail.com>
6
6
  License: Apache License
@@ -216,7 +216,7 @@ Classifier: Programming Language :: Python :: 3.13
216
216
  Classifier: Topic :: Software Development :: Libraries
217
217
  Requires-Python: >=3.11
218
218
  Requires-Dist: anyio>=4.0
219
- Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<3.0.0,>=2.1.0
219
+ Requires-Dist: auth-sdk-m8[config,events,fastapi,observability,security]<3.0.0,>=2.1.1
220
220
  Requires-Dist: fastapi>=0.136.3
221
221
  Requires-Dist: httpx>=0.27.0
222
222
  Requires-Dist: packaging>=24.0
@@ -620,18 +620,27 @@ Required only when `TOKEN_MODE=stateful` and `AUTH_SERVICE_ROLE=consumer`.
620
620
  #### Per-consumer internal auth (item 9.1)
621
621
 
622
622
  By default a consumer authenticates private calls with the single shared
623
- `PRIVATE_API_SECRET` (legacy mode), which matches fa-auth's fallback when it has no
624
- `PRIVATE_API_CONSUMERS` registry. Set `INTERNAL_CLIENT_ID` to switch to the
625
- **per-consumer** model: `PRIVATE_API_SECRET` becomes this service's *bootstrap*
626
- secret, fa-auth authorizes each private route by the credential's granted scope
627
- (deny-by-default), and the blast radius collapses from the whole fleet to one
628
- consumer. Optionally exchange the bootstrap credential for short-TTL `Authorization:
629
- Bearer` service tokens so rotation comes for free from the token TTL. Selection is
630
- purely by config the home lab keeps working untouched.
623
+ `PRIVATE_API_SECRET` (legacy mode). **Legacy mode is development-only.** `fa-auth-m8`
624
+ has retired the issuer's legacy single-shared-secret fallback — its
625
+ `PRIVATE_API_CONSUMERS` registry is required in production/strict (item 11.2a) — so a
626
+ production/strict consumer left in legacy mode is guaranteed to fail against a hardened
627
+ issuer. Set `INTERNAL_CLIENT_ID` to switch to the **per-consumer** model:
628
+ `PRIVATE_API_SECRET` becomes this service's *bootstrap* secret, fa-auth authorizes each
629
+ private route by the credential's granted scope (deny-by-default), and the blast radius
630
+ collapses from the whole fleet to one consumer. Optionally exchange the bootstrap
631
+ credential for short-TTL `Authorization: Bearer` service tokens so rotation comes for
632
+ free from the token TTL. Selection is purely by config — the home lab keeps working
633
+ untouched.
634
+
635
+ > **Fatal under production/strict (item 11.2b).** When `ENVIRONMENT=production` or
636
+ > `STRICT_PRODUCTION_MODE=true`, settings validation fails at construction if
637
+ > `INTROSPECTION_URL` is set but `INTERNAL_CLIENT_ID` is unset. Two coherence rules
638
+ > apply in every environment: `SERVICE_TOKEN_EXCHANGE_ENABLED=true` requires
639
+ > `INTERNAL_CLIENT_ID`, and setting `INTERNAL_CLIENT_ID` requires `PRIVATE_API_SECRET`.
631
640
 
632
641
  | Variable | Required | Default | Description |
633
642
  |---|---|---|---|
634
- | `INTERNAL_CLIENT_ID` | No | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode; set = per-consumer bootstrap mode. |
643
+ | `INTERNAL_CLIENT_ID` | No (Yes in prod/strict when `INTROSPECTION_URL` is set) | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode (development-only; rejected under production/strict once `INTROSPECTION_URL` is set); set = per-consumer bootstrap mode. |
635
644
  | `SERVICE_TOKEN_EXCHANGE_ENABLED` | No | `false` | Exchange the bootstrap credential for short-TTL Bearer service tokens at `{issuer}/private/v1/service-token` (requires `INTERNAL_CLIENT_ID`). |
636
645
  | `SERVICE_TOKEN_SCOPES` | No | `["introspection"]` | Scopes requested when minting a service token; fa-auth narrows to the subset the bootstrap credential was granted. |
637
646
  | `SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS` | No | `30` | Refresh a cached service token this many seconds before its `exp` so a call never races expiry. |
@@ -724,11 +733,19 @@ do not connect to Redis directly.
724
733
  | `REDIS_PASSWORD` | Redis password |
725
734
  | `REDIS_SSL` | Enable TLS (`true`/`false`, default `false`) |
726
735
 
727
- ### Health Detail Gating (item 9.3)
736
+ ### Health Detail Gating (items 9.3 + 9.4)
728
737
 
729
738
  | Variable | Default | Description |
730
739
  |---|---|---|
731
- | `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns only `{"status": "ok/fail"}` to all callers (fail-closed). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the full per-check breakdown. **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
740
+ | `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness response). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the real aggregate status, per-check breakdown, and the correct HTTP code (200 or 503). **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
741
+
742
+ > **Constant ungated body (item 9.4 Design B):** ungated callers always receive
743
+ > `200 {"status":"ok"}` regardless of the real aggregate health (including `degraded`
744
+ > or `fail`). This prevents operational-state leakage on public-HTTPS stacks: a
745
+ > `degraded` response is a timing oracle that signals fail-open degradation is active.
746
+ > Only callers presenting a valid `HEALTH_DETAIL_CREDENTIAL` see the real status, the
747
+ > per-check detail, and the 503 HTTP code when checks fail. Liveness probes and
748
+ > `{API_PREFIX}/ping` are unaffected.
732
749
 
733
750
  > **No-reuse enforcement (item 9.3):** at startup, `create_app` asserts that neither
734
751
  > `HEALTH_DETAIL_CREDENTIAL` nor `METRICS_SCRAPE_CREDENTIAL` equals `PRIVATE_API_SECRET`.
@@ -827,8 +844,8 @@ inherited from `auth-sdk-m8`, and made *fatal* only when pointed at production.
827
844
  | `ALLOWED_HOSTS` | inherited from `CommonSettings`; unset = no host check (dev) | unset under `STRICT_PRODUCTION_MODE` (strict fatal); wildcard `*` under strict |
828
845
  | `EVENT_SIGNING_ENABLED` / `EVENT_SIGNING_ACCEPT_UNSIGNED` | inherited secure defaults (signing on, unsigned rejected); the gate fires through the consumer's auto-run config-health (item 7.x.1) | `ENABLED=false` under strict; `ACCEPT_UNSIGNED=true` under production or strict |
829
846
  | `METRICS_SCRAPE_CREDENTIAL` | unset = `/metrics` relies on network isolation only; set = constant-time `Authorization: Bearer` gate. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (network-isolation is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
830
- | `HEALTH_DETAIL_CREDENTIAL` (item 9.3) | unset = `/health` returns only shallow status to all callers (fail-closed, detail never shown); set = `X-Internal-Token` must match to receive the full per-check breakdown. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = no detail, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
831
- | `INTERNAL_CLIENT_ID` (item 9.1) | unset = legacy single `PRIVATE_API_SECRET`; set = per-consumer bootstrap / service-token auth on private calls | never fatal (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) |
847
+ | `HEALTH_DETAIL_CREDENTIAL` (items 9.3 + 9.4) | unset = `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness; real status never leaked); set = `X-Internal-Token` must match to receive the real aggregate status, per-check breakdown, and correct HTTP code. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = constant liveness response, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
848
+ | `INTERNAL_CLIENT_ID` (items 9.1 + 11.2b) | unset = legacy single `PRIVATE_API_SECRET` (development-only); set = per-consumer bootstrap / service-token auth on private calls (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) | **fatal** under production/strict when `INTROSPECTION_URL` is set but `INTERNAL_CLIENT_ID` is unset; also fatal (any env) when `SERVICE_TOKEN_EXCHANGE_ENABLED=true` without `INTERNAL_CLIENT_ID`, or `INTERNAL_CLIENT_ID` set without `PRIVATE_API_SECRET` |
832
849
  | `_FILE` secret mounts | **inherited** from `CommonSettings` — every secret (`PRIVATE_API_SECRET_FILE`, `DB_PASSWORD_FILE`, `METRICS_SCRAPE_CREDENTIAL_FILE`, `HEALTH_DETAIL_CREDENTIAL_FILE`, …) can be sourced from `/run/secrets/*` with no code change | a referenced `<FIELD>_FILE` path is missing (fails closed at construction) |
833
850
 
834
851
  ---
@@ -1405,6 +1422,9 @@ async def test_health(client):
1405
1422
 
1406
1423
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1407
1424
  |---|---|---|
1425
+ | `3.3.0` | `>=2.1.1, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1426
+ | `3.2.0` | `>=2.1.0, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1427
+ | `3.1.0` | `>=2.1.0, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1408
1428
  | `3.0.0` | `>=2.0.1, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1409
1429
  | `2.1.0` | `>=1.5.0, <2.0.0` | 3.11, 3.12, 3.13 |
1410
1430
  | `2.0.0` | `>=1.4.0, <2.0.0` | 3.11, 3.12, 3.13 |
@@ -364,18 +364,27 @@ Required only when `TOKEN_MODE=stateful` and `AUTH_SERVICE_ROLE=consumer`.
364
364
  #### Per-consumer internal auth (item 9.1)
365
365
 
366
366
  By default a consumer authenticates private calls with the single shared
367
- `PRIVATE_API_SECRET` (legacy mode), which matches fa-auth's fallback when it has no
368
- `PRIVATE_API_CONSUMERS` registry. Set `INTERNAL_CLIENT_ID` to switch to the
369
- **per-consumer** model: `PRIVATE_API_SECRET` becomes this service's *bootstrap*
370
- secret, fa-auth authorizes each private route by the credential's granted scope
371
- (deny-by-default), and the blast radius collapses from the whole fleet to one
372
- consumer. Optionally exchange the bootstrap credential for short-TTL `Authorization:
373
- Bearer` service tokens so rotation comes for free from the token TTL. Selection is
374
- purely by config the home lab keeps working untouched.
367
+ `PRIVATE_API_SECRET` (legacy mode). **Legacy mode is development-only.** `fa-auth-m8`
368
+ has retired the issuer's legacy single-shared-secret fallback — its
369
+ `PRIVATE_API_CONSUMERS` registry is required in production/strict (item 11.2a) — so a
370
+ production/strict consumer left in legacy mode is guaranteed to fail against a hardened
371
+ issuer. Set `INTERNAL_CLIENT_ID` to switch to the **per-consumer** model:
372
+ `PRIVATE_API_SECRET` becomes this service's *bootstrap* secret, fa-auth authorizes each
373
+ private route by the credential's granted scope (deny-by-default), and the blast radius
374
+ collapses from the whole fleet to one consumer. Optionally exchange the bootstrap
375
+ credential for short-TTL `Authorization: Bearer` service tokens so rotation comes for
376
+ free from the token TTL. Selection is purely by config — the home lab keeps working
377
+ untouched.
378
+
379
+ > **Fatal under production/strict (item 11.2b).** When `ENVIRONMENT=production` or
380
+ > `STRICT_PRODUCTION_MODE=true`, settings validation fails at construction if
381
+ > `INTROSPECTION_URL` is set but `INTERNAL_CLIENT_ID` is unset. Two coherence rules
382
+ > apply in every environment: `SERVICE_TOKEN_EXCHANGE_ENABLED=true` requires
383
+ > `INTERNAL_CLIENT_ID`, and setting `INTERNAL_CLIENT_ID` requires `PRIVATE_API_SECRET`.
375
384
 
376
385
  | Variable | Required | Default | Description |
377
386
  |---|---|---|---|
378
- | `INTERNAL_CLIENT_ID` | No | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode; set = per-consumer bootstrap mode. |
387
+ | `INTERNAL_CLIENT_ID` | No (Yes in prod/strict when `INTROSPECTION_URL` is set) | — | This consumer's `X-Internal-Client` id. Unset = legacy single-secret mode (development-only; rejected under production/strict once `INTROSPECTION_URL` is set); set = per-consumer bootstrap mode. |
379
388
  | `SERVICE_TOKEN_EXCHANGE_ENABLED` | No | `false` | Exchange the bootstrap credential for short-TTL Bearer service tokens at `{issuer}/private/v1/service-token` (requires `INTERNAL_CLIENT_ID`). |
380
389
  | `SERVICE_TOKEN_SCOPES` | No | `["introspection"]` | Scopes requested when minting a service token; fa-auth narrows to the subset the bootstrap credential was granted. |
381
390
  | `SERVICE_TOKEN_REFRESH_LEEWAY_SECONDS` | No | `30` | Refresh a cached service token this many seconds before its `exp` so a call never races expiry. |
@@ -468,11 +477,19 @@ do not connect to Redis directly.
468
477
  | `REDIS_PASSWORD` | Redis password |
469
478
  | `REDIS_SSL` | Enable TLS (`true`/`false`, default `false`) |
470
479
 
471
- ### Health Detail Gating (item 9.3)
480
+ ### Health Detail Gating (items 9.3 + 9.4)
472
481
 
473
482
  | Variable | Default | Description |
474
483
  |---|---|---|
475
- | `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns only `{"status": "ok/fail"}` to all callers (fail-closed). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the full per-check breakdown. **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
484
+ | `HEALTH_DETAIL_CREDENTIAL` | — | Optional credential for the `/health` detail body gate. When **unset**, `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness response). When set, callers must present `X-Internal-Token: <value>` (constant-time match) to receive the real aggregate status, per-check breakdown, and the correct HTTP code (200 or 503). **Must not equal `PRIVATE_API_SECRET`** — accidental reuse is a fatal startup misconfiguration. Supports `_FILE` mount: set `HEALTH_DETAIL_CREDENTIAL_FILE=/run/secrets/health_cred.txt`. |
485
+
486
+ > **Constant ungated body (item 9.4 Design B):** ungated callers always receive
487
+ > `200 {"status":"ok"}` regardless of the real aggregate health (including `degraded`
488
+ > or `fail`). This prevents operational-state leakage on public-HTTPS stacks: a
489
+ > `degraded` response is a timing oracle that signals fail-open degradation is active.
490
+ > Only callers presenting a valid `HEALTH_DETAIL_CREDENTIAL` see the real status, the
491
+ > per-check detail, and the 503 HTTP code when checks fail. Liveness probes and
492
+ > `{API_PREFIX}/ping` are unaffected.
476
493
 
477
494
  > **No-reuse enforcement (item 9.3):** at startup, `create_app` asserts that neither
478
495
  > `HEALTH_DETAIL_CREDENTIAL` nor `METRICS_SCRAPE_CREDENTIAL` equals `PRIVATE_API_SECRET`.
@@ -571,8 +588,8 @@ inherited from `auth-sdk-m8`, and made *fatal* only when pointed at production.
571
588
  | `ALLOWED_HOSTS` | inherited from `CommonSettings`; unset = no host check (dev) | unset under `STRICT_PRODUCTION_MODE` (strict fatal); wildcard `*` under strict |
572
589
  | `EVENT_SIGNING_ENABLED` / `EVENT_SIGNING_ACCEPT_UNSIGNED` | inherited secure defaults (signing on, unsigned rejected); the gate fires through the consumer's auto-run config-health (item 7.x.1) | `ENABLED=false` under strict; `ACCEPT_UNSIGNED=true` under production or strict |
573
590
  | `METRICS_SCRAPE_CREDENTIAL` | unset = `/metrics` relies on network isolation only; set = constant-time `Authorization: Bearer` gate. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (network-isolation is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
574
- | `HEALTH_DETAIL_CREDENTIAL` (item 9.3) | unset = `/health` returns only shallow status to all callers (fail-closed, detail never shown); set = `X-Internal-Token` must match to receive the full per-check breakdown. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = no detail, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
575
- | `INTERNAL_CLIENT_ID` (item 9.1) | unset = legacy single `PRIVATE_API_SECRET`; set = per-consumer bootstrap / service-token auth on private calls | never fatal (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) |
591
+ | `HEALTH_DETAIL_CREDENTIAL` (items 9.3 + 9.4) | unset = `/health` returns a constant `{"status":"ok"}` to all callers (fail-closed liveness; real status never leaked); set = `X-Internal-Token` must match to receive the real aggregate status, per-check breakdown, and correct HTTP code. **Must not equal `PRIVATE_API_SECRET`** (fatal reuse check at startup). | never fatal (no credential = constant liveness response, which is a valid posture); reuse of `PRIVATE_API_SECRET` is fatal |
592
+ | `INTERNAL_CLIENT_ID` (items 9.1 + 11.2b) | unset = legacy single `PRIVATE_API_SECRET` (development-only); set = per-consumer bootstrap / service-token auth on private calls (must be coordinated with the issuer's `PRIVATE_API_CONSUMERS`) | **fatal** under production/strict when `INTROSPECTION_URL` is set but `INTERNAL_CLIENT_ID` is unset; also fatal (any env) when `SERVICE_TOKEN_EXCHANGE_ENABLED=true` without `INTERNAL_CLIENT_ID`, or `INTERNAL_CLIENT_ID` set without `PRIVATE_API_SECRET` |
576
593
  | `_FILE` secret mounts | **inherited** from `CommonSettings` — every secret (`PRIVATE_API_SECRET_FILE`, `DB_PASSWORD_FILE`, `METRICS_SCRAPE_CREDENTIAL_FILE`, `HEALTH_DETAIL_CREDENTIAL_FILE`, …) can be sourced from `/run/secrets/*` with no code change | a referenced `<FIELD>_FILE` path is missing (fails closed at construction) |
577
594
 
578
595
  ---
@@ -1149,6 +1166,9 @@ async def test_health(client):
1149
1166
 
1150
1167
  | `fastapi-m8` | `auth-sdk-m8` | Python |
1151
1168
  |---|---|---|
1169
+ | `3.3.0` | `>=2.1.1, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1170
+ | `3.2.0` | `>=2.1.0, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1171
+ | `3.1.0` | `>=2.1.0, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1152
1172
  | `3.0.0` | `>=2.0.1, <3.0.0` | 3.11, 3.12, 3.13, 3.14 |
1153
1173
  | `2.1.0` | `>=1.5.0, <2.0.0` | 3.11, 3.12, 3.13 |
1154
1174
  | `2.0.0` | `>=1.4.0, <2.0.0` | 3.11, 3.12, 3.13 |
@@ -0,0 +1,237 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.12
3
+ # by the following command:
4
+ #
5
+ # pip-compile --extra=all --extra=dev --no-emit-index-url --output-file=constraints-all.txt pyproject.toml
6
+ #
7
+ alembic==1.18.5
8
+ # via fastapi-m8
9
+ annotated-doc==0.0.4
10
+ # via fastapi
11
+ annotated-types==0.7.0
12
+ # via pydantic
13
+ anyio[trio]==4.14.1
14
+ # via
15
+ # fastapi-m8
16
+ # fastapi-m8 (pyproject.toml)
17
+ # httpx
18
+ # pytest-anyio
19
+ # starlette
20
+ asgi-lifespan==2.1.0
21
+ # via fastapi-m8 (pyproject.toml)
22
+ ast-serialize==0.6.0
23
+ # via mypy
24
+ attrs==26.1.0
25
+ # via
26
+ # outcome
27
+ # trio
28
+ auth-sdk-m8[config,events,fastapi,observability,security]==2.1.1
29
+ # via
30
+ # fastapi-m8
31
+ # fastapi-m8 (pyproject.toml)
32
+ bandit==1.9.4
33
+ # via fastapi-m8 (pyproject.toml)
34
+ boolean-py==5.0
35
+ # via license-expression
36
+ cachecontrol[filecache]==0.14.4
37
+ # via
38
+ # cachecontrol
39
+ # pip-audit
40
+ certifi==2026.6.17
41
+ # via
42
+ # httpcore
43
+ # httpx
44
+ # requests
45
+ cffi==2.0.0
46
+ # via cryptography
47
+ charset-normalizer==3.4.7
48
+ # via requests
49
+ coverage[toml]==7.14.3
50
+ # via
51
+ # fastapi-m8 (pyproject.toml)
52
+ # pytest-cov
53
+ cryptography==49.0.0
54
+ # via auth-sdk-m8
55
+ cyclonedx-python-lib==11.11.0
56
+ # via pip-audit
57
+ defusedxml==0.7.1
58
+ # via py-serializable
59
+ dnspython==2.8.0
60
+ # via email-validator
61
+ email-validator==2.3.0
62
+ # via auth-sdk-m8
63
+ fastapi==0.139.0
64
+ # via
65
+ # auth-sdk-m8
66
+ # fastapi-m8
67
+ # fastapi-m8 (pyproject.toml)
68
+ fastapi-m8[all,db,mysql,postgres] @ file:///workspace/fastapi-m8
69
+ # via fastapi-m8 (pyproject.toml)
70
+ filelock==3.29.4
71
+ # via cachecontrol
72
+ greenlet==3.5.3
73
+ # via sqlalchemy
74
+ h11==0.16.0
75
+ # via httpcore
76
+ httpcore==1.0.9
77
+ # via httpx
78
+ httpx==0.28.1
79
+ # via
80
+ # auth-sdk-m8
81
+ # fastapi-m8
82
+ # fastapi-m8 (pyproject.toml)
83
+ idna==3.18
84
+ # via
85
+ # anyio
86
+ # email-validator
87
+ # httpx
88
+ # requests
89
+ # trio
90
+ iniconfig==2.3.0
91
+ # via pytest
92
+ librt==0.12.0
93
+ # via mypy
94
+ license-expression==30.4.4
95
+ # via cyclonedx-python-lib
96
+ mako==1.3.12
97
+ # via alembic
98
+ markdown-it-py==4.2.0
99
+ # via rich
100
+ markupsafe==3.0.3
101
+ # via mako
102
+ mdurl==0.1.2
103
+ # via markdown-it-py
104
+ msgpack==1.2.1
105
+ # via cachecontrol
106
+ mypy==2.1.0
107
+ # via fastapi-m8 (pyproject.toml)
108
+ mypy-extensions==1.1.0
109
+ # via mypy
110
+ outcome==1.3.0.post0
111
+ # via trio
112
+ packageurl-python==0.17.6
113
+ # via cyclonedx-python-lib
114
+ packaging==26.2
115
+ # via
116
+ # fastapi-m8
117
+ # fastapi-m8 (pyproject.toml)
118
+ # pip-audit
119
+ # pip-requirements-parser
120
+ # pytest
121
+ pathspec==1.1.1
122
+ # via mypy
123
+ pip-api==0.0.34
124
+ # via pip-audit
125
+ pip-audit==2.10.1
126
+ # via fastapi-m8 (pyproject.toml)
127
+ pip-requirements-parser==32.0.1
128
+ # via pip-audit
129
+ platformdirs==4.10.0
130
+ # via pip-audit
131
+ pluggy==1.6.0
132
+ # via
133
+ # pytest
134
+ # pytest-cov
135
+ prometheus-client==0.25.0
136
+ # via auth-sdk-m8
137
+ psycopg2-binary==2.9.12
138
+ # via fastapi-m8
139
+ py-serializable==2.1.0
140
+ # via cyclonedx-python-lib
141
+ pycparser==3.0
142
+ # via cffi
143
+ pydantic==2.13.4
144
+ # via
145
+ # auth-sdk-m8
146
+ # fastapi
147
+ # pydantic-settings
148
+ # sqlmodel
149
+ pydantic-core==2.46.4
150
+ # via pydantic
151
+ pydantic-settings==2.14.2
152
+ # via auth-sdk-m8
153
+ pygments==2.20.0
154
+ # via
155
+ # pytest
156
+ # rich
157
+ pyjwt==2.13.0
158
+ # via auth-sdk-m8
159
+ pymysql==1.2.0
160
+ # via fastapi-m8
161
+ pyparsing==3.3.2
162
+ # via pip-requirements-parser
163
+ pytest==9.1.1
164
+ # via
165
+ # fastapi-m8 (pyproject.toml)
166
+ # pytest-anyio
167
+ # pytest-cov
168
+ pytest-anyio==0.0.0
169
+ # via fastapi-m8 (pyproject.toml)
170
+ pytest-cov==7.1.0
171
+ # via fastapi-m8 (pyproject.toml)
172
+ python-dotenv==1.2.2
173
+ # via pydantic-settings
174
+ pyyaml==6.0.3
175
+ # via bandit
176
+ requests==2.34.2
177
+ # via
178
+ # cachecontrol
179
+ # pip-audit
180
+ rich==15.0.0
181
+ # via
182
+ # bandit
183
+ # pip-audit
184
+ ruff==0.15.20
185
+ # via fastapi-m8 (pyproject.toml)
186
+ sniffio==1.3.1
187
+ # via
188
+ # asgi-lifespan
189
+ # trio
190
+ sortedcontainers==2.4.0
191
+ # via
192
+ # cyclonedx-python-lib
193
+ # trio
194
+ sqlalchemy==2.0.51
195
+ # via
196
+ # alembic
197
+ # fastapi-m8
198
+ # sqlmodel
199
+ sqlmodel==0.0.39
200
+ # via fastapi-m8
201
+ starlette==1.3.1
202
+ # via
203
+ # auth-sdk-m8
204
+ # fastapi
205
+ stevedore==5.8.0
206
+ # via bandit
207
+ tenacity==9.1.4
208
+ # via fastapi-m8 (pyproject.toml)
209
+ tomli==2.4.1
210
+ # via pip-audit
211
+ tomli-w==1.2.0
212
+ # via pip-audit
213
+ trio==0.33.0
214
+ # via anyio
215
+ typing-extensions==4.16.0
216
+ # via
217
+ # alembic
218
+ # anyio
219
+ # cyclonedx-python-lib
220
+ # fastapi
221
+ # mypy
222
+ # pydantic
223
+ # pydantic-core
224
+ # sqlalchemy
225
+ # sqlmodel
226
+ # starlette
227
+ # typing-inspection
228
+ typing-inspection==0.4.2
229
+ # via
230
+ # fastapi
231
+ # pydantic
232
+ # pydantic-settings
233
+ urllib3==2.7.0
234
+ # via requests
235
+
236
+ # The following packages are considered to be unsafe in a requirements file:
237
+ # pip