# S3 Compatibility Completion Design Date: 2026-07-07 ## Goal Close the remaining known S3 compatibility gaps in TeleUploader while preserving strict SigV4 security. The target is real-client compatibility for common S3 flows against the production endpoint, especially AWS SDK v3, without accepting malformed or unverifiable signatures. ## Scope Implement and verify: 1. Presigned `GET` URLs return object content instead of `403 SignatureDoesNotMatch`. 2. `GetObject` supports `Range: bytes=...` for single-part objects. 3. Multipart `GetObject` returns the complete logical object, not just the first Telegram part. 4. Multipart `GetObject` supports byte ranges across part boundaries. 5. AWS SDK multipart upload is investigated and fixed under strict SigV4 if the mismatch is in TeleUploader canonicalization/routing. 6. The remaining CSS lint warning in `src/home.html` is removed. Out of scope: - Weakening SigV4 verification to accept invalid signatures. - Full AWS S3 feature parity for ACLs, bucket policy, object tags, virtual-hosted-style routing, or checksum-specific APIs. - Replacing Telegram storage with a different backend. ## Design Choice Use a **strict compatibility, shared object streaming layer**. Rather than adding one-off fixes inside `handleGetObject`, introduce small helpers with clear responsibilities: - request authentication remains strict and spec-based; - object lookup returns object metadata and an ordered list of physical Telegram parts; - range parsing converts a `Range` header into byte offsets; - response streaming emits either a full body (`200`) or a partial body (`206`) with correct S3/HTTP headers. This keeps S3 auth, object resolution, byte planning, and streaming separately testable. ## Presigned URL Verification Current behavior builds a URL with `http://localhost/...`, which can produce a host mismatch for presigned signatures generated against the public production host. New behavior: - pass the original request URL and headers into presigned verification; - compute canonical URI from the actual URL path; - compute canonical query string by excluding `X-Amz-Signature`, sorting encoded key/value pairs, and preserving AWS SigV4 encoding rules; - validate credential scope parts: access key, date, region, service `s3`, termination `aws4_request`; - use the signed `host` header value when present, otherwise the request URL host; - parse `X-Amz-Date` as UTC and enforce `X-Amz-Expires`. Production E2E must change from accepting `[200, 302, 403]` to requiring `200` and body content for presigned `GET` when `PROXY_S3_GET` is enabled. ## Object Body Model Represent the physical body as ordered parts: ```ts interface ObjectPartSource { telegramFileId: string; sizeBytes: number; partNumber: number; } ``` Single-part objects produce one source from the `files` row. Multipart objects produce sources from `multipart_parts` ordered by `part_number`. The logical object size is: - `file.sizeBytes` for single-part; - sum of part sizes for multipart, cross-checked against `file.sizeBytes` when available. ## Range Handling Support single-range requests only: - `bytes=0-4` - `bytes=5-` - `bytes=-500` Invalid or unsatisfiable ranges return `416` with `Content-Range: bytes */`. Multiple ranges such as `bytes=0-1,3-4` are not supported and return `416`. For a valid range: - return status `206`; - set `Content-Range: bytes -/`; - set `Content-Length` to `end - start + 1`; - keep `Accept-Ranges: bytes`. ## Telegram Streaming For each needed part: 1. Call `getFileInfo(telegramFileId)`. 2. Build the Telegram CDN URL. 3. Fetch only the needed range when possible using a `Range` request to Telegram. 4. If Telegram returns `206`, stream that response body. 5. If Telegram returns `200` for a partial request, slice the response in TeleUploader for correctness. 6. If Telegram fetch fails, return S3 XML `InternalError` with HTTP `502`. Multipart full-object responses concatenate part streams in order with a `ReadableStream`. Multipart range responses calculate part overlap using cumulative byte offsets and only stream overlapped segments. ## AWS SDK Multipart Upload Add a real AWS SDK multipart test path using: - `CreateMultipartUploadCommand` - `UploadPartCommand` - `CompleteMultipartUploadCommand` - `GetObjectCommand` - cleanup via `AbortMultipartUploadCommand` on failure Keep SigV4 strict. If the SDK multipart signature still fails: 1. capture server-side canonical request components in a temporary diagnostic path or test-only log; 2. compare against the SDK request inputs; 3. fix TeleUploader canonicalization if wrong; 4. if the mismatch is caused by upstream proxy/header mutation outside the app, leave strict behavior intact and document the blocker with evidence. No compatibility fallback is allowed in this pass. ## CSS Lint Warning Remove `noDescendingSpecificity` by reordering the `.modal input` rule before the more specific `.topbar .search input` rule, or by making selector ordering explicitly non-conflicting. No visual redesign is required. ## Tests and Verification Unit tests: - presigned URL canonicalization and expiration edge cases; - range parser valid/invalid forms; - multipart range planning across part boundaries. Production E2E: - presigned `GET` returns `200` and expected body; - single-part range returns `206` and expected substring; - multipart upload via manual signer returns complete body; - multipart range returns expected cross-part substring; - invalid range returns `416`. AWS SDK E2E: - existing SDK suite remains passing; - `GetObjectCommand({ Range: 'bytes=...' })` verifies partial body; - real SDK multipart is enabled if strict SigV4 succeeds after canonicalization fixes. Deploy verification: - `bun run lint` has no errors and no warnings; - targeted unit tests pass; - production E2E passes; - AWS SDK E2E passes; - `./deploy.sh` completes and container is healthy; - post-deploy production E2E and SDK E2E pass. ## Error Handling - Authentication failures return S3 XML errors with `403`. - Missing bucket/key behavior is unchanged. - Telegram fetch failures return S3 XML `InternalError` with `502`. - Invalid ranges return `InvalidRange` XML with `416`. - Multipart objects with no parts return `InternalError`. ## Rollout The existing `PROXY_S3_GET` flag remains: - default `true`: S3-compatible streaming responses; - `false`: legacy redirect path for single-part objects only. Multipart complete-body support requires proxy mode. If proxy mode is disabled and a multipart object is requested, the response may fall back to the first-part redirect only as legacy behavior; production compatibility should keep proxy mode enabled.