Closed bug report

Private component pages fail in the 3D viewer: unauthenticated asset fetch returns PAGE_NOT_FOUND

barrett-land · 2mo ago ·closed by Colby Knox

The native 3D viewer cannot render a private component/molecule page. It loads the model in an unauthenticated context, so the wiki correctly refuses the private page and the viewer shows a raw error instead of the model.

What a user sees in the 3D viewer:

{"error":"Page not found","code":"PAGE_NOT_FOUND","data":{"owner":null,"slug":"large-automotive-eth-switch-molecule"}}

Root cause: the page is private and the viewer's fetch is not authenticated. Access matrix on adom/large-automotive-eth-switch-molecule (private to the adom org):

Request Authenticated Unauthenticated
adom/<slug> page 200 404
bare <slug> 200 404 (this is the error above)
the .glb asset 200 404
public molecule adom/bme690 (contrast) 200 200

Public molecules render because they are readable logged out. Private ones fail because the viewer's request carries no session.

Two things to fix:

  1. Primary: the viewer must fetch the page and its assets with the viewer's own credentials. Forward the logged-in session into the viewer iframe and its model/manifest fetches, so an authorized org member can view a private component page.
  2. Secondary: the viewer resolves the page by a bare slug (note owner:null). It should use the owner-qualified <owner>/<slug>. Even owner-qualified 404s while unauthenticated, so this alone will not fix it, but it makes the error accurate.

Repro: open the 3D viewer for any private component page (for example adom/large-automotive-eth-switch-molecule). Public molecules render; private ones show the PAGE_NOT_FOUND error.

Not a data problem: the page assets are intact and load fine when authenticated (GLB 256 KB, plus the PCB and hierarchical schematics). Only the unauthenticated viewer fetch fails.

2 Replies

Colby Knox · 2mo ago

Investigated end to end. Verdict: the primary claim does not reproduce; the secondary claim is real and I am fixing it now.

Primary ("the viewer must fetch with the viewer's own credentials; an authorized member cannot view a private page"): works as designed today. Verified three ways:

  1. Bearer-authed request to the viewer route on a private test page: 200, viewer HTML.
  2. Real logged-in browser session (cookie via the browser-session flow, headless Chromium on staging): the private overview renders, the viewer renders with a live canvas, and the GLB blob fetch returns 200. The iframe and its asset fetches are same-origin, so the session cookie rides along, and the server accepts cookie auth on every one of these routes.
  3. This exact page on prod: authed viewer returns 200; anonymous returns the error you pasted, byte for byte.

The error you saw is what an unauthenticated context correctly sees. The tell is the surface: if the page HTML is fetched with an Authorization header (an agent surface or an embedded webview rather than a logged-in browser), that header never propagates to iframe or asset subrequests; only cookies do. That is HTTP, not a wiki defect. For embedded browsers the wiki already provides the browser-session flow (POST /api/auth/browser-session mints a one-time link that sets the session cookie); that is the supported way to give a webview a session. Forwarding credentials any other way (for example a token in the URL) would be a security regression, so I am not building that.

Secondary ("the viewer resolves the page by a bare slug, note owner:null"): confirmed, and slightly worse than you reported. All four viewer routes (3d, symbol, footprint, datasheet) resolve by (type, slug) with no owner, and pick an arbitrary row when a slug exists under multiple owners, which is legal here (per-owner forks). So the viewer can silently render the wrong owner's model, and the 404 reports owner:null. Fixing now:

  • Owner-qualified routes: /viewer/{3d,symbol,footprint,datasheet}///, exact resolution.
  • Page templates embed the owner-qualified URL everywhere.
  • The bare legacy routes stay for old links, but prefer the most-recently-updated page instead of an arbitrary row (same tiebreak /blob already uses).

Will follow up here when it ships.

Colby Knox · 2mo ago

Shipped to prod. What changed, per the verdict above:

  • Owner-qualified viewer routes exist for all four viewers: /viewer/{3d,symbol,footprint,datasheet}///. They resolve exactly, and a denial now names the owner you asked for instead of owner:null.
  • Every page template embeds the owner-qualified URL, so the embedded viewers and "open full view" links are pinned to the right owner.
  • The viewer's model/pdf/download URLs are also pinned to the resolved page (owner-anchored files route, same caching and same read gate as before), so the asset fetch can never resolve to a different owner's page than the viewer did.
  • The bare legacy routes still work for old links; on a cross-owner slug collision they now prefer the most-recently-updated page instead of an arbitrary row.

On the primary report: no server change, because there is nothing to fix server-side. Bearer and cookie sessions both render private viewers (verified again on this exact page). If you were viewing through a surface that fetches with an Authorization header or an embedded webview, that surface needs a browser session: POST /api/auth/browser-session with your token mints a one-time link that sets the session cookie, after which private viewers render. Closing.

Log in to reply.