fastapi-m8 3.2.0__tar.gz → 4.0.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 (66) hide show
  1. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.codacy.yml +2 -0
  2. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.dockerignore +0 -0
  3. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/workflows/CI.yaml +26 -9
  4. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/workflows/PiPy.yml +5 -7
  5. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.gitignore +1 -4
  6. fastapi_m8-4.0.0/.gitleaks.toml +15 -0
  7. fastapi_m8-4.0.0/AGENTS.md +27 -0
  8. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/CHANGELOG.md +189 -0
  9. fastapi_m8-4.0.0/CLAUDE.md +27 -0
  10. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/PKG-INFO +181 -22
  11. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/README.md +177 -20
  12. fastapi_m8-4.0.0/constraints-all.txt +237 -0
  13. fastapi_m8-4.0.0/constraints.txt +114 -0
  14. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/__init__.py +22 -1
  15. fastapi_m8-4.0.0/fastapi_m8/_api_key.py +415 -0
  16. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_compat.py +17 -0
  17. fastapi_m8-4.0.0/fastapi_m8/_deps.py +502 -0
  18. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_internal_auth.py +5 -2
  19. fastapi_m8-4.0.0/fastapi_m8/_revocation.py +567 -0
  20. fastapi_m8-4.0.0/fastapi_m8/_route_audit.py +77 -0
  21. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_version.py +1 -1
  22. fastapi_m8-4.0.0/fastapi_m8/config.py +356 -0
  23. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/docker_start.sh +0 -0
  24. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/pyproject.toml +8 -3
  25. fastapi_m8-4.0.0/tests/test_api_key.py +618 -0
  26. fastapi_m8-4.0.0/tests/test_api_key_deps.py +438 -0
  27. fastapi_m8-4.0.0/tests/test_ci_policy.py +136 -0
  28. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_compat.py +30 -0
  29. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_config.py +80 -0
  30. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_deps.py +341 -35
  31. fastapi_m8-4.0.0/tests/test_packaging.py +164 -0
  32. fastapi_m8-4.0.0/tests/test_public_typing.py +118 -0
  33. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_revocation.py +389 -38
  34. fastapi_m8-4.0.0/tests/test_route_audit.py +151 -0
  35. fastapi_m8-3.2.0/.github/workflows/ci.yml +0 -54
  36. fastapi_m8-3.2.0/fastapi_m8/_deps.py +0 -231
  37. fastapi_m8-3.2.0/fastapi_m8/_revocation.py +0 -321
  38. fastapi_m8-3.2.0/fastapi_m8/config.py +0 -186
  39. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.env.example +0 -0
  40. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.gitattributes +0 -0
  41. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/FUNDING.yml +0 -0
  42. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/dependabot.yml +0 -0
  43. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.pydocstyle +0 -0
  44. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/LICENSE +0 -0
  45. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/SECURITY.md +0 -0
  46. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_app.py +0 -0
  47. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_async_stub.py +0 -0
  48. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_engine.py +0 -0
  49. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_events.py +0 -0
  50. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_health.py +0 -0
  51. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/__init__.py +0 -0
  52. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/pre_start.py +0 -0
  53. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/__init__.py +0 -0
  54. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/conftest.py +0 -0
  55. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_app.py +0 -0
  56. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_app_extra.py +0 -0
  57. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_async_stub.py +0 -0
  58. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_config_file_secrets.py +0 -0
  59. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_engine.py +0 -0
  60. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_event_signing_gate.py +0 -0
  61. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_events.py +0 -0
  62. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_health.py +0 -0
  63. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_host_header_routing.py +0 -0
  64. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_internal_auth.py +0 -0
  65. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_meta.py +0 -0
  66. {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_pre_start.py +0 -0
@@ -27,3 +27,5 @@ exclude_paths:
27
27
  # Codacy's prose analyzer misclassifies a security policy doc (flags standard
28
28
  # security acronyms such as CA/TTL/JTI as "undefined" on every revision).
29
29
  - "SECURITY.md"
30
+ - "AGENTS.md"
31
+ - "CLAUDE.md"
File without changes
@@ -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
@@ -215,7 +215,4 @@ marimo/_lsp/
215
215
  __marimo__/
216
216
 
217
217
  # Streamlit
218
- .streamlit/secrets.toml
219
-
220
- Claude.md
221
- Agents.md
218
+ .streamlit/secrets.toml
@@ -0,0 +1,15 @@
1
+ title = "fastapi-m8 gitleaks config"
2
+
3
+ [extend]
4
+ useDefault = true
5
+
6
+ [allowlist]
7
+ description = "Project-wide allowlist"
8
+ paths = [
9
+ # Test fixtures may contain deterministic low-entropy test values
10
+ '''tests/''',
11
+ ]
12
+ # Add SHAs of any prior accidental secret commits here after rotation:
13
+ # commits = [
14
+ # "abc123def456", # description of what was rotated and when
15
+ # ]
@@ -0,0 +1,27 @@
1
+ # fastapi-m8
2
+
3
+ ## Layer
4
+ Platform (FastAPI application framework)
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+ Application factory wiring `auth-sdk-m8` into FastAPI services using `fa-auth-m8`.
10
+
11
+ ---
12
+
13
+ ## Rules
14
+ - No business logic
15
+ - Only reusable service scaffolding
16
+ - Must remain minimal and reusable across services
17
+ - No coupling to fa-auth-m8 or any domain service
18
+
19
+ ---
20
+
21
+ ## Workspace integration
22
+
23
+ When nested in a workspace, locate the nearest ancestor containing
24
+ `.workspace/policy.index.json` and apply its `python` policy.
25
+
26
+ If no workspace policy exists, use this file, `pyproject.toml`, repository
27
+ documentation and existing CI as the authoritative local context.`
@@ -5,6 +5,195 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
5
5
 
6
6
  ---
7
7
 
8
+ ## [4.0.0] — 2026-07-17 · auth-sdk-m8 3.0.0 alignment — canonical role/flag invariant + JTI-status v2 + remote API-key principal
9
+
10
+ > **MAJOR — coordinated release with `auth-sdk-m8 3.0.0`.** `fastapi-m8` raises the SDK
11
+ > floor to `>=3.0.0,<4.0.0` to consume the canonical role/flag authorization invariant
12
+ > (`has_superuser_privileges()` and `has_minimum_role()`), the subject-bound JTI-status v2
13
+ > introspection contract, and the API-key principal/introspection schemas. All role guards
14
+ > now delegate to the shared SDK policy helpers, and the new remote API-key principal
15
+ > dependencies (`get_current_api_key_reader`/`_writer` and `require_api_key_role(...)`)
16
+ > resolve a key to its owner's **current** authority through the issuer's introspection
17
+ > endpoint, with no administrative or superuser capability surface. The API-key path is
18
+ > inherently fail-closed, never caches a positive principal, and never retries after
19
+ > transmission — ensuring a role downgrade lands on the key's very next request.
20
+
21
+ ### Added
22
+
23
+ - **JTI-status v2 (subject-bound, generation-tagged)** — `RemoteRevocationClient`
24
+ now speaks the v2 introspection contract: the request asserts the subject read
25
+ from the token (`{jti, expected_user_id, schema_version}`), and an active reply
26
+ carries the owner's `auth_generation`, which tags the cache entry it produces.
27
+ As defense in depth an active reply naming a different subject is refused and
28
+ never cached. **The v2 decision never falls open:** a reply this release cannot
29
+ interpret — malformed, unknown `schema_version`, or a pre-2.0 issuer's bare
30
+ `{"active": true}` with no generation — raises the new `RevocationDecisionError`
31
+ (a `RevocationCheckError`, so dependencies still answer `503`) regardless of
32
+ `ACCESS_REVOCATION_FAILURE_MODE`, which remains a transport-outage escape only.
33
+ - **`AuthDeps.handle_auth_event(event)`** — the supported `on_event` handler for
34
+ `build_event_stream_client`, so no service re-derives the eviction rules
35
+ locally. Accepts **both v1 and v2** `session-revoked` events for the rollout
36
+ interval in which the issuer emits v2 before every consumer has upgraded, and
37
+ routes `user-deleted` as before. v2 events apply the exact `<`/`==`/`>`
38
+ watermark rule per user, deduplicated on the durable `event_id` (never the SSE
39
+ transport id, which resets on issuer restart), so a replay is idempotent while
40
+ sibling JTI events at one generation stay distinguishable. A user-wide event
41
+ evicts the user's entries older than the revoking generation and spares
42
+ sessions minted after it. A v1 event carries no generation to compare, so it
43
+ evicts the whole user; an unusable payload flushes the cache.
44
+ - **Remote API-key principal dependencies** — `AuthDeps` gains
45
+ `get_current_api_key_principal`, the `require_api_key_role(...)` factory, and its
46
+ `get_current_api_key_reader`/`_writer` specializations, built by the same single
47
+ `build_auth_deps()` call as the JWT guards. A key presented on `X-API-Key` is
48
+ resolved to its **owner's current authority** through the issuer's
49
+ `POST /private/v1/api-keys/introspect`, then authorized with the same shared SDK
50
+ predicate the JWT guards use. The key itself carries no role, so a
51
+ `writer → reader` downgrade denies the key's very next request in every token
52
+ mode. Effective authority is an intersection that can only narrow: the owner's
53
+ live role ∩ the key's immutable `access_mode` ∩ the matching audience ∩ the route
54
+ policy. The members are `None` unless `API_KEY_INTROSPECTION_ENABLED=true`.
55
+ - **`ApiKeyIntrospectionClient`** — the issuer introspection client, alongside
56
+ `RemoteRevocationClient` and following the same split (the SDK owns the schemas,
57
+ the transport lives here). Bounded connection pool and concurrency semaphore,
58
+ redirects disabled, a hard response-size cap enforced mid-read, a consecutive-
59
+ failure circuit breaker, and pool-timeout-bounded load shedding.
60
+ - **Dedicated API-key configuration group** — `API_KEY_INTROSPECTION_ENABLED`,
61
+ `API_KEY_INTROSPECTION_URL` (or derived from `INTROSPECTION_URL`),
62
+ `_SCHEMA_VERSION`, `_CONNECT_TIMEOUT`, `_READ_TIMEOUT`, `_POOL_TIMEOUT`,
63
+ `_MAX_CONCURRENCY`, `_MAX_RESPONSE_BYTES`, `_CIRCUIT_FAILURE_THRESHOLD`, and
64
+ `_CIRCUIT_RESET_SECONDS`. Enabling the feature without an endpoint,
65
+ `INTERNAL_CLIENT_ID`, `PRIVATE_API_SECRET`, or with a schema version this SDK
66
+ release cannot speak **fails at startup**, so a half-configured guard never
67
+ serves traffic.
68
+ - **Public exports** — `API_KEY_HEADER`, `ApiKeyIntrospectionError`,
69
+ `ApiKeyQuotaExceededError`, and `derive_api_key_introspection_url`.
70
+ - **`AuthDeps.get_current_active_writer`** — the JWT writer dependency, built by the
71
+ single `build_auth_deps()` call alongside the existing authenticated/admin/superuser
72
+ members. Requires a minimum role of `WRITER`, so `WRITER`/`ADMIN`/`SUPERADMIN` pass
73
+ and `USER`/`READER` receive the unchanged `403 "The user doesn't have enough
74
+ privileges"` response.
75
+ - **`audit_api_key_routes(app, bare_dependency=..., exempt_paths=...)`** — walks a
76
+ built `FastAPI` app's routes and reports any that depend directly on the bare
77
+ `get_current_api_key_principal` instead of a role-capped dependency
78
+ (`require_api_key_role(...)`'s output). Depending on the bare dependency alone
79
+ grants no capability, but a route wired to it directly is a wiring mistake this
80
+ catches at test time (§3.3.1). Returns `BareApiKeyDependency(path, methods)`
81
+ findings; an explicit `exempt_paths` allowlist covers intentional read-only
82
+ routes (e.g. a `/verify`-style endpoint).
83
+
84
+ ### Changed
85
+
86
+ - **All JWT role guards now authorize through the `auth-sdk-m8` policy helpers.**
87
+ `get_current_active_writer`/`_admin` delegate the hierarchy to
88
+ `has_minimum_role()` (replacing the local `RoleType.is_valid_role_auth()` call), so
89
+ the role ordering exists only in the SDK and the two guards cannot drift.
90
+
91
+ ### Security
92
+
93
+ - **`get_current_active_writer`/`_admin`/`_superuser` never answer from the positive
94
+ revocation cache (`REV-CACHE-01`, 3.5.4).** They now resolve the token through a
95
+ dedicated fresh-only path (`RemoteRevocationClient.is_revoked(..., bypass_cache=True)`)
96
+ instead of the shared `get_current_user` dependency, so the first role-sensitive
97
+ request after a revocation commits always observes the new state. The short-TTL
98
+ positive cache (`REVOCATION_CACHE_TTL_SECONDS`) now serves only the general
99
+ authenticated tier (`CurrentUser`/`get_current_user`); a bypassed lookup still
100
+ refreshes the shared cache entry so the general tier benefits from it too.
101
+ - **`get_current_active_superuser` now requires canonical dual evidence.** It delegates
102
+ to the SDK's `has_superuser_privileges()`, which demands `role == SUPERADMIN` **and**
103
+ `is_superuser=True` together. Previously the guard checked the `is_superuser` flag and
104
+ the role in two separate steps; the flag is no longer sufficient evidence on its own at
105
+ any step. A token whose claims disagree (e.g. `is_superuser=true` with a lower role) is
106
+ already rejected at validation by the SDK 3.0.0 model invariant — this guard-level check
107
+ is defense in depth for a principal that reaches the dependency inconsistent.
108
+ - **No role guard consults `is_superuser` for a role threshold.** The writer and admin
109
+ dependencies are pure role checks, so a stray `is_superuser=true` can never bypass them.
110
+ - **An API key never grants administrative or superuser authority.** No
111
+ `get_current_api_key_admin`/`_superuser` dependency exists and no setting creates
112
+ one — `require_api_key_role()` raises `ApiKeyCapabilityCeilingError` at wiring time
113
+ for any role above `WRITER`. An owner's `ADMIN`/`SUPERADMIN` role grants an API-key
114
+ request nothing beyond writer-level capability, and `is_superuser` on the principal
115
+ is evidence about the owner's record, never authority. (Earlier drafts of this work
116
+ described a remote `_superuser` specialization disabled behind a per-deployment
117
+ opt-in; it is **removed**, not disabled-by-default.)
118
+ - **The API-key path is fail-closed with no configurable escape.** It never reads
119
+ `ACCESS_REVOCATION_FAILURE_MODE`, whose `fail_open` value must not exist here: an
120
+ issuer outage, timeout, open circuit, shed request, oversized/malformed response,
121
+ unknown schema version, or an audience mismatch always denies with `503`. There is
122
+ no fallback to a cached principal, a locally stored role, or bare key validity.
123
+ - **No positive principal caching.** Every capability-bearing request introspects the
124
+ issuer; only HTTP connection pooling is reused. `auth_generation` on the response is
125
+ evidence for the current decision, never authorization to reuse the principal later.
126
+ - **No post-transmission retries.** Introspection consumes the key's quota, so only a
127
+ failure known to precede transmission (connection establishment) is retried once. A
128
+ read timeout or `5xx` is never replayed, since that could double-charge the owner.
129
+ - **The raw key is confined to the outbound payload.** It is a `SecretStr` in every
130
+ model, and the request body is built explicitly at the transmission point rather
131
+ than serialized generically (which would send the mask). It never appears in the
132
+ URL, query, logs, traces, metrics, exception text, or model `repr`. Redirects are
133
+ disabled so the key cannot be replayed to an unauthenticated host.
134
+ - **Denials carry no owner-state oracle.** An unknown, revoked, or expired key and
135
+ every missing/inactive/inconsistent-owner cause share the single generic
136
+ `401 Invalid or expired API key`, identical to the response for no key at all.
137
+
138
+ ---
139
+
140
+ ## [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)
141
+
142
+ > **MINOR — security remediation, no public API removed.** The only runtime behaviour change
143
+ > is a new fail-closed boot check: a production/strict consumer (`ENVIRONMENT=production` or
144
+ > `STRICT_PRODUCTION_MODE=true`) that still runs legacy single-shared-secret private auth
145
+ > (`INTROSPECTION_URL` set, `INTERNAL_CLIENT_ID` unset) now raises at settings construction.
146
+ > Such a consumer already could not authenticate against a hardened `fa-auth-m8` issuer
147
+ > (item 11.2a retired the shared-secret fallback), so this converts a latent deployment fault
148
+ > into an explicit error rather than changing any working configuration. Local/development is
149
+ > unaffected. `auth-sdk-m8` floor raised to `>=2.1.1,<3.0.0` (carries the SDK's latest
150
+ > dependency/CI maintenance release; no new SDK API is required).
151
+
152
+ ### Security
153
+
154
+ - **Legacy single-shared-secret private-auth mode is now development-only (OWASP remediation
155
+ item 11.2b).** `ConsumerServiceSettings` gained a `model_validator` that fails settings
156
+ construction when a production/strict consumer (`ENVIRONMENT=production` or
157
+ `STRICT_PRODUCTION_MODE=true`) has `INTROSPECTION_URL` configured but no
158
+ `INTERNAL_CLIENT_ID`. `fa-auth-m8` has retired the issuer's legacy single-shared-secret
159
+ private-API fallback (item 11.2a), so a production/strict consumer left in legacy
160
+ token-only mode is guaranteed to fail against a hardened issuer — this turns that latent
161
+ deployment fault into an explicit, fail-closed boot error. Legacy mode remains valid for
162
+ local/development.
163
+ - **Coherent per-consumer private-auth group validation.** The same validator now rejects
164
+ (in every environment) `SERVICE_TOKEN_EXCHANGE_ENABLED=true` without `INTERNAL_CLIENT_ID`
165
+ (exchange has no identity to present and would silently downgrade to legacy) and
166
+ `INTERNAL_CLIENT_ID` set without `PRIVATE_API_SECRET` (per-consumer mode needs its
167
+ bootstrap secret). No validation message echoes a secret value.
168
+ - **PyPI Trusted Publishing is now the sole publish mechanism (item 11.6).** The long-lived
169
+ `PYPI_API_TOKEN` secret was removed from `PiPy.yml`; the release job publishes via OIDC
170
+ (`id-token: write`) into a protected `pypi` environment. No API token is stored in the repo
171
+ or referenced by any workflow.
172
+ - **CI policy tests (item 11.7).** New `tests/test_ci_policy.py` asserts the supply-chain
173
+ posture in CI: no `PYPI_API_TOKEN` reference, `id-token: write` present, the protected
174
+ `pypi` environment is used, the duplicate `ci.yml` is gone, the secret-scan job is present,
175
+ every action is SHA-pinned in both workflows, and key runtime packages are pinned in
176
+ `constraints-all.txt`.
177
+ - **Locked dependency constraints (item 11.8).** `constraints.txt` and `constraints-all.txt`
178
+ are generated via `pip-compile` from public PyPI only (no custom index), giving CI a
179
+ reproducible, fully-pinned install closure.
180
+
181
+ ### Changed
182
+
183
+ - CI workflows consolidated — the secret-scan job was absorbed into `CI.yaml` and the
184
+ duplicate `ci.yml` removed; dependabot SHA bumps for `checkout`, `setup-python`,
185
+ `sbom-action`, `codecov-action`, and `gitleaks-action` applied across both workflows.
186
+ - `COMPAT_MATRIX` gains a `3.3` entry (`auth-sdk-m8 >=2.1.0,<3.0.0`, unchanged floor).
187
+
188
+ ### Docs
189
+
190
+ - README *Per-consumer internal auth* section and the defaults-by-layer table now document
191
+ that legacy mode is development-only and fatal under production/strict; removed wording
192
+ that implied the issuer still offers a shared-secret fallback. Compatibility table updated
193
+ with the `3.1.0`–`3.3.0` rows.
194
+
195
+ ---
196
+
8
197
  ## [3.2.0] — 2026-06-28 · ungated `/health` constant liveness body (item 9.4 Design B)
9
198
 
10
199
  > **MINOR — additive, backward-compatible.** The only behaviour change is in the ungated
@@ -0,0 +1,27 @@
1
+ # fastapi-m8
2
+
3
+ ## Layer
4
+ Platform (FastAPI application framework)
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+ Application factory wiring `auth-sdk-m8` into FastAPI services using `fa-auth-m8`.
10
+
11
+ ---
12
+
13
+ ## Rules
14
+ - No business logic
15
+ - Only reusable service scaffolding
16
+ - Must remain minimal and reusable across services
17
+ - No coupling to fa-auth-m8 or any domain service
18
+
19
+ ---
20
+
21
+ ## Workspace integration
22
+
23
+ When nested in a workspace, locate the nearest ancestor containing
24
+ `.workspace/policy.index.json` and apply its `python` policy.
25
+
26
+ If no workspace policy exists, use this file, `pyproject.toml`, repository
27
+ documentation and existing CI as the authoritative local context.