From dc86841547511ef5bb70a0de46a4269c82d5ba50 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Mon, 25 May 2026 08:46:48 +0800 Subject: [PATCH] fix: send joplin token in auth header --- CHANGELOG.md | 5 ++++- api/routes.py | 6 +++--- tests/test_webui_notes_sources.py | 33 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494f2eca..106b6230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Joplin notes drawer API calls now send the Web Clipper token in an `Authorization` header instead of placing it in the request URL query string. + ## [v0.51.132] — 2026-05-24 — Release DD (stage-batch14 — 4-PR replayed-context + interrupted-response + shutdown affordance + passkey opt-in) ### Added @@ -27,7 +31,6 @@ - CHANGELOG entries added for PR #2685 and PR #2824 (both originally missing despite functional code changes) - Deferred to follow-up: per-turn cumulative live-tool-prompt token cap (#2685 only added per-call cap; aggregate across many tool calls is a separate refactor). - **i18n parity**: 7 new shutdown-affordance keys added across all 11 non-en locales (it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr) so locale parity tests pass on first run. - ## [v0.51.131] — 2026-05-24 — Release DC (stage-batch13 — 6-PR notes-drawer + context-parity + PWA-swipe + locale polish) ### Added diff --git a/api/routes.py b/api/routes.py index 0b623ed8..8140f3f9 100644 --- a/api/routes.py +++ b/api/routes.py @@ -12534,7 +12534,7 @@ def _joplin_connection_from_config() -> tuple[str, str]: def _joplin_api_get(path: str, params: dict | None = None) -> dict: """Call the local Joplin Web Clipper API without logging credentials.""" from urllib.parse import urlencode - from urllib.request import urlopen + from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError base_url, token = _joplin_connection_from_config() @@ -12542,10 +12542,10 @@ def _joplin_api_get(path: str, params: dict | None = None) -> dict: raise ValueError("Joplin token is not configured") safe_path = "/" + str(path or "").lstrip("/") query = dict(params or {}) - query["token"] = token url = f"{base_url}{safe_path}?{urlencode(query)}" + request = Request(url, headers={"Authorization": f"token {token}"}) try: - with urlopen(url, timeout=8) as response: + with urlopen(request, timeout=8) as response: raw = response.read(2_000_000).decode("utf-8", errors="replace") except HTTPError as exc: raise ValueError(f"Joplin API returned HTTP {exc.code}") from None diff --git a/tests/test_webui_notes_sources.py b/tests/test_webui_notes_sources.py index bcf8c8fe..da5de925 100644 --- a/tests/test_webui_notes_sources.py +++ b/tests/test_webui_notes_sources.py @@ -132,6 +132,39 @@ def test_joplin_get_note_validates_id_and_truncates_body(monkeypatch): assert "Preview truncated" in note["body"] +def test_joplin_api_get_uses_authorization_header(monkeypatch): + from api import routes + + captured = {} + + class FakeResponse: + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + def read(self, _limit): + return b'{"ok": true}' + + def fake_urlopen(request, timeout): + captured["url"] = request.full_url + captured["authorization"] = request.get_header("Authorization") + captured["timeout"] = timeout + return FakeResponse() + + monkeypatch.setattr(routes, "_joplin_connection_from_config", lambda: ("http://127.0.0.1:41184", "secret-token")) + monkeypatch.setattr("urllib.request.urlopen", fake_urlopen) + + data = routes._joplin_api_get("/notes", {"query": "hello world"}) + + assert data == {"ok": True} + assert captured["timeout"] == 8 + assert "token=" not in captured["url"] + assert "query=hello+world" in captured["url"] + assert captured["authorization"] == "token secret-token" + + def test_joplin_recent_ai_notes_uses_configured_prefill_script(monkeypatch, tmp_path): from api import routes