Skip to content

Commit a7d3e86

Browse files
committed
docs(mkdocs): review readme and mkdocs flux schema
1 parent 385b5cb commit a7d3e86

File tree

8 files changed

+304
-13
lines changed

8 files changed

+304
-13
lines changed

README.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,105 @@ If they match, the key is valid.
164164

165165
Here is a diagram showing what happens after you initialize your API key service with a global prefix and delimiter when you provide an API key to the `.verify_key()` method.
166166

167-
<img src="./docs/schema.svg">
167+
```mermaid
168+
---
169+
title: "keyshield — verify_key() Flow"
170+
---
171+
flowchart LR
172+
%% ── Styles ──────────────────────────────────────────────
173+
classDef startNode fill:#90CAF9,stroke:#1565C0,color:#000
174+
classDef processNode fill:#FFF9C4,stroke:#F9A825,color:#000
175+
classDef rejectNode fill:#EF9A9A,stroke:#C62828,color:#000
176+
classDef acceptNode fill:#A5D6A7,stroke:#2E7D32,color:#000
177+
classDef cacheNode fill:#90CAF9,stroke:#1565C0,color:#000
178+
classDef noteStyle fill:#FFFDE7,stroke:#FBC02D,color:#555,font-size:11px
179+
180+
%% ── Entry ───────────────────────────────────────────────
181+
INPUT(["`**Api Key**
182+
_ak-7a74…10d-mAfP…bzw_`"]):::startNode
183+
184+
%% ── Main flow ───────────────────────────────────────────
185+
CACHED{"`**Is cached key?**
186+
_(hash api key to SHA-256
187+
to avoid Argon slow hashing)_`"}:::processNode
188+
189+
NULL_CHECK{"`**Is null or empty
190+
string value?**`"}:::processNode
191+
192+
SPLIT["`**Split string**
193+
_(by global_prefix)_`"]:::processNode
194+
195+
THREE_PARTS{"`**Has strictly
196+
3 parts?**`"}:::processNode
197+
198+
PREFIX_CHECK{"`**First part equals
199+
to global prefix?**`"}:::processNode
200+
201+
QUERY_DB["`**Query API Key
202+
by key_id**`"]:::processNode
203+
204+
COMPARE{"`**Compare db api key hash
205+
to received api key hash**`"}:::processNode
206+
207+
%% ── Outcomes ────────────────────────────────────────────
208+
REJECT(["`🔴 **Reject API Key**`"]):::rejectNode
209+
ACCEPT(["`🟢 **Accept API Key**`"]):::acceptNode
210+
CACHE_STORE(["`🔵 **Cache API Key**`"]):::cacheNode
211+
212+
%% ── Happy path (left → right) ──────────────────────────
213+
INPUT --> CACHED
214+
CACHED -- "no" --> NULL_CHECK
215+
NULL_CHECK -- "no" --> SPLIT
216+
SPLIT -- "exec" --> THREE_PARTS
217+
THREE_PARTS -- "yes" --> PREFIX_CHECK
218+
PREFIX_CHECK -- "yes" --> QUERY_DB
219+
QUERY_DB -- "found" --> COMPARE
220+
221+
%% ── Accept path ─────────────────────────────────────────
222+
CACHED -- "yes" --> ACCEPT
223+
COMPARE -- "equals" --> ACCEPT
224+
ACCEPT -- "`**APIKey.touch()**
225+
_(update last_used_at)_`" --> CACHE_STORE
226+
227+
%% ── Reject paths ────────────────────────────────────────
228+
NULL_CHECK -- "yes" --> REJECT
229+
SPLIT -- "Exception" --> REJECT
230+
THREE_PARTS -- "no" --> REJECT
231+
PREFIX_CHECK -- "no" --> REJECT
232+
QUERY_DB -- "not found" --> REJECT
233+
COMPARE -- "not equals" --> REJECT
234+
235+
%% ── Notes (annotations) ─────────────────────────────────
236+
NOTE_FORMAT["`**Format API Key**
237+
{global_prefix}: str
238+
{separator}: str
239+
{key_prefix}: UUID
240+
{separator}: str
241+
{key_secret}: UUID
242+
243+
_global_prefix = 'ak'_
244+
_separator = '-'_`"]:::noteStyle
245+
246+
NOTE_CACHE["`**Cache rules**
247+
InMemory / Redis
248+
• invalid if api key updated or deleted
249+
• invalid after 3600s`"]:::noteStyle
250+
251+
NOTE_ARGON["`**ArgonApiKeyHasher**
252+
• line salt apikey
253+
• global pepper api key`"]:::noteStyle
254+
255+
NOTE_SLEEP["`**sleep random (0.1s – 0.5s)**
256+
makes brute force less effective;
257+
randomization helps prevent
258+
timing attacks`"]:::noteStyle
259+
260+
NOTE_FORMAT ~~~ INPUT
261+
NOTE_CACHE ~~~ CACHE_STORE
262+
NOTE_ARGON ~~~ COMPARE
263+
NOTE_SLEEP ~~~ REJECT
264+
265+
```
168266

169267
### Mount the FastAPI router
170268

docs/index.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,105 @@ If they match, the key is valid.
6363

6464
Here is a diagram showing what happens after you initialize your API key service with a global prefix and delimiter when you provide an API key to the `.verify_key()` method.
6565

66-
<img src="./schema.svg">
66+
```mermaid
67+
---
68+
title: "keyshield — verify_key() Flow"
69+
---
70+
flowchart LR
71+
%% ── Styles ──────────────────────────────────────────────
72+
classDef startNode fill:#90CAF9,stroke:#1565C0,color:#000
73+
classDef processNode fill:#FFF9C4,stroke:#F9A825,color:#000
74+
classDef rejectNode fill:#EF9A9A,stroke:#C62828,color:#000
75+
classDef acceptNode fill:#A5D6A7,stroke:#2E7D32,color:#000
76+
classDef cacheNode fill:#90CAF9,stroke:#1565C0,color:#000
77+
classDef noteStyle fill:#FFFDE7,stroke:#FBC02D,color:#555,font-size:11px
78+
79+
%% ── Entry ───────────────────────────────────────────────
80+
INPUT(["`**Api Key**
81+
_ak-7a74…10d-mAfP…bzw_`"]):::startNode
82+
83+
%% ── Main flow ───────────────────────────────────────────
84+
CACHED{"`**Is cached key?**
85+
_(hash api key to SHA-256
86+
to avoid Argon slow hashing)_`"}:::processNode
87+
88+
NULL_CHECK{"`**Is null or empty
89+
string value?**`"}:::processNode
90+
91+
SPLIT["`**Split string**
92+
_(by global_prefix)_`"]:::processNode
93+
94+
THREE_PARTS{"`**Has strictly
95+
3 parts?**`"}:::processNode
96+
97+
PREFIX_CHECK{"`**First part equals
98+
to global prefix?**`"}:::processNode
99+
100+
QUERY_DB["`**Query API Key
101+
by key_id**`"]:::processNode
102+
103+
COMPARE{"`**Compare db api key hash
104+
to received api key hash**`"}:::processNode
105+
106+
%% ── Outcomes ────────────────────────────────────────────
107+
REJECT(["`🔴 **Reject API Key**`"]):::rejectNode
108+
ACCEPT(["`🟢 **Accept API Key**`"]):::acceptNode
109+
CACHE_STORE(["`🔵 **Cache API Key**`"]):::cacheNode
110+
111+
%% ── Happy path (left → right) ──────────────────────────
112+
INPUT --> CACHED
113+
CACHED -- "no" --> NULL_CHECK
114+
NULL_CHECK -- "no" --> SPLIT
115+
SPLIT -- "exec" --> THREE_PARTS
116+
THREE_PARTS -- "yes" --> PREFIX_CHECK
117+
PREFIX_CHECK -- "yes" --> QUERY_DB
118+
QUERY_DB -- "found" --> COMPARE
119+
120+
%% ── Accept path ─────────────────────────────────────────
121+
CACHED -- "yes" --> ACCEPT
122+
COMPARE -- "equals" --> ACCEPT
123+
ACCEPT -- "`**APIKey.touch()**
124+
_(update last_used_at)_`" --> CACHE_STORE
125+
126+
%% ── Reject paths ────────────────────────────────────────
127+
NULL_CHECK -- "yes" --> REJECT
128+
SPLIT -- "Exception" --> REJECT
129+
THREE_PARTS -- "no" --> REJECT
130+
PREFIX_CHECK -- "no" --> REJECT
131+
QUERY_DB -- "not found" --> REJECT
132+
COMPARE -- "not equals" --> REJECT
133+
134+
%% ── Notes (annotations) ─────────────────────────────────
135+
NOTE_FORMAT["`**Format API Key**
136+
{global_prefix}: str
137+
{separator}: str
138+
{key_prefix}: UUID
139+
{separator}: str
140+
{key_secret}: UUID
141+
142+
_global_prefix = 'ak'_
143+
_separator = '-'_`"]:::noteStyle
144+
145+
NOTE_CACHE["`**Cache rules**
146+
InMemory / Redis
147+
• invalid if api key updated or deleted
148+
• invalid after 3600s`"]:::noteStyle
149+
150+
NOTE_ARGON["`**ArgonApiKeyHasher**
151+
• line salt apikey
152+
• global pepper api key`"]:::noteStyle
153+
154+
NOTE_SLEEP["`**sleep random (0.1s – 0.5s)**
155+
makes brute force less effective;
156+
randomization helps prevent
157+
timing attacks`"]:::noteStyle
158+
159+
NOTE_FORMAT ~~~ INPUT
160+
NOTE_CACHE ~~~ CACHE_STORE
161+
NOTE_ARGON ~~~ COMPARE
162+
NOTE_SLEEP ~~~ REJECT
163+
164+
```
67165

68166
## Additional notes
69167

docs/schema.mermaid

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: "keyshield — verify_key() Flow"
3+
---
4+
flowchart LR
5+
%% ── Styles ──────────────────────────────────────────────
6+
classDef startNode fill:#90CAF9,stroke:#1565C0,color:#000
7+
classDef processNode fill:#FFF9C4,stroke:#F9A825,color:#000
8+
classDef rejectNode fill:#EF9A9A,stroke:#C62828,color:#000
9+
classDef acceptNode fill:#A5D6A7,stroke:#2E7D32,color:#000
10+
classDef cacheNode fill:#90CAF9,stroke:#1565C0,color:#000
11+
classDef noteStyle fill:#FFFDE7,stroke:#FBC02D,color:#555,font-size:11px
12+
13+
%% ── Entry ───────────────────────────────────────────────
14+
INPUT(["`**Api Key**
15+
_ak-7a74…10d-mAfP…bzw_`"]):::startNode
16+
17+
%% ── Main flow ───────────────────────────────────────────
18+
CACHED{"`**Is cached key?**
19+
_(hash api key to SHA-256
20+
to avoid Argon slow hashing)_`"}:::processNode
21+
22+
NULL_CHECK{"`**Is null or empty
23+
string value?**`"}:::processNode
24+
25+
SPLIT["`**Split string**
26+
_(by global_prefix)_`"]:::processNode
27+
28+
THREE_PARTS{"`**Has strictly
29+
3 parts?**`"}:::processNode
30+
31+
PREFIX_CHECK{"`**First part equals
32+
to global prefix?**`"}:::processNode
33+
34+
QUERY_DB["`**Query API Key
35+
by key_id**`"]:::processNode
36+
37+
COMPARE{"`**Compare db api key hash
38+
to received api key hash**`"}:::processNode
39+
40+
%% ── Outcomes ────────────────────────────────────────────
41+
REJECT(["`🔴 **Reject API Key**`"]):::rejectNode
42+
ACCEPT(["`🟢 **Accept API Key**`"]):::acceptNode
43+
CACHE_STORE(["`🔵 **Cache API Key**`"]):::cacheNode
44+
45+
%% ── Happy path (left → right) ──────────────────────────
46+
INPUT --> CACHED
47+
CACHED -- "no" --> NULL_CHECK
48+
NULL_CHECK -- "no" --> SPLIT
49+
SPLIT -- "exec" --> THREE_PARTS
50+
THREE_PARTS -- "yes" --> PREFIX_CHECK
51+
PREFIX_CHECK -- "yes" --> QUERY_DB
52+
QUERY_DB -- "found" --> COMPARE
53+
54+
%% ── Accept path ─────────────────────────────────────────
55+
CACHED -- "yes" --> ACCEPT
56+
COMPARE -- "equals" --> ACCEPT
57+
ACCEPT -- "`**APIKey.touch()**
58+
_(update last_used_at)_`" --> CACHE_STORE
59+
60+
%% ── Reject paths ────────────────────────────────────────
61+
NULL_CHECK -- "yes" --> REJECT
62+
SPLIT -- "Exception" --> REJECT
63+
THREE_PARTS -- "no" --> REJECT
64+
PREFIX_CHECK -- "no" --> REJECT
65+
QUERY_DB -- "not found" --> REJECT
66+
COMPARE -- "not equals" --> REJECT
67+
68+
%% ── Notes (annotations) ─────────────────────────────────
69+
NOTE_FORMAT["`**Format API Key**
70+
{global_prefix}: str
71+
{separator}: str
72+
{key_prefix}: UUID
73+
{separator}: str
74+
{key_secret}: UUID
75+
76+
_global_prefix = 'ak'_
77+
_separator = '-'_`"]:::noteStyle
78+
79+
NOTE_CACHE["`**Cache rules**
80+
InMemory / Redis
81+
• invalid if api key updated or deleted
82+
• invalid after 3600s`"]:::noteStyle
83+
84+
NOTE_ARGON["`**ArgonApiKeyHasher**
85+
• line salt apikey
86+
• global pepper api key`"]:::noteStyle
87+
88+
NOTE_SLEEP["`**sleep random (0.1s – 0.5s)**
89+
makes brute force less effective;
90+
randomization helps prevent
91+
timing attacks`"]:::noteStyle
92+
93+
NOTE_FORMAT ~~~ INPUT
94+
NOTE_CACHE ~~~ CACHE_STORE
95+
NOTE_ARGON ~~~ COMPARE
96+
NOTE_SLEEP ~~~ REJECT

mkdocs.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ theme:
4040
icon: material/weather-sunny
4141
name: Switch to light mode
4242
markdown_extensions:
43+
44+
- pymdownx.superfences:
45+
custom_fences:
46+
- name: mermaid
47+
class: mermaid
48+
format: !!python/name:pymdownx.superfences.fence_code_format
4349
- admonition
4450
- def_list
4551
- footnotes

src/keyshield/api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import sqlalchemy # noqa: F401
99
except ModuleNotFoundError as e: # pragma: no cover
1010
raise ImportError(
11-
"FastAPI and SQLAlchemy backend requires 'fastapi' and 'sqlalchemy'. "
12-
"Install it with: uv add keyshield[fastapi]"
11+
"FastAPI and SQLAlchemy backend requires 'fastapi' and 'sqlalchemy'. Install it with: uv add keyshield[fastapi]"
1312
) from e
1413

1514
from typing import Annotated, Awaitable, Callable, List, Optional, Union

src/keyshield/litestar_api.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ async def provide_svc() -> ApiKeyService:
2727
try:
2828
import litestar # noqa: F401
2929
except ModuleNotFoundError as e: # pragma: no cover
30-
raise ImportError(
31-
"Litestar integration requires 'litestar'. Install it with: uv add keyshield[litestar]"
32-
) from e
30+
raise ImportError("Litestar integration requires 'litestar'. Install it with: uv add keyshield[litestar]") from e
3331

3432
from typing import Awaitable, Callable, List, Optional
3533

src/keyshield/repositories/sql.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
try:
55
import sqlalchemy # noqa: F401
66
except ModuleNotFoundError as e:
7-
raise ImportError(
8-
"SQLAlchemy backend requires 'sqlalchemy'. Install it with: uv add keyshield[sqlalchemy]"
9-
) from e
7+
raise ImportError("SQLAlchemy backend requires 'sqlalchemy'. Install it with: uv add keyshield[sqlalchemy]") from e
108

119

1210
from datetime import datetime

src/keyshield/services/cached.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
try:
22
import aiocache # noqa: F401
33
except ModuleNotFoundError as e:
4-
raise ImportError(
5-
"CachedApiKeyService requires 'aiocache'. Install it with: uv add keyshield[aiocache]"
6-
) from e
4+
raise ImportError("CachedApiKeyService requires 'aiocache'. Install it with: uv add keyshield[aiocache]") from e
75

86
import hashlib
97
from typing import List, Optional

0 commit comments

Comments
 (0)