feat(13-06): content adapter — ContentItem to Film/Song/Podcast, close the streamUrl JWT leak

- archyContentAdapter.ts: hand-written adaptContentItems mapping (D-12),
  fixture-pinned at the adjacency, empty, ordering and paid-lock edges
  named in AIUI-03; classifyByMime covers the m4a/aac/opus/wma extension
  gap ShareModal.vue's mime map leaves today; buildMediaUrl never puts a
  credential in a query string (T-13-32).
- filebrowser-client.ts: streamUrl now returns a query-free same-origin
  raw-file URL, relying on the path=/ cookie login() already sets instead
  of also putting the JWT in the URL (T-13-39 — closes the pre-existing
  leak CONTEXT.md names, rather than merely not repeating it).
- filebrowserStreamUrl.test.ts: regression pin for the fix, including a
  traversal case confirming sanitizePath behavior is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 19:27:09 -04:00
co-authored by Claude Opus 5
parent 6520cffc95
commit f7691fd1bb
4 changed files with 806 additions and 5 deletions
+15 -5
View File
@@ -165,15 +165,25 @@ class FileBrowserClient {
}
/**
* Get a direct streaming URL with auth token in query string.
* Use for video/audio <src> where browser needs to stream (range requests).
* The token is a short-lived JWT so exposure in URL is acceptable.
* Get a direct streaming URL for video/audio `<src>` where the browser
* needs to make Range requests.
*
* Carries NO credential in the query string (T-13-39, fixed 2026-08-03 —
* this was "the known leak to fix rather than propagate", per
* 13-CONTEXT.md). `login()` already sets the filebrowser JWT as a
* `path=/` cookie on this page's own origin, `baseUrl` is that same
* origin, and the browser attaches the cookie to the same-origin media
* subresource request automatically — the same mechanism filebrowser's
* own web UI relies on. Putting the token in the URL too was redundant,
* and it reached browser history, `Referer` headers and any access log on
* the path. The cookie itself is unchanged by this fix: it is still a
* 24-hour JWT, now confined to the cookie jar rather than also appearing
* in the URL.
*/
async streamUrl(path: string): Promise<string> {
await this.ensureAuth()
const token = this.getAuthCookie()
const safePath = sanitizePath(path)
return `${this.baseUrl}/api/raw${safePath}?auth=${token}`
return `${this.baseUrl}/api/raw${safePath}`
}
/**