diff --git a/docs/systems/uploads.md b/docs/systems/uploads.md index 750126cd..27e41868 100644 --- a/docs/systems/uploads.md +++ b/docs/systems/uploads.md @@ -80,6 +80,7 @@ Stats: `getStorageStats()` exposes `staleTusSessions` + `staleTusSize` for the a - JWT verified on every tus request (PRE_CREATE, PRE_PATCH, finalize, HEAD, DELETE). - **Federated uploads use a per-origin JWT.** When the target space is hosted on a remote instance, the client must send that instance's scoped token (resolved via `getTokenForOrigin(origin)` in `crossStoreResolvers.ts`), not the home-instance token — otherwise the remote rejects the request as it can't verify the home signature or resolve the userId. +- **CORS for federated tus uploads.** The server's `@fastify/cors` registration in `index.ts` permits the tus protocol's request headers (`Tus-Resumable`, `Upload-Length`, `Upload-Offset`, `Upload-Metadata`, `Upload-Defer-Length`, `Upload-Concat`, `Upload-Checksum`, `X-HTTP-Method-Override`) and exposes the response headers tus-js-client needs to read across origins (`Location`, `Tus-Resumable`, `Tus-Version`, `Tus-Extension`, `Tus-Max-Size`, `Tus-Checksum-Algorithm`, `Upload-Offset`, `Upload-Length`, `Upload-Metadata`, `Upload-Expires`). Without these, browser preflight blocks cross-origin POST/HEAD/PATCH/DELETE on `/api/files/*`. - PRE_PATCH ownership check: `metadata.userId === req.user.id`. Required to prevent in-flight upload hijack between session creation and finalize. - Size validated against `instance_settings.maxUploadSizeBytes` at PRE_CREATE; tus's own `maxSize` is set as defense-in-depth. - Original filename round-trips through tus metadata (base64-encoded per spec); the on-disk filename uses snowflake + sanitized extension only. diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 53e76fc1..9e2b9cdd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -46,8 +46,37 @@ async function main(): Promise { await app.register(cors, { origin: true, credentials: true, - methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], + // Tus-* and Upload-* headers are required for federated tus uploads + // (cross-origin POST/HEAD/PATCH/DELETE on /api/files/*). Without them the + // browser preflight blocks the request before it ever reaches the server. + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'Tus-Resumable', + 'Upload-Length', + 'Upload-Offset', + 'Upload-Metadata', + 'Upload-Defer-Length', + 'Upload-Concat', + 'Upload-Checksum', + 'X-HTTP-Method-Override', + ], + // Expose tus response headers so tus-js-client can read them across origins + // (Location is the per-upload URL returned on POST; the rest are standard + // tus protocol headers). + exposedHeaders: [ + 'Location', + 'Tus-Resumable', + 'Tus-Version', + 'Tus-Extension', + 'Tus-Max-Size', + 'Tus-Checksum-Algorithm', + 'Upload-Offset', + 'Upload-Length', + 'Upload-Metadata', + 'Upload-Expires', + ], }); await app.register(rateLimit, {