From 017dcea9ead21a756dba778445af8886084de2ca Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 16 Sep 2026 14:24:31 -0500 Subject: [PATCH] feat(view): links open the viewer's /view page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xbrlkit view` and the MCP `view_filing` tool now open `https://xbrlkit.com/view?url=…` instead of `/?url=…`. The viewer reads both paths the same way, so nothing changes for the person opening it. The path is what lets the viewer count opens from xbrlkit apart from report links on the web: its page-view analytics record the path and drop the query string, so a query-string marker would never be seen. Earlier releases keep opening `/?url=`, which the viewer serves for good. The CORS header is unchanged: it names the viewer's origin, and the path does not enter into it. --- README.md | 3 ++- tests/test_serve.py | 2 +- tests/test_view.py | 14 ++++++++++++-- xbrlkit/view.py | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42d26af..861c09e 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,8 @@ uvx xbrlkit view NVDA A browser cannot be handed a local path — `file://` is unreachable from an https page, and a file input cannot be pre-populated — so this serves the document instead, on an ephemeral loopback port with an unguessable path, and -opens `xbrlkit.com/?url=…` pointing at it. `http://127.0.0.1` is a +opens `xbrlkit.com/view?url=…` pointing at it (earlier releases open +`xbrlkit.com/?url=…`, which keeps working). `http://127.0.0.1` is a potentially trustworthy origin, so the https page may read it; the CORS header names the viewer's origin and no other. The document is readable there, by that origin, until you press Ctrl-C. `--viewer` points at a different build. diff --git a/tests/test_serve.py b/tests/test_serve.py index b075658..aa544ab 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -755,7 +755,7 @@ def test_view_filing_serves_it_for_the_viewer(loaded: LoadedFiling) -> None: try: out = tools.view_filing(loaded, "holon", viewers) assert out["filing"] == loaded.id and out["bytes"] > 0 - assert out["viewer_url"].startswith(f"{DEFAULT_VIEWER}/?url=") + assert out["viewer_url"].startswith(f"{DEFAULT_VIEWER}/view?url=") assert out["document_url"] in out["viewer_url"] assert out["document_url"].startswith("http://127.0.0.1:") with pytest.raises(tools.ToolError): diff --git a/tests/test_view.py b/tests/test_view.py index 1bb273f..afa8271 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -20,6 +20,7 @@ from xbrlkit.view import ( DEFAULT_VIEWER, + VIEWER_PAGE, ViewerHost, origin_of, serve_report, @@ -71,13 +72,22 @@ def test_the_path_carries_an_unguessable_token(served) -> None: def test_the_viewer_url_names_the_document(served) -> None: - assert served.viewer_url.startswith(f"{DEFAULT_VIEWER}/?url=") + assert served.viewer_url.startswith(f"{DEFAULT_VIEWER}/view?url=") assert served.file_url in served.viewer_url +def test_the_link_opens_the_viewers_view_page() -> None: + # `/view`, not `/`: the viewer's analytics see the path and drop the query, + # so the path is what tells an open from xbrlkit apart from a web link. + assert VIEWER_PAGE == "view" + assert viewer_url(DEFAULT_VIEWER, "http://127.0.0.1:1/t/a.json") == ( + "https://xbrlkit.com/view?url=http://127.0.0.1:1/t/a.json" + ) + + def test_viewer_url_and_origin_helpers() -> None: assert viewer_url("https://example.test/", "http://127.0.0.1:1/a.json") == ( - "https://example.test/?url=http://127.0.0.1:1/a.json" + "https://example.test/view?url=http://127.0.0.1:1/a.json" ) assert origin_of("https://example.test/path?q=1") == "https://example.test" with pytest.raises(ValueError): diff --git a/xbrlkit/view.py b/xbrlkit/view.py index 60b906d..3ecee2f 100644 --- a/xbrlkit/view.py +++ b/xbrlkit/view.py @@ -17,6 +17,12 @@ The only machinery this needs, then, is a CORS header, because the viewer's origin is not this one. +**The link.** It opens the viewer's ``/view`` page, ``https://xbrlkit.com/view?url=…``. +The viewer reads ``/view`` and ``/`` the same way: a ``?url=`` on either opens the +document. The path is what separates an open from here from a report link on the +web, because the viewer's page-view analytics record the path and drop the query +string. Earlier releases open ``/?url=…``, which the viewer keeps serving for good. + **Posture.** Loopback only, one document per path, and each path carries an unguessable token — so the document is readable by the viewer origin, for as long as the process runs, and not by every other page the browser has open. @@ -40,6 +46,10 @@ # origin, so the old name must keep working for those installs. DEFAULT_VIEWER = "https://xbrlkit.com" +# The viewer page a link from xbrlkit opens: `/view` rather than `/`, so the +# viewer can count these opens apart from report links on the web. +VIEWER_PAGE = "view" + _CONTENT_TYPES = { ".jsonld": "application/ld+json", ".json": "application/json", @@ -55,8 +65,8 @@ def origin_of(url: str) -> str: def viewer_url(viewer: str, file_url: str) -> str: - """The viewer page that opens ``file_url`` — the ``?url=`` link.""" - return f"{viewer.rstrip('/')}/?url={quote(file_url, safe=':/')}" + """The viewer's ``/view`` page, opening ``file_url`` — the ``?url=`` link.""" + return f"{viewer.rstrip('/')}/{VIEWER_PAGE}?url={quote(file_url, safe=':/')}" class _Handler(BaseHTTPRequestHandler):