From e5543144ceca2dae9d8cf5ecb8ea8a1aba25c3a6 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Fri, 18 Sep 2026 22:11:07 +0200 Subject: [PATCH] fix(releases): redirect without downloading the artifact The latest-artifact redirects fetched the whole object on every cache miss to compare it with its .sha256 sidecar. Several concurrent misses on staging pulled multiple recovery images at once, the instance was marked unhealthy and the requests failed with 504. The sidecar is written by the release script that uploads the file, and the sync script verifies hash and signature when it registers a release, so a second check at request time added nothing. Check that the object exists and redirect. --- src/releases.ts | 57 ++++++--------------------------------- test/releases.test.ts | 63 +++++++------------------------------------ 2 files changed, 18 insertions(+), 102 deletions(-) diff --git a/src/releases.ts b/src/releases.ts index 7115c06..d34ba83 100644 --- a/src/releases.ts +++ b/src/releases.ts @@ -16,7 +16,6 @@ import { objectKeyFromArtifactUrl, streamToString, toSemverRange, - verifyHash, } from "./helpers"; import { z, ZodError } from "zod"; import { @@ -686,39 +685,19 @@ export const RetrieveLatestSystemRecovery = cachedRedirect( recovery.file, ); - const [firmwareFile, hashFile] = await Promise.all([ - // TODO: store file hash using custom header to avoid extra request - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: artifactPath, - }), - ), - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: `${artifactPath}.sha256`, - }), - ), - ]); - - if (!firmwareFile.Body || !hashFile.Body) { - throw new NotFoundError( - `Recovery image or hash file not found for version ${latestVersion}`, - ); + if (!(await s3ObjectExists(artifactPath))) { + throw new NotFoundError(`Recovery image not found for version ${latestVersion}`); } - await verifyHash(firmwareFile, hashFile, "recovery image hash does not match"); - - console.log("recovery image hash matches", latestVersion); - return `${baseUrl}/${artifactPath}`; }, ); /** - * 302 to the newest over-the-air artifact of one kind for the requested SKU, - * after verifying the object against its .sha256 sibling. The product table + * 302 to the newest over-the-air artifact of one kind for the requested SKU. + * Integrity is checked at publish time (the .sha256 sidecar is written by the + * release script, the sync script verifies hash and signature); here the + * object only has to exist. The product table * says which prefix holds it, so the same URL serves every product. Used by * build tooling (rv1106-system pulls the app binary into the system image) * and by flashing scripts. @@ -763,30 +742,10 @@ function latestArtifactRedirect(kind: OtaKind) { artifact.file, ); - const [artifactFile, hashFile] = await Promise.all([ - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: artifactPath, - }), - ), - s3Client.send( - new GetObjectCommand({ - Bucket: bucketName, - Key: `${artifactPath}.sha256`, - }), - ), - ]); - - if (!artifactFile.Body || !hashFile.Body) { - throw new NotFoundError( - `${prefix} artifact or hash file not found for version ${latestVersion}`, - ); + if (!(await s3ObjectExists(artifactPath))) { + throw new NotFoundError(`${prefix} artifact not found for version ${latestVersion}`); } - await verifyHash(artifactFile, hashFile, `${prefix} hash does not match`); - - console.log(`${prefix} hash matches`, latestVersion); return `${baseUrl}/${artifactPath}`; }, ); diff --git a/test/releases.test.ts b/test/releases.test.ts index 2294de9..cc32725 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -134,6 +134,9 @@ function mockS3LegacyVersionWithContent( Contents: [], }); + // Legacy artifact exists (HeadObjectCommand for the existence check) + s3Mock.on(HeadObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({}); + // Mock legacy file path with content s3Mock.on(GetObjectCommand, { Key: `${prefix}/${version}/${fileName}` }).resolves({ Body: createAsyncIterable(content) as any, @@ -929,25 +932,6 @@ describe("RetrieveLatestApp S3 redirect handler", () => { ); }); - it("should throw InternalServerError when hash does not match", async () => { - const req = createMockRequest({}); - const res = createMockResponse(); - - s3Mock.on(ListObjectsV2Command, { Prefix: "app/" }).resolves({ - CommonPrefixes: [{ Prefix: "app/1.0.0/" }], - }); - - mockS3LegacyVersionWithContent( - "app", - "1.0.0", - "jetkvm_app", - "actual-content", - "wrong-hash-value", - ); - - await expect(RetrieveLatestApp(req, res)).rejects.toThrow(InternalServerError); - }); - it("should throw NotFoundError when app file is missing", async () => { const req = createMockRequest({}); const res = createMockResponse(); @@ -961,12 +945,9 @@ describe("RetrieveLatestApp S3 redirect handler", () => { Contents: [], }); - s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app" }).resolves({ - Body: undefined, - }); - s3Mock.on(GetObjectCommand, { Key: "app/1.0.0/jetkvm_app.sha256" }).resolves({ - Body: createAsyncIterable("some-hash") as any, - }); + s3Mock + .on(HeadObjectCommand, { Key: "app/1.0.0/jetkvm_app" }) + .rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } }); await expect(RetrieveLatestApp(req, res)).rejects.toThrow(NotFoundError); }); @@ -1304,28 +1285,7 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => { ); }); - it("should throw InternalServerError when hash does not match", async () => { - const req = createMockRequest({}); - const res = createMockResponse(); - - s3Mock.on(ListObjectsV2Command, { Prefix: "system/" }).resolves({ - CommonPrefixes: [{ Prefix: "system/1.0.0/" }], - }); - - mockS3LegacyVersionWithContent( - "system", - "1.0.0", - "update.img", - "actual-content", - "mismatched-hash", - ); - - await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow( - InternalServerError, - ); - }); - - it("should throw NotFoundError when recovery image or hash file is missing", async () => { + it("should throw NotFoundError when recovery image is missing", async () => { const req = createMockRequest({}); const res = createMockResponse(); @@ -1338,12 +1298,9 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => { Contents: [], }); - s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img" }).resolves({ - Body: undefined, - }); - s3Mock.on(GetObjectCommand, { Key: "system/1.0.0/update.img.sha256" }).resolves({ - Body: undefined, - }); + s3Mock + .on(HeadObjectCommand, { Key: "system/1.0.0/update.img" }) + .rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } }); await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow(NotFoundError); });