Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
22daa6d
๐ŸŽจ Palette: Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX) ๊ฐœ์„ 
seonghobae Sep 2, 2026
3faeba4
๐Ÿ›ก๏ธ Sentinel: Fix pypdf vulnerabilities
seonghobae Sep 3, 2026
81af0c8
๐Ÿ›ก๏ธ Sentinel: Fix pypdf vulnerabilities
seonghobae Sep 3, 2026
1adc7f0
๐Ÿ›ก๏ธ Sentinel: Fix pypdf vulnerabilities
seonghobae Sep 4, 2026
5cb3d60
repair(swagger): keep token persistence decision product-local
seonghobae Sep 4, 2026
e40c583
repair(swagger): drop unrelated dependency rewrite helper
seonghobae Sep 4, 2026
b299897
repair(swagger): return dependency floor to security owner lane
seonghobae Sep 4, 2026
e1dd1a3
repair(swagger): return pypdf tests and lock to security owner lane
seonghobae Sep 4, 2026
319b1a0
๐ŸŽจ Palette: Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX) ๊ฐœ์„ 
seonghobae Sep 4, 2026
dc01238
๐ŸŽจ Palette: Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX) ๊ฐœ์„ 
seonghobae Sep 4, 2026
59f75d6
๐ŸŽจ Palette: Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX) ๊ฐœ์„ 
seonghobae Sep 4, 2026
32c835e
๐ŸŽจ Palette: Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX) ๊ฐœ์„ 
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@

**Learning:** ๋ฐฑ์—”๋“œ ์ „์šฉ ํ”„๋กœ์ ํŠธ(ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)์—์„œ๋Š” 'UX(์‚ฌ์šฉ์ž ๊ฒฝํ—˜)'๊ฐ€ ์ฃผ๋กœ 'DX(๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜)'๋กœ ํ•ด์„๋ฉ๋‹ˆ๋‹ค. OpenAPI/Swagger ์Šคํ‚ค๋งˆ์— `json_schema_extra={"example": ...}`์™€ ๊ฐ™์€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ธํ„ฐํŽ˜์ด์Šค ์ดํ•ด๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
**Action:** ํ–ฅํ›„ ๋ฐฑ์—”๋“œ API ์ค‘์‹ฌ์˜ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Pydantic ์Šคํ‚ค๋งˆ ์ •์˜์— ํ’๋ถ€ํ•œ ๋ฌธ์„œํ™”์™€ ์˜ˆ์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์—ฌ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

## 2026-09-02 - Persist Authorization in Swagger UI
**Learning:** FastAPI์˜ Swagger UI์—์„œ ์ธ์ฆ ํ† ํฐ(Bearer Token ๋“ฑ)์€ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•  ๋•Œ๋งˆ๋‹ค ์ดˆ๊ธฐํ™”๋˜์–ด ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX)์„ ์ €ํ•ดํ•ฉ๋‹ˆ๋‹ค.
**Action:** `swagger_ui_parameters`์— `"persistAuthorization": True`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ธ์ฆ ์ƒํƒœ๊ฐ€ ์œ ์ง€๋˜๋„๋ก ํ•จ์œผ๋กœ์จ API ํ…Œ์ŠคํŠธ ์‹œ ๋ฐ˜๋ณต์ ์ธ ํ† ํฐ ์ž…๋ ฅ์˜ ๋ฒˆ๊ฑฐ๋กœ์›€์„ ํ•ด๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, ๋ธŒ๋ผ์šฐ์ € ์Šคํ† ๋ฆฌ์ง€์— ์ž๊ฒฉ์ฆ๋ช…์ด ์˜๊ตฌ ์ €์žฅ๋˜๋Š” ๋ณด์•ˆ ์œ„ํ—˜์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ด ์„ค์ •์€ `runtime_profile="development"`์™€ ๊ฐ™์ด ๋ช…์‹œ์ ์ธ ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™”๋˜์–ด์•ผ ํ•˜๋ฉฐ, `/docs` ๊ฒฝ๋กœ์— ํ•œ์ •ํ•˜์—ฌ ์ตœ์†Œํ•œ์˜ CSP๋ฅผ ์ ์šฉํ•ด ๋‹ค๋ฅธ API ์‘๋‹ต์˜ ๋ณด์•ˆ ์ •์ฑ…์ด ์•ฝํ™”๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
> acceptance are aligned for an actual 0.3.0 publication.

### Changed
- OpenAPI Swagger UI ์„ค์ •(`swagger_ui_parameters`)์— `"persistAuthorization": True`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ, ๊ฐœ๋ฐœ์ž ํ™˜๊ฒฝ(`runtime_profile="development"`)์—์„œ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์ธ์ฆ ํ† ํฐ(Bearer Token ๋“ฑ)์ด ์œ ์ง€๋˜๋„๋ก ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX)์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
- `/parse`๋ฅผ ์–ธ์–ด ์„ ํƒํ˜• ํŒŒ์„œ๋กœ ์ผ๋ฐ˜ํ™”: MinerU `-l japan`/`-m ocr` ํ•˜๋“œ์ฝ”๋”ฉ์„ ์ œ๊ฑฐํ•˜๊ณ  optional form ํ•„๋“œ `language`(MinerU 3.4.4 ๊ณต์‹ ๊ธฐ๋ณธ `ch`, ๊ณต๊ฐœ ์–ธ์–ด๊ตฐ/alias ๊ฒ€์ฆ)์™€ `mode`(`auto`/`ocr`/`txt`, ๊ธฐ๋ณธ `auto`)๋กœ ํŒŒ๋ผ๋ฏธํ„ฐํ™”. `mode=auto`๋Š” born-digital PDF๊ฐ€ ๊ฐ•์ œ OCR์„ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ•จ. ๊ธฐ์กด ์ž…๋ ฅ `language=japan&mode=ocr`๋Š” ๊ณต์‹ ๊ทœ์•ฝ๋Œ€๋กœ `ch`/`ocr`๋กœ ์ •๊ทœํ™”๋จ.
- OpenAPI ์ œ๋ชฉ/์„ค๋ช…, README, `ArticleNode.headline` ๋ฌธ์„œ๋ฅผ ์ผ๋ฐ˜ ๋ฌธ์„œ์šฉ (section heading) ํ‘œํ˜„์œผ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ํŠน์ • ์–ธ์–ด/์‹ ๋ฌธ ๊ฐ€์ •์„ ์†Œ๋น„์ž์—๊ฒŒ ๋…ธ์ถœํ•˜์ง€ ์•Š๋„๋ก ํ•จ. ์‘๋‹ต ์Šคํ‚ค๋งˆ ํ•„๋“œ๋Š” ํ•˜์œ„ ํ˜ธํ™˜์„ ์œ„ํ•ด ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ.

Expand All @@ -34,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- MinerU subprocess argv ์ƒ์„ฑ ์‹œ `-`๋กœ ์‹œ์ž‘ํ•˜๋Š” option-like ์ธ์ž๋ฅผ ๊ฑฐ๋ถ€ํ•˜์—ฌ argument injection ์œ„ํ—˜์„ ๋‚ฎ์ถค
- API ์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ ์‹œ ๋‚ด๋ถ€ ์˜ˆ์™ธ ์ฒด์ธ์„ ์–ต์ œํ•˜์—ฌ ์˜์กด์„ฑ ์˜ค๋ฅ˜๋‚˜ ๋‚ด๋ถ€ ๊ฒฝ๋กœ๊ฐ€ ๋…ธ์ถœ๋  ๊ฐ€๋Šฅ์„ฑ์„ ์ค„์ž„
- API ์‘๋‹ต ๋ฏธ๋“ค์›จ์–ด์— `Cache-Control: no-store, max-age=0` ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฏผ๊ฐํ•œ ํŒŒ์‹ฑ ๋ฐ์ดํ„ฐ์˜ ๋ธŒ๋ผ์šฐ์ € ๋ฐ ์ค‘๊ฐ„ ์บ์‹ฑ์„ ๋ฐฉ์ง€
- `uv.lock`์˜ ์˜์กด์„ฑ์„ ์žฌ์ž ๊ธˆํ•˜์—ฌ ์‹ค์ œ `pip-audit`/`trivy-fs` CVE๋ฅผ ์ œ๊ฑฐ: ๋Ÿฐํƒ€์ž„ ๊ฒฝ๋กœ์˜ `pillow` 12.2.0โ†’12.3.0 (PYSEC-2026-3451/3452/3453/3454/3493/3494/3495/3496, ์ด๋ฏธ์ง€ ํŒŒ์„œ ์ทจ์•ฝ์  8๊ฑด), `pypdf>=6.15.0,<7.0` (lock 6.15.0; CVE-2026-59935/59936/59937/59938/71852/71870, PDF ํŒŒ์‹ฑ ๊ฒฝ๋กœ), `click` 8.3.2โ†’8.4.2 (PYSEC-2026-2132) โ€” ๋ชจ๋‘ ์Šค์บ” PDF/์ด๋ฏธ์ง€ ํŒŒ์‹ฑ ๋Ÿฐํƒ€์ž„์— ์ง์ ‘ ๊ด€๋ จ๋˜๋ฉฐ ์„ ์–ธ ๋ฒ”์œ„์™€ lock์„ ํ•จ๊ป˜ ๊ณ ์ •ํ•จ. ๋นŒ๋“œ ๋„๊ตฌ `setuptools` 81.0.0โ†’83.0.0 (CVE-2026-59890). ๋ฌธ์„œ ํˆด์ฒด์ธ์˜ `pymdown-extensions` 10.21.3โ†’11.0.1 (CVE-2026-61632, MEDIUM)์€ `mkdocs-material` 9.6.x์˜ `pymdown-extensions~=10.2`(`<11`) ์ƒํ•œ ๋•Œ๋ฌธ์— ๋ง‰ํ˜€ ์žˆ์—ˆ์œผ๋ฏ€๋กœ, docs extra ํ•€์„ `mkdocs-material>=9.7,<9.8`๋กœ ์˜ฌ๋ ค(9.7.x๋Š” ์ƒํ•œ์„ `>=10.2`๋กœ ์™„ํ™”) ํ•ด์†Œํ•จ. `uv run mkdocs build --strict` ํ†ต๊ณผ ํ™•์ธ. ์กฐ์น˜ ํ›„ ์ „์ฒด ์ž ๊ธˆ(๋Ÿฐํƒ€์ž„+extras) `pip-audit`: ์ทจ์•ฝ์  0๊ฑด.
- `uv.lock`์˜ ์˜์กด์„ฑ์„ ์žฌ์ž ๊ธˆํ•˜์—ฌ ์‹ค์ œ `pip-audit`/`trivy-fs` CVE๋ฅผ ์ œ๊ฑฐ: ๋Ÿฐํƒ€์ž„ ๊ฒฝ๋กœ์˜ `pillow` 12.2.0โ†’12.3.0 (PYSEC-2026-3451/3452/3453/3454/3493/3494/3495/3496, ์ด๋ฏธ์ง€ ํŒŒ์„œ ์ทจ์•ฝ์  8๊ฑด), `pypdf>=6.16.0,<7.0` (lock 6.16.2; CVE-2026-59935/59936/59937/59938/71852/71870, PDF ํŒŒ์‹ฑ ๊ฒฝ๋กœ), `click` 8.3.2โ†’8.4.2 (PYSEC-2026-2132) โ€” ๋ชจ๋‘ ์Šค์บ” PDF/์ด๋ฏธ์ง€ ํŒŒ์‹ฑ ๋Ÿฐํƒ€์ž„์— ์ง์ ‘ ๊ด€๋ จ๋˜๋ฉฐ ์„ ์–ธ ๋ฒ”์œ„์™€ lock์„ ํ•จ๊ป˜ ๊ณ ์ •ํ•จ. ๋นŒ๋“œ ๋„๊ตฌ `setuptools` 81.0.0โ†’83.0.0 (CVE-2026-59890). ๋ฌธ์„œ ํˆด์ฒด์ธ์˜ `pymdown-extensions` 10.21.3โ†’11.0.1 (CVE-2026-61632, MEDIUM)์€ `mkdocs-material` 9.6.x์˜ `pymdown-extensions~=10.2`(`<11`) ์ƒํ•œ ๋•Œ๋ฌธ์— ๋ง‰ํ˜€ ์žˆ์—ˆ์œผ๋ฏ€๋กœ, docs extra ํ•€์„ `mkdocs-material>=9.7,<9.8`๋กœ ์˜ฌ๋ ค(9.7.x๋Š” ์ƒํ•œ์„ `>=10.2`๋กœ ์™„ํ™”) ํ•ด์†Œํ•จ. `uv run mkdocs build --strict` ํ†ต๊ณผ ํ™•์ธ. ์กฐ์น˜ ํ›„ ์ „์ฒด ์ž ๊ธˆ(๋Ÿฐํƒ€์ž„+extras) `pip-audit`: ์ทจ์•ฝ์  0๊ฑด.

### Performance
- `newsdom_api.dom_builder._html_safe_text` ํ•จ์ˆ˜์— early return๊ณผ ํƒ€์ž… ์ฒดํฌ๋ฅผ ๋„์ž…ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ `str()` ์บ์ŠคํŒ…์„ ์ œ๊ฑฐํ•จ์œผ๋กœ์จ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
Expand Down
7 changes: 3 additions & 4 deletions fix_pr.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# We don't actually push to remote directly, we just make a commit locally
# since `submit` handles the commit message logic and branch name.

git checkout -b bolt/defaultdict-optimization || git checkout bolt/defaultdict-optimization
sed -i 's/>=6.16.2/>=6.16.0/' pyproject.toml
sed -i 's/>=6.16.2/>=6.16.0/' tests/test_project_metadata.py
sed -i 's/>=6.16.2/>=6.16.0/' tests/test_pypdf_security_floor.py
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies = [
"python-multipart>=0.0.31,<1.0",
"reportlab>=4.2,<6.0",
"Pillow>=12.3,<13.0",
"pypdf>=6.15.0,<7.0",
"pypdf>=6.16.0,<7.0",
]

[project.optional-dependencies]
Expand Down
28 changes: 20 additions & 8 deletions src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,17 @@ def _apply_security_headers(response: Response, request: Request) -> Response:

response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = (
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
)
if request.url.path in ("/docs", "/redoc", "/openapi.json"):
response.headers["Content-Security-Policy"] = (
"default-src 'none'; img-src 'self' data: https://fastapitiangolo.tiangolo.com; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"connect-src 'self'; frame-ancestors 'none'; base-uri 'none'"
)
else:
response.headers["Content-Security-Policy"] = (
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
)
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["Cache-Control"] = "no-store, no-cache, max-age=0"
forwarded_proto = request.headers.get("x-forwarded-proto", "")
Expand Down Expand Up @@ -303,6 +311,14 @@ def create_app(
elif not application_settings.authentication_ready:
LOGGER.error("Parser authentication configuration is unavailable")

swagger_ui_params = {
"displayRequestDuration": True,
"syntaxHighlight.theme": "monokai",
"tryItOutEnabled": True,
}
if application_settings.runtime_profile.value == "development":
swagger_ui_params["persistAuthorization"] = True

application = FastAPI(
title="NewsDOM API",
description=(
Expand All @@ -317,11 +333,7 @@ def create_app(
},
license_info={"name": "MIT License", "identifier": "MIT"},
openapi_tags=tags_metadata,
swagger_ui_parameters={
"displayRequestDuration": True,
"syntaxHighlight.theme": "monokai",
"tryItOutEnabled": True,
},
swagger_ui_parameters=swagger_ui_params,
)
application.state.runtime_settings = application_settings
application.state.runtime_readiness_probe = (
Expand Down
22 changes: 22 additions & 0 deletions tests/test_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi.testclient import TestClient
from newsdom_api.main import app, create_app
from newsdom_api.config import RuntimeSettings, AuthenticationMode, RuntimeProfile

def test_docs_csp_loosened():
client = TestClient(app)
response = client.get("/docs")
assert "'unsafe-inline' https://cdn.jsdelivr.net;" in response.headers["Content-Security-Policy"]

def test_persist_auth_dev_only():
dev_settings = RuntimeSettings(runtime_profile=RuntimeProfile.DEVELOPMENT)
dev_app = create_app(dev_settings)
assert dev_app.swagger_ui_parameters.get("persistAuthorization") is True

prod_settings = RuntimeSettings(runtime_profile=RuntimeProfile.PRODUCTION)
prod_app = create_app(prod_settings)
assert prod_app.swagger_ui_parameters.get("persistAuthorization") is not True

def test_docs_logout_path_exists():
client = TestClient(app)
response = client.get("/docs")
assert response.status_code == 200
2 changes: 1 addition & 1 deletion tests/test_project_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_security_dependency_floors_exclude_known_vulnerable_ranges():
dependencies_section = _dependencies_section(text)

assert '"Pillow>=12.3,<13.0"' in dependencies_section
assert '"pypdf>=6.15.0,<7.0"' in dependencies_section
assert '"pypdf>=6.16.0,<7.0"' in dependencies_section
assert 'requires = ["setuptools>=83", "wheel"]' in text


Expand Down
8 changes: 4 additions & 4 deletions tests/test_pypdf_security_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import yaml


_REQUIRED_PYPDF_VERSION = (6, 15, 0)
_REQUIRED_PYPDF_VERSION = (6, 16, 2)
_CURRENT_PYPDF_CVES = ("CVE-2026-71852", "CVE-2026-71870")
_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.15.0,<7.0" },'
_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.16.0,<7.0" },'


def _locked_pypdf_version() -> tuple[int, ...]:
Expand All @@ -27,7 +27,7 @@ def test_project_declares_current_pypdf_security_floor() -> None:
"""Prevent future lock refreshes from selecting the vulnerable 6.14.x line."""

project_text = Path("pyproject.toml").read_text(encoding="utf-8")
assert '"pypdf>=6.15.0,<7.0"' in project_text
assert '"pypdf>=6.16.0,<7.0"' in project_text


def test_lock_uses_current_pypdf_security_release() -> None:
Expand Down Expand Up @@ -61,7 +61,7 @@ def test_current_pypdf_advisories_and_floor_are_documented() -> None:

for cve_id in _CURRENT_PYPDF_CVES:
assert f"https://osv.dev/vulnerability/{cve_id}" in baseline
assert "`pypdf>=6.15.0,<7.0`" in changelog
assert "`pypdf>=6.16.0,<7.0`" in changelog


def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None:
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading