From ce873a6ef95c6d686a6e4134672cef6edabc7ab4 Mon Sep 17 00:00:00 2001 From: Yangshun Date: Fri, 23 Oct 2026 07:34:31 +0800 Subject: [PATCH 1/2] perf(platform): index approved OAuth assets by content tuple --- platform/packages/api/src/routes/deploy.ts | 16 ++++++---------- .../api/test/integration/oauth.test.ts | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/platform/packages/api/src/routes/deploy.ts b/platform/packages/api/src/routes/deploy.ts index dfd90c155..f0138a139 100644 --- a/platform/packages/api/src/routes/deploy.ts +++ b/platform/packages/api/src/routes/deploy.ts @@ -163,18 +163,14 @@ app.post('/:id/deploy/upload-urls', async (context) => { context.get('companyIdentity'), true, ); - const approved = Object.values(approval.data.artifact!.assetManifest); + const approved = new Set( + Object.values(approval.data.artifact!.assetManifest).map( + (entry) => `${entry.blake3}:${entry.md5}:${entry.size}`, + ), + ); if ( approval.data.projectId !== projectId || - assets.some( - (asset) => - !approved.some( - (entry) => - entry.blake3 === asset.blake3 && - entry.md5 === asset.md5 && - entry.size === asset.size, - ), - ) + assets.some((asset) => !approved.has(`${asset.blake3}:${asset.md5}:${asset.size}`)) ) { throw new OAuthError( 'access_denied', diff --git a/platform/packages/api/test/integration/oauth.test.ts b/platform/packages/api/test/integration/oauth.test.ts index 42953f605..d520b46f8 100644 --- a/platform/packages/api/test/integration/oauth.test.ts +++ b/platform/packages/api/test/integration/oauth.test.ts @@ -208,9 +208,18 @@ describe('registered OAuth applications', () => { size: 3, contentType: 'text/javascript', }; + const otherAsset = { + ...asset, + blake3: computeAssetHash(new TextEncoder().encode('page').buffer, '/page.js'), + md5: 'b'.repeat(32), + size: 4, + }; const prepared = createDeployArtifact({ ...artifact, - assetManifest: { '/app.js': { ...asset, hash: asset.blake3 } }, + assetManifest: { + '/app.js': { ...asset, hash: asset.blake3 }, + '/page.js': { ...otherAsset, hash: otherAsset.blake3 }, + }, }); const issued = await request('/oauth/preview-deployment-requests', { method: 'POST', @@ -222,15 +231,18 @@ describe('registered OAuth applications', () => { }), }); const receipt = (await issued.json()) as { access_token: string; approval_id: string }; - const upload = (entry: typeof asset) => + const upload = (...assets: Array) => request('/projects/' + project.project_id + '/deploy/upload-urls', { method: 'POST', headers: { ...headers, Authorization: 'Bearer ' + receipt.access_token }, - body: JSON.stringify({ assets: [entry] }), + body: JSON.stringify({ assets }), }); expect((await upload(asset)).status).toBe(200); + expect((await upload(otherAsset, asset, asset)).status).toBe(200); expect((await upload({ ...asset, size: 4 })).status).toBe(403); + expect((await upload({ ...asset, md5: 'b'.repeat(32) })).status).toBe(403); expect((await upload({ ...asset, blake3: 'c'.repeat(32) })).status).toBe(403); + expect((await upload(otherAsset, { ...asset, size: 4 })).status).toBe(403); const [record] = await env.db .select() .from(oauthRequests) From 22378405ab113668b5ca91e9b5e2676fc022bffa Mon Sep 17 00:00:00 2001 From: Yangshun Date: Fri, 23 Oct 2026 08:39:26 +0800 Subject: [PATCH 2/2] fix(platform): preserve OAuth asset tuple types in lookup keys --- platform/packages/api/src/routes/deploy.ts | 13 +++++++++---- .../packages/api/test/integration/oauth.test.ts | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/platform/packages/api/src/routes/deploy.ts b/platform/packages/api/src/routes/deploy.ts index f0138a139..dbdfd1423 100644 --- a/platform/packages/api/src/routes/deploy.ts +++ b/platform/packages/api/src/routes/deploy.ts @@ -163,14 +163,19 @@ app.post('/:id/deploy/upload-urls', async (context) => { context.get('companyIdentity'), true, ); + // JSON preserves tuple boundaries and field types, including malformed + // approval metadata that must not match a valid upload through coercion. + function assetApprovalKey( + asset: Pick, + ): string { + return JSON.stringify([asset.blake3, asset.md5, asset.size]); + } const approved = new Set( - Object.values(approval.data.artifact!.assetManifest).map( - (entry) => `${entry.blake3}:${entry.md5}:${entry.size}`, - ), + Object.values(approval.data.artifact!.assetManifest).map(assetApprovalKey), ); if ( approval.data.projectId !== projectId || - assets.some((asset) => !approved.has(`${asset.blake3}:${asset.md5}:${asset.size}`)) + assets.some((asset) => !approved.has(assetApprovalKey(asset))) ) { throw new OAuthError( 'access_denied', diff --git a/platform/packages/api/test/integration/oauth.test.ts b/platform/packages/api/test/integration/oauth.test.ts index d520b46f8..aeaa45bef 100644 --- a/platform/packages/api/test/integration/oauth.test.ts +++ b/platform/packages/api/test/integration/oauth.test.ts @@ -219,6 +219,12 @@ describe('registered OAuth applications', () => { assetManifest: { '/app.js': { ...asset, hash: asset.blake3 }, '/page.js': { ...otherAsset, hash: otherAsset.blake3 }, + '/wrong-type.js': { + ...asset, + blake3: 'd'.repeat(32), + hash: 'd'.repeat(32), + size: '3' as unknown as number, + }, }, }); const issued = await request('/oauth/preview-deployment-requests', { @@ -243,6 +249,7 @@ describe('registered OAuth applications', () => { expect((await upload({ ...asset, md5: 'b'.repeat(32) })).status).toBe(403); expect((await upload({ ...asset, blake3: 'c'.repeat(32) })).status).toBe(403); expect((await upload(otherAsset, { ...asset, size: 4 })).status).toBe(403); + expect((await upload({ ...asset, blake3: 'd'.repeat(32) })).status).toBe(403); const [record] = await env.db .select() .from(oauthRequests)