Repository navigation
Conversation
The fileAccess middleware decided inline whether a user may access a file: same tenant for tenant-scoped files, then ownership, then access through the agents the file is attached to. The decision now lives in canAccessFile(user, file), which the middleware calls and exports, so a route that already holds a file record can apply the same rules without going through req.params.file_id. Behaviour is unchanged; the cross-tenant denial is still logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With fileStrategy s3, browsers load images, previews and downloads straight from presigned URLs. Those links expire after S3_URL_EXPIRY_SECONDS and are stored in messages and file records, so images break once they expire, and the bucket has to answer requests from the internet, so it cannot be kept private to the deployment's network (for instance with a bucket policy on aws:SourceVpce). STORAGE_PROXY_FILES=true makes storage strategies link files to LibreChat instead: /api/stored-files/<source>/<key>, which never expires. The route reads the object through the strategy's StoredFileSource adapter and streams it to viewers who may read it. This commit adds the storage-agnostic route and links, and the S3 adapter. With the proxy on, getS3URL returns stored file links, extractKeyFromS3Url reads them back to keys, presigned links are replaced where the file list and avatars already renew links, and getS3DownloadURL returns no direct link, so the download and shared-link routes stream through their own checks. With it off, stored links go back to presigned ones the same way. Off by default. The route signs the viewer in from the session cookie, as /images does, since an image tag sends no bearer token; authenticateViewer carries the image route's account checks (retired tokens, required 2FA enrollment). Within the viewer's tenant it serves the owner named in the key, avatars to any signed-in user (as CloudFront avatar cookies do), and anyone the stored file's own rules admit through canAccessFile, such as a file on an agent shared with the viewer; everyone else gets a 404. The file lookup is scoped to the key's owner and source. Uploads are now served from the app's own origin, so only raster images are sent inline; every response carries a sandboxing CSP and nosniff. Related to LibreChat-AI#16912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With fileStrategy azure_blob, LibreChat stores each blob's plain URL, so browsers can only display images from a container with public blob access. A private container (AZURE_STORAGE_PUBLIC_ACCESS=false, or an account that disallows public access) breaks every image and preview. This adds the Azure adapter for the stored-file route. With STORAGE_PROXY_FILES=true, uploads, saved buffers and avatars in the configured container store /api/stored-files/azure_blob/<blob path> instead of the blob URL, getAzureFileStream and deleteFileFromAzure read those links back to blob paths, and the route streams blobs to viewers who may read them under the same rules as S3. Azure keeps avatars beside other images, so the adapter marks avatar-<ts> and agent-<id>-avatar-<ts> files as avatars. Blobs in any other container keep their URLs, since a stored link names no container. Off by default. Related to LibreChat-AI#16912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records written before STORAGE_PROXY_FILES was turned on hold presigned S3 URLs or Azure blob URLs, in file records and in every copy made of them. The file list and S3 avatars convert as they load, but messages, tool calls, agent avatars and Azure user avatars never do, so their images keep expiring (S3) or keep pointing at a container that may no longer be public (Azure). npm run migrate:stored-file-links rewrites them once, in batches, under system context like the other cross-tenant migrations: - files: filepath, and storageKey for S3 records that lack it, which the stored-file route uses to find a record; - messages: files, attachments, steer-part files and image_file parts; - tool calls: attachments; - agents and users: avatars. Each storage source converts its own links through a Relinker (S3 and Azure), so the walk is storage-agnostic. It is idempotent, has a :dry-run, and :revert writes the storage service's links back for turning the proxy off. It refuses a direction the current STORAGE_PROXY_FILES setting would not serve. The read path is untouched. Related to LibreChat-AI#16912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
Vidminas
marked this pull request as ready for review
October 9, 2026 11:00
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
👍
Summary
Records written before
STORAGE_PROXY_FILESwas turned on hold presigned S3 URLs or Azure blob URLs, in file records and in every copy made of them. The file list and S3 avatars convert as they load. Messages, tool calls, agent avatars and Azure user avatars never do, so their images keep expiring (S3) or keep pointing at a container that may no longer be public (Azure).npm run migrate:stored-file-linksrewrites them once:filepath, plusstorageKeyfor S3 records that lack it, which the stored-file route uses to find a record;files,attachments, steer-part files andimage_fileparts;How it runs:
:dry-runreports counts without writing.:revertwrites storage links back, for turning the proxy off.The read path is untouched.
Covers the conversation-history part of #16628: stored Azure blob URLs in messages and attachments become stored-file links.
Related to #16912. Depends on #16915
How it works
Recognising a link without a source. A user's avatar is a bare string with no
sourcefield, so each relinker identifies its own links from the URL. S3 links are always presigned. Azure links start with the configured container's URL.Type of change
Testing
STORAGE_PROXY_FILES=trueand runnpm run migrate:stored-file-links:dry-run. It reports counts and writes nothing.npm run migrate:stored-file-links. Old conversations' images, agent avatars and user avatars load through/api/stored-files/.... A second run reports 0 changes.STORAGE_PROXY_FILES=false npm run migrate:stored-file-links:revert, then restart with the setting off. The links are storage URLs again.Tested environments/configuration:
STORAGE_PROXY_FILESon for migrating, off for--revertAutomated tests:
npm run static-checks -- --against upstream/dev --fullpasses.New tests:
proxy/__tests__/relink.test.ts:s3/__tests__/source.test.ts:s3Relinker, in both directions.config/__tests__/migrate-stored-file-links.spec.js(in-memory MongoDB):Screenshots / recordings
No user-facing change.
Risk / compatibility
:dry-runfirst.:revertrestores storage links.files,attachmentsandcontent.--batch-sizecontrols the write batches.Checklist