diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2680d8..4388bae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Auxiliary model settings now reject unknown task slots instead of allowing arbitrary keys under `config.yaml`'s `auxiliary` block. Valid slots and the `__reset__` sentinel continue to work. - Update Now no longer reports success or enters the restart wait flow when no WebUI or Agent update target is selected. - Cached WebUI agents no longer overwrite `prefill_messages` with an empty list when a later request does not include explicit prefill context. +- 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) 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