kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(api): avoid checksum query params in presigned S3 uploads

Tin d7987723 390183f9

+70 -2
+3
apps/api/src/storage/s3.ts
··· 145 145 endpoint: config.endpoint, 146 146 region: config.region, 147 147 forcePathStyle: config.forcePathStyle, 148 + // Avoid auto-injecting checksum params for presigned PUT URLs. Some 149 + // S3-compatible providers (e.g. Garage/R2) reject mismatched hoisted CRCs. 150 + requestChecksumCalculation: "WHEN_REQUIRED", 148 151 credentials: { 149 152 accessKeyId: config.accessKeyId, 150 153 secretAccessKey: config.secretAccessKey,
+67 -2
tests/api/storage/s3.test.ts
··· 3 3 assertTaskImageKeyMatchesContext, 4 4 buildObjectKey, 5 5 buildObjectKeyPrefix, 6 + createTaskImageUploadUrl, 6 7 getFileExtension, 7 8 isImageContentType, 8 9 parseBoolean, ··· 13 14 14 15 describe("S3 helpers", () => { 15 16 const originalMaxSize = process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 17 + const originalEndpoint = process.env.S3_ENDPOINT; 18 + const originalBucket = process.env.S3_BUCKET; 19 + const originalAccessKeyId = process.env.S3_ACCESS_KEY_ID; 20 + const originalSecretAccessKey = process.env.S3_SECRET_ACCESS_KEY; 21 + const originalRegion = process.env.S3_REGION; 22 + const originalPathStyle = process.env.S3_FORCE_PATH_STYLE; 16 23 17 24 beforeEach(() => { 18 25 delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; ··· 20 27 21 28 afterEach(() => { 22 29 vi.restoreAllMocks(); 30 + 23 31 if (originalMaxSize === undefined) { 24 32 delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 25 - return; 33 + } else { 34 + process.env.S3_MAX_IMAGE_UPLOAD_BYTES = originalMaxSize; 35 + } 36 + 37 + if (originalEndpoint === undefined) { 38 + delete process.env.S3_ENDPOINT; 39 + } else { 40 + process.env.S3_ENDPOINT = originalEndpoint; 41 + } 42 + 43 + if (originalBucket === undefined) { 44 + delete process.env.S3_BUCKET; 45 + } else { 46 + process.env.S3_BUCKET = originalBucket; 47 + } 48 + 49 + if (originalAccessKeyId === undefined) { 50 + delete process.env.S3_ACCESS_KEY_ID; 51 + } else { 52 + process.env.S3_ACCESS_KEY_ID = originalAccessKeyId; 53 + } 54 + 55 + if (originalSecretAccessKey === undefined) { 56 + delete process.env.S3_SECRET_ACCESS_KEY; 57 + } else { 58 + process.env.S3_SECRET_ACCESS_KEY = originalSecretAccessKey; 59 + } 60 + 61 + if (originalRegion === undefined) { 62 + delete process.env.S3_REGION; 63 + } else { 64 + process.env.S3_REGION = originalRegion; 26 65 } 27 66 28 - process.env.S3_MAX_IMAGE_UPLOAD_BYTES = originalMaxSize; 67 + if (originalPathStyle === undefined) { 68 + delete process.env.S3_FORCE_PATH_STYLE; 69 + } else { 70 + process.env.S3_FORCE_PATH_STYLE = originalPathStyle; 71 + } 29 72 }); 30 73 31 74 it("recognizes allowed image content types case-insensitively", () => { ··· 97 140 validateTaskAssetUploadInput("image/png", 2 * 1024 * 1024), 98 141 ).toThrow("Upload exceeds the maximum upload size of 1MB."); 99 142 expect(() => validateTaskAssetUploadInput("image/png", 512)).not.toThrow(); 143 + }); 144 + 145 + it("creates presigned upload URLs without hoisted checksum query params", async () => { 146 + process.env.S3_ENDPOINT = "https://storage.example.test"; 147 + process.env.S3_BUCKET = "kaneo"; 148 + process.env.S3_ACCESS_KEY_ID = "test-access-key"; 149 + process.env.S3_SECRET_ACCESS_KEY = "test-secret-key"; 150 + process.env.S3_REGION = "us-east-1"; 151 + process.env.S3_FORCE_PATH_STYLE = "true"; 152 + 153 + const upload = await createTaskImageUploadUrl({ 154 + workspaceId: "workspace-1", 155 + projectId: "project-1", 156 + taskId: "task-1", 157 + surface: "description", 158 + filename: "report.png", 159 + contentType: "image/png", 160 + }); 161 + 162 + const searchParams = new URL(upload.uploadUrl).searchParams; 163 + expect(searchParams.has("x-amz-checksum-crc32")).toBe(false); 164 + expect(searchParams.has("x-amz-sdk-checksum-algorithm")).toBe(false); 100 165 }); 101 166 });