Skip to content

fix(panoramax): load pictures from every instance in the federation - #766

Merged
Medformatik merged 3 commits into
mainfrom
fix/panoramax-federated-images
Nov 4, 2026
Merged

Medformatik merged 3 commits into
mainfrom
fix/panoramax-federated-images

Conversation

@Medformatik

@Medformatik Medformatik commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

What changed and why

Panoramax is a federation. Its catalogue (api.panoramax.xyz) lists pictures from many instances, and each item's asset links point at whichever instance or storage bucket holds the file. The image proxy admitted only api.panoramax.xyz and panoramax.openstreetmap.fr, so every picture from another instance answered 403 Domain not allowed: the street-level viewer showed a black frame and place galleries silently left those photos out. In a sample of catalogue searches, that was 44% of Berlin's pictures (IGN and MapComplete) and 85% of Hamburg's (IGN), so this is common in Germany, not an edge case. An allowlist cannot keep up: IGN alone serves its files from an OVH S3 bucket, and the federation adds instances without notice.

The catalogue's own per-picture endpoint, /api/pictures/<uuid>/<size>.jpg, redirects to wherever the file lives. This uses it:

  • Integrations. The street-level and place-photo Panoramax integrations hand out that catalogue URL for each size an item has, through a shared panoramaxPictureAssetUrl in core. An item without a picture UUID keeps its asset link.
  • Image proxy. A request that starts at an allowlisted Panoramax host's per-picture endpoint may follow at most the two redirects a picture takes to reach hosts no allowlist names: the first only from the requested catalogue URL, the second only from a per-picture endpoint (an instance's, as IGN's leads to its storage bucket). Targets must be HTTPS on the default port; a plain file cannot redirect onward, and nothing changes for any other request. Every hop still connects only to public addresses, and the response must still be a bounded image within the client's byte budget.

The request URL stays on an allowlisted host, so the place-card photo filter, which checks hosts, keeps accepting these photos. The manifests already describe images from other instances as licensed per item and loaded server-side, so their disclosures are unchanged.

Verification

  • Live, through the real image-proxy route registered on a local Fastify app against the real catalogue: an IGN picture in Hamburg (catalogue → panoramax.ign.fr → OVH S3), a MapComplete picture by Cologne Cathedral and an OSM-France picture each returned 200 image/jpeg for thumb, sd and hd. A raw MapComplete file URL is still refused with 403.
  • New tests: the proxy follows the catalogue's and an instance's picture redirects in order, and refuses a redirect from a plain file, a third federated redirect even between picture endpoints, a redirect over HTTP or to another port, and any federated redirect for a request that is not a catalogue picture; both providers emit catalogue URLs and keep the asset link for non-UUID ids; the core helper rejects non-UUID ids.
  • Vitest for apps/api/src/routes, the photo and Panoramax integrations, packages/core/src/utils and packages/integration-framework: 2,678 passed. tsc for apps/api (with its integrations config) and packages/core, biome.
  • Not checked: the viewer and a place gallery on a device. That needs this API deployed; I'll check both on openmapx.com after it is.

Release and upgrade impact

Product impact: ordinary

No package release: the only shared-core change adds panoramaxPictureAssetUrl and its type next to panoramaxAuthor; a tsup build of @openmapx/extension-cli produces byte-identical dist/ files with and without it.

Panoramax's catalogue lists pictures from many instances, and each item's
asset links point at whichever instance or storage bucket holds the file.
The image proxy admitted only api.panoramax.xyz and the OSM-France
instance, so pictures from IGN, MapComplete and other instances answered
"Domain not allowed": in a sample, 44% of Berlin's and 85% of Hamburg's.
They never showed in the street-level viewer, and place photos dropped
them.

Both Panoramax integrations now load images through the catalogue's own
per-picture endpoint, which redirects to the file. For a request that
starts at that endpoint on an allowlisted Panoramax host, the proxy
follows a redirect from a per-picture endpoint to any HTTPS host on the
default port. Every hop still connects only to public addresses, and the
response must still be a bounded image; a plain file cannot redirect
onward, and nothing else changes for other hosts.
@Medformatik
Medformatik enabled auto-merge (squash) November 4, 2026 00:04
The first version trusted a redirect from any per-picture-shaped path, so a
federated instance could chain the proxy through further hops up to the
fetch's redirect limit. The policy is now per request and sees every
redirect in order: the first only from the requested catalogue URL, the
second only from a per-picture endpoint (an instance's, as IGN's leads to
its bucket), and none after that.
@Medformatik
Medformatik disabled auto-merge November 4, 2026 00:20
@Medformatik
Medformatik merged commit f80ec90 into main Nov 4, 2026
27 checks passed
@Medformatik
Medformatik deleted the fix/panoramax-federated-images branch November 4, 2026 00:34
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants