Closed feature request

Private-page media in widget frames: mint a short-lived signed read URL (third-party cookie blocking breaks ::adom-video on private pages)

John Lauer · 7d ago ·closed by Colby Knox

Symptom. ::adom-video (adom/widgets) does not play a video stored on a PRIVATE page in real Chrome (third-party cookies blocked) or Safari. In pup's browser it plays. Repro on a private scratch page I own: john/adom-video-test, embeds 2, 3 and 6. The private-manifest form ::adom-video{src="/OWNER/SLUG/render/x.video.json"} behaves the same: the manifest inlines fine (chapters and captions show), but duration stays null.

Cause. Widget frames are served with a CSP sandbox and no allow-same-origin, so the frame is opaque-origin. Its media request to wiki.adom.inc is cross-site from the browser's point of view. The session cookie is SameSite=None; Secure, which is right, but any browser that blocks third-party cookies drops it (Safari always, Chrome with that setting). The request arrives anonymous, and the private file answers 404. There is no URL-borne credential the widget could use instead: ?token= / ?access_token= are ignored, and presigned URLs exist only for uploads.

Ask: a short-lived signed read URL, minted by the server when it renders the widget frame for a viewer who has read access.

  • Rewrite every same-page file reference the frame will load to /api/pages/OWNER/SLUG/files/PATH?sig=…&exp=…. That covers the tag's video, poster, captions and chapters attributes, and the same fields inside an inlined src manifest. Use an HMAC over owner, slug, path and expiry (viewer id optional). Scope each signature to that one path. A TTL of a few hours covers a long video; reloading the frame mints fresh ones.
  • Honor Range on the signed URL (206, Content-Range, Accept-Ranges), as the files API already does with a cookie.
  • Viewers without access get the frame's existing 404 and no URL minted, so nothing leaks. Reject a wrong path or an expired exp.
  • No widget change is needed if the URLs are rewritten in place: adom-video loads whatever URL it is given, and it leaves /api/pages/…/files/… URLs untouched.

Related finding: /render/ caps file size. /OWNER/SLUG/render/PATH answers 413 {"error":"File too large to render"} for files above roughly 4 to 5 MB, with or without Range and with any auth: a 5.68 MB MP4 is refused, a 3.85 MB one is served. The files API serves the same file with 206. adom-video already streams through the files API, but a README <video src=".../render/..."> of a normal-size demo video gets nothing. Raise the cap for media, or make /render/ stream ranged like the files API (see also #225).

Acceptance (please add as a regression test)

  • A 14 MB private MP4 on a private page, both tag forms, plays and seeks for a logged-in viewer with access in Chrome with third-party cookies blocked, in Edge and in Safari.
  • Poster, captions and chapters show.
  • A logged-out viewer gets nothing.
  • A signed URL for a different path, or past exp, is refused.

Stopgap shipped in the widget (adom/widgets, today). When the stream is refused in the frame, adom-video shows "This video can't play inside the page" with a Play it in a new tab button. That opens the file as a first-party page, where it plays with seeking.

1 Reply

Colby Knox · 7d ago

Covered without signed URLs, now on prod (32c7ba1):

  • #229 inline widgets: an ::adom-video whose registry entry says "render": "inline" runs in the page itself, so its requests carry the reader's normal session in every browser, including Chrome with third-party cookies blocked and Safari. Private videos, posters, captions, chapters and manifests load like any page asset. adom-video is page-safe as is; it just needs "render": "inline" in adom/widgets/widgets.json.
  • The /render/ size cap you found: the render route now streams video, audio, PDFs, models, images and archives from the blob cache with Range (206), like the files API, instead of reading the file whole and refusing anything over 4 MB with a 413. A README <video src=".../render/demo.mp4"> of a normal-size demo plays and seeks. A range that ends past the file is now clamped to its last byte instead of 416 (route and files API both).

Closing, since signed media URLs aren't needed for registry widgets. Reopen if a case remains that inline doesn't cover.

Log in to reply.