Skip to content

v4.0.0: secure defaults, storage adapters, signed links, resumable uploads - #921

Draft
dr-dimitru wants to merge 45 commits into
devfrom
release/4.0.0
Draft

dr-dimitru wants to merge 45 commits into
devfrom
release/4.0.0

Conversation

@dr-dimitru

@dr-dimitru dr-dimitru commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

  • ⚠️ Remove FilesCursor#hasNext, FilesCursor#countAsync, the server findOne shim, and the plain-object session lookup.
  • ⚠️ Run upload pipes in the order they were added.
  • ⚠️ Accept only a String _id in _Remove; client cursor removal sends one call per file.
  • ⚠️ Default allowClientCode to false and nosniff to true.
  • ⚠️ Send Cache-Control: private by default for protected collections.
  • ⚠️ Serve downloads as attachment unless the type is safe to show inline.
  • ⚠️ Name files on the server only and keep only name, type, size, and meta from the client file.
  • ⚠️ Store the file type detected from content; new trustClientMimeType option keeps the 3.x behavior.
  • ⚠️ Store, serve, and remove files through a storage adapter (storage option, built-in FSStorage and GridFSStorage); serve() is async.
  • ⚠️ Record written chunks so uploads resume after a server restart; Start rejects more than 100000 chunks.
  • ✨ Add signed download links: downloadTokenSecret, createDownloadToken(), link(..., { token }).
  • 👨‍💻 Deprecate protected: true in favor of a function that checks ownership.
  • 🧪 Add a browser test suite (playwright) for the upload state machine, with a CI job.
  • 📔 Add docs/migration-to-v4.md and the 4.0.0 changelog.

Verification

  • GitHub Actions: lint, typecheck, package and browser tests on 3.2.2 and 3.5.2 pass.
  • npm run lint and npm run typecheck: pass.
  • npm run test:mocha on Meteor 3.5.2 and 3.2.2: 403 passing.
  • npm run test:browser on Meteor 3.5.2 and 3.2.2: 403 server and 19 client tests passing.

Migration

Breaking changes and an upgrade checklist are in docs/migration-to-v4.md.

Issues

Closes #908, closes #906, closes #874, closes #861, closes #862, closes #799, closes #753, closes #505.

Thanks to @codeonprod, @jankapunkt, @ThaumRystra, @ToyboxZach, @xet7, @dhana-exe, and @sylido for reporting them.

The puppeteer driver in meteortesting:browser-tests resolves its module through Meteor's Npm.require, which never looks at NODE_PATH or this repo's node_modules under test-packages. The playwright driver imports from $PWD/node_modules, so it works without a test app.
BREAKING CHANGE: FilesCursor#hasNext() and FilesCursor#countAsync() are gone. Use hasNextAsync() and countDocuments().
…ookup

BREAKING CHANGE: FilesCollectionCore no longer has findOne(). The client class has it, the server class throws Meteor.Error(404) as before. Meteor.server.sessions must be a Map (Meteor 3), a plain object now throws.
BREAKING CHANGE: the first pipe() call now runs first. Reverse the order of chained pipe() calls written for v3.
BREAKING CHANGE: client remove() and removeAsync() take a String _id. Use find(selector).removeAsync() to remove several files.
BREAKING CHANGE: clients can no longer call remove() unless allowClientCode: true is set on the server and the client. Set onBeforeRemove when you enable it.
BREAKING CHANGE: file responses carry X-Content-Type-Options: nosniff. Set nosniff: false to turn it off.
BREAKING CHANGE: only image/* (not SVG), video/*, audio/*, application/pdf, and text/plain are served inline. Set Content-Disposition in responseHeaders to change it.
BREAKING CHANGE: the client namingFunction option and the FSName field are ignored. Set namingFunction on the server. It receives { file, fileId, userId } in upload Start, writeAsync(), and loadAsync().
… file

BREAKING CHANGE: other keys of the client file object are dropped. Send custom data in meta.
BREAKING CHANGE: type, mime, versions.original.type, and the is* flags come from the file content. Set trustClientMimeType: true to store the uploader's type as in 3.x.
…types

A UTF-8 file labeled text/html, text/xml, text/css, or text/javascript was stored with that type, so a browser could run it. Only text/plain, text/csv, text/markdown, text/tab-separated-values, text/calendar, text/vtt, and application/json are kept, anything else becomes text/plain. Client types ending in +xml no longer refine a zip or CFB container.
…apter

BREAKING CHANGE: serve() is async and returns a Promise; await it when code runs after it. unlinkAsync() and removeAsync() remove files through the storage adapter (FSStorage by default, same files as before). Custom adapters receive { source } as the 4th put() argument and must keep the caller's file when source is 'addFile'.
…tart

The server guessed written chunks from the file size after a restart, so a hole before the last written chunk passed as complete.

BREAKING CHANGE: uploads started before 4.0 get 410 on their next chunk and must restart. Start rejects more than 100000 chunks with 400; the client raises chunkSize to stay below.
.versions also moves base64 from 1.0.14 to 1.0.16, the version Meteor 3.5.2 resolves in a throwaway app.
The docs share one secret across collections, and clients choose upload
file ids, so a token for an `_id` in one collection also opened the same
`_id` in another collection. The token string format stays the same.
The default `public, max-age=31536000` let a CDN keep a token response for
a year, so anyone with the URL could fetch the file after the token
expired. Token downloads now get `private, max-age=<seconds until exp>`
unless `responseHeaders` sets `Cache-Control`.
When a resumed upload finished on another process, the observer on this
process called `end()`. Its stream lacked the other process's chunks, so
it waited 25 seconds, aborted, and unlinked the finished file. The
observer now only closes the file handle.
…lections

A shared cache in front of the app could keep a protected file and serve it to other users.

BREAKING CHANGE: protected collections now send `Cache-Control: private, max-age=31536000` by default. Set `cacheControl` to restore `public, max-age=31536000, s-maxage=31536000`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant