From 60fcd7f4260df720dda1aaf143cae33015f575f1 Mon Sep 17 00:00:00 2001 From: Daryl Cecile Date: Sat, 11 Nov 2023 00:28:39 +0000 Subject: [PATCH 1/3] Updated client.ts Updated response error handling to attempt to extract error message from body. --- packages/blob/src/client.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/blob/src/client.ts b/packages/blob/src/client.ts index f2e717118..6a71b68d4 100644 --- a/packages/blob/src/client.ts +++ b/packages/blob/src/client.ts @@ -4,7 +4,7 @@ import type { IncomingMessage } from 'node:http'; // When bundled via a bundler supporting the `browser` field, then // the `undici` module will be replaced with https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API // for browser contexts. See ./undici-browser.js and ./package.json -import { fetch } from 'undici'; +import { fetch, type Response } from 'undici'; import type { BlobCommandOptions } from './helpers'; import { BlobError, getTokenFromOptionsOrEnv } from './helpers'; import { createPutMethod } from './put'; @@ -329,9 +329,12 @@ async function retrieveClientToken(options: { method: 'POST', body: JSON.stringify(event), }); + if (!res.ok) { - throw new BlobError('Failed to retrieve the client token'); + const errorMessage = await tryGetErrorMessageFromResponse(res); + throw new BlobError(errorMessage ?? 'Failed to retrieve the client token'); } + try { const { clientToken } = (await res.json()) as { clientToken: string }; return clientToken; @@ -340,6 +343,15 @@ async function retrieveClientToken(options: { } } +async function tryGetErrorMessageFromResponse(res: Response) { + const jsonBody = await res.json().catch(() => null); + if (!jsonBody) return; + if (typeof jsonBody !== 'object') return; + if ('error' in jsonBody && typeof jsonBody.error === 'string') { + return jsonBody.error; + } +} + function toAbsoluteUrl(url: string): string { return new URL(url, window.location.href).href; } From 252328a47457d5aca2ca7cc529c17c9f08999b1d Mon Sep 17 00:00:00 2001 From: Daryl Cecile Date: Sat, 11 Nov 2023 00:49:14 +0000 Subject: [PATCH 2/3] fix merge conflict typo --- packages/blob/src/client.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/blob/src/client.ts b/packages/blob/src/client.ts index 6a71b68d4..66114448d 100644 --- a/packages/blob/src/client.ts +++ b/packages/blob/src/client.ts @@ -22,7 +22,7 @@ export const put = createPutMethod({ extraChecks(options: ClientPutCommandOptions) { if (typeof window === 'undefined') { throw new BlobError( - 'client/`put` must be called from a client environment', + 'client/`put` must be called from a client environment' ); } @@ -37,7 +37,7 @@ export const put = createPutMethod({ options.cacheControlMaxAge !== undefined ) { throw new BlobError( - 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.', + 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.' ); } }, @@ -79,7 +79,7 @@ export const upload = createPutMethod({ extraChecks(options: UploadOptions) { if (typeof window === 'undefined') { throw new BlobError( - 'client/`upload` must be called from a client environment', + 'client/`upload` must be called from a client environment' ); } @@ -95,7 +95,7 @@ export const upload = createPutMethod({ options.cacheControlMaxAge !== undefined ) { throw new BlobError( - 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.', + 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.' ); } }, @@ -115,13 +115,13 @@ async function importKey(token: string): Promise { new TextEncoder().encode(token), { name: 'HMAC', hash: 'SHA-256' }, false, - ['sign', 'verify'], + ['sign', 'verify'] ); } async function signPayload( payload: string, - token: string, + token: string ): Promise { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Node.js < 20: globalThis.crypto is undefined (in a real script.js, because the REPL has it linked to the crypto module). Node.js >= 20, Browsers and Cloudflare workers: globalThis.crypto is defined and is the Web Crypto API. if (!globalThis.crypto) { @@ -131,7 +131,7 @@ async function signPayload( const signature = await globalThis.crypto.subtle.sign( 'HMAC', await importKey(token), - new TextEncoder().encode(payload), + new TextEncoder().encode(payload) ); return Buffer.from(new Uint8Array(signature)).toString('hex'); } @@ -168,7 +168,7 @@ async function verifyCallbackSignature({ 'HMAC', await importKey(token), hexToArrayByte(signature), - new TextEncoder().encode(body), + new TextEncoder().encode(body) ); return verified; } @@ -194,7 +194,7 @@ export type DecodedClientTokenPayload = Omit< }; export function getPayloadFromClientToken( - clientToken: string, + clientToken: string ): DecodedClientTokenPayload { const [, , , , encodedToken] = clientToken.split('_'); const encodedPayload = Buffer.from(encodedToken ?? '', 'base64') @@ -229,7 +229,7 @@ export interface HandleUploadOptions { body: HandleUploadBody; onBeforeGenerateToken: ( pathname: string, - clientPayload?: string, + clientPayload?: string ) => Promise< Pick< GenerateClientTokenOptions, @@ -370,7 +370,7 @@ export async function generateClientTokenFromReadWriteToken({ }: GenerateClientTokenOptions): Promise { if (typeof window !== 'undefined') { throw new BlobError( - '"generateClientTokenFromReadWriteToken" must be called from a server environment', + '"generateClientTokenFromReadWriteToken" must be called from a server environment' ); } @@ -382,7 +382,7 @@ export async function generateClientTokenFromReadWriteToken({ if (!storeId) { throw new BlobError( - token ? 'Invalid `token` parameter' : 'Invalid `BLOB_READ_WRITE_TOKEN`', + token ? 'Invalid `token` parameter' : 'Invalid `BLOB_READ_WRITE_TOKEN`' ); } @@ -390,7 +390,7 @@ export async function generateClientTokenFromReadWriteToken({ JSON.stringify({ ...argsWithoutToken, validUntil: argsWithoutToken.validUntil ?? timestamp.getTime(), - }), + }) ).toString('base64'); const securedKey = await signPayload(payload, readWriteToken); @@ -399,7 +399,7 @@ export async function generateClientTokenFromReadWriteToken({ throw new BlobError('Unable to sign client token'); } return `vercel_blob_client_${storeId}_${Buffer.from( - `${securedKey}.${payload}`, + `${securedKey}.${payload}` ).toString('base64')}`; } From 96fe921edd53e3137d5dc4b3de3bc2900617f4c7 Mon Sep 17 00:00:00 2001 From: Daryl Cecile Date: Sat, 11 Nov 2023 00:57:12 +0000 Subject: [PATCH 3/3] fix broken lint --- packages/blob/src/client.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/blob/src/client.ts b/packages/blob/src/client.ts index 66114448d..6a71b68d4 100644 --- a/packages/blob/src/client.ts +++ b/packages/blob/src/client.ts @@ -22,7 +22,7 @@ export const put = createPutMethod({ extraChecks(options: ClientPutCommandOptions) { if (typeof window === 'undefined') { throw new BlobError( - 'client/`put` must be called from a client environment' + 'client/`put` must be called from a client environment', ); } @@ -37,7 +37,7 @@ export const put = createPutMethod({ options.cacheControlMaxAge !== undefined ) { throw new BlobError( - 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.' + 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.', ); } }, @@ -79,7 +79,7 @@ export const upload = createPutMethod({ extraChecks(options: UploadOptions) { if (typeof window === 'undefined') { throw new BlobError( - 'client/`upload` must be called from a client environment' + 'client/`upload` must be called from a client environment', ); } @@ -95,7 +95,7 @@ export const upload = createPutMethod({ options.cacheControlMaxAge !== undefined ) { throw new BlobError( - 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.' + 'addRandomSuffix and cacheControlMaxAge are not supported in client uploads. Configure these options at the server side when generating client tokens.', ); } }, @@ -115,13 +115,13 @@ async function importKey(token: string): Promise { new TextEncoder().encode(token), { name: 'HMAC', hash: 'SHA-256' }, false, - ['sign', 'verify'] + ['sign', 'verify'], ); } async function signPayload( payload: string, - token: string + token: string, ): Promise { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Node.js < 20: globalThis.crypto is undefined (in a real script.js, because the REPL has it linked to the crypto module). Node.js >= 20, Browsers and Cloudflare workers: globalThis.crypto is defined and is the Web Crypto API. if (!globalThis.crypto) { @@ -131,7 +131,7 @@ async function signPayload( const signature = await globalThis.crypto.subtle.sign( 'HMAC', await importKey(token), - new TextEncoder().encode(payload) + new TextEncoder().encode(payload), ); return Buffer.from(new Uint8Array(signature)).toString('hex'); } @@ -168,7 +168,7 @@ async function verifyCallbackSignature({ 'HMAC', await importKey(token), hexToArrayByte(signature), - new TextEncoder().encode(body) + new TextEncoder().encode(body), ); return verified; } @@ -194,7 +194,7 @@ export type DecodedClientTokenPayload = Omit< }; export function getPayloadFromClientToken( - clientToken: string + clientToken: string, ): DecodedClientTokenPayload { const [, , , , encodedToken] = clientToken.split('_'); const encodedPayload = Buffer.from(encodedToken ?? '', 'base64') @@ -229,7 +229,7 @@ export interface HandleUploadOptions { body: HandleUploadBody; onBeforeGenerateToken: ( pathname: string, - clientPayload?: string + clientPayload?: string, ) => Promise< Pick< GenerateClientTokenOptions, @@ -370,7 +370,7 @@ export async function generateClientTokenFromReadWriteToken({ }: GenerateClientTokenOptions): Promise { if (typeof window !== 'undefined') { throw new BlobError( - '"generateClientTokenFromReadWriteToken" must be called from a server environment' + '"generateClientTokenFromReadWriteToken" must be called from a server environment', ); } @@ -382,7 +382,7 @@ export async function generateClientTokenFromReadWriteToken({ if (!storeId) { throw new BlobError( - token ? 'Invalid `token` parameter' : 'Invalid `BLOB_READ_WRITE_TOKEN`' + token ? 'Invalid `token` parameter' : 'Invalid `BLOB_READ_WRITE_TOKEN`', ); } @@ -390,7 +390,7 @@ export async function generateClientTokenFromReadWriteToken({ JSON.stringify({ ...argsWithoutToken, validUntil: argsWithoutToken.validUntil ?? timestamp.getTime(), - }) + }), ).toString('base64'); const securedKey = await signPayload(payload, readWriteToken); @@ -399,7 +399,7 @@ export async function generateClientTokenFromReadWriteToken({ throw new BlobError('Unable to sign client token'); } return `vercel_blob_client_${storeId}_${Buffer.from( - `${securedKey}.${payload}` + `${securedKey}.${payload}`, ).toString('base64')}`; }