UNPKG

@vercel/blob

Version:

The Vercel Blob JavaScript API client

1 lines 19.6 kB
{"version":3,"sources":["/home/runner/work/storage/storage/packages/blob/dist/client.cjs","../src/client.ts"],"names":[],"mappings":"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","file":"/home/runner/work/storage/storage/packages/blob/dist/client.cjs","sourcesContent":[null,"// eslint-disable-next-line unicorn/prefer-node-protocol -- node:crypto does not resolve correctly in browser and edge runtime\nimport * as crypto from 'crypto';\nimport type { IncomingMessage } from 'node:http';\n// When bundled via a bundler supporting the `browser` field, then\n// the `undici` module will be replaced with https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API\n// for browser contexts. See ./undici-browser.js and ./package.json\nimport { fetch } from 'undici';\nimport type { BlobCommandOptions, WithUploadProgress } from './helpers';\nimport { BlobError, getTokenFromOptionsOrEnv } from './helpers';\nimport { createPutMethod } from './put';\nimport type { PutBlobResult } from './put-helpers';\nimport type { CommonCompleteMultipartUploadOptions } from './multipart/complete';\nimport { createCompleteMultipartUploadMethod } from './multipart/complete';\nimport { createCreateMultipartUploadMethod } from './multipart/create';\nimport { createUploadPartMethod } from './multipart/upload';\nimport type { CommonMultipartUploadOptions } from './multipart/upload';\nimport { createCreateMultipartUploaderMethod } from './multipart/create-uploader';\n\n// interface for put, upload and multipartUpload.\n// This types omits all options that are encoded in the client token.\nexport interface ClientCommonCreateBlobOptions {\n /**\n * Whether the blob should be publicly accessible.\n */\n access: 'public';\n /**\n * Defines the content type of the blob. By default, this value is inferred from the pathname. Sent as the 'content-type' header when downloading a blob.\n */\n contentType?: string;\n /**\n * `AbortSignal` to cancel the running request. See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal\n */\n abortSignal?: AbortSignal;\n}\n\n// shared interface for put and multipartUpload\nexport interface ClientTokenOptions {\n /**\n * A client token that was generated by your server using the `generateClientToken` method.\n */\n token: string;\n}\n\n// shared interface for put and upload\ninterface ClientCommonPutOptions\n extends ClientCommonCreateBlobOptions,\n WithUploadProgress {\n /**\n * Whether to use multipart upload. Use this when uploading large files. It will split the file into multiple parts, upload them in parallel and retry failed parts.\n */\n multipart?: boolean;\n}\n\nfunction createPutExtraChecks<\n TOptions extends ClientTokenOptions & ClientCommonCreateBlobOptions,\n>(methodName: string) {\n return function extraChecks(options: TOptions) {\n if (!options.token.startsWith('vercel_blob_client_')) {\n throw new BlobError(`${methodName} must be called with a client token`);\n }\n\n if (\n // @ts-expect-error -- Runtime check for DX.\n options.addRandomSuffix !== undefined ||\n // @ts-expect-error -- Runtime check for DX.\n options.cacheControlMaxAge !== undefined\n ) {\n throw new BlobError(\n `${methodName} doesn't allow addRandomSuffix and cacheControlMaxAge. Configure these options at the server side when generating client tokens.`,\n );\n }\n };\n}\n\n// client.put()\n\nexport type ClientPutCommandOptions = ClientCommonPutOptions &\n ClientTokenOptions;\n\nexport const put = createPutMethod<ClientPutCommandOptions>({\n allowedOptions: ['contentType'],\n extraChecks: createPutExtraChecks('client/`put`'),\n});\n\n// vercelBlob. createMultipartUpload()\n// vercelBlob. uploadPart()\n// vercelBlob. completeMultipartUpload()\n// vercelBlob. createMultipartUploader()\n\nexport type ClientCreateMultipartUploadCommandOptions =\n ClientCommonCreateBlobOptions & ClientTokenOptions;\n\nexport const createMultipartUpload =\n createCreateMultipartUploadMethod<ClientCreateMultipartUploadCommandOptions>({\n allowedOptions: ['contentType'],\n extraChecks: createPutExtraChecks('client/`createMultipartUpload`'),\n });\n\nexport const createMultipartUploader =\n createCreateMultipartUploaderMethod<ClientCreateMultipartUploadCommandOptions>(\n {\n allowedOptions: ['contentType'],\n extraChecks: createPutExtraChecks('client/`createMultipartUpload`'),\n },\n );\n\ntype ClientMultipartUploadCommandOptions = ClientCommonCreateBlobOptions &\n ClientTokenOptions &\n CommonMultipartUploadOptions &\n WithUploadProgress;\n\nexport const uploadPart =\n createUploadPartMethod<ClientMultipartUploadCommandOptions>({\n allowedOptions: ['contentType'],\n extraChecks: createPutExtraChecks('client/`multipartUpload`'),\n });\n\ntype ClientCompleteMultipartUploadCommandOptions =\n ClientCommonCreateBlobOptions &\n ClientTokenOptions &\n CommonCompleteMultipartUploadOptions;\n\nexport const completeMultipartUpload =\n createCompleteMultipartUploadMethod<ClientCompleteMultipartUploadCommandOptions>(\n {\n allowedOptions: ['contentType'],\n extraChecks: createPutExtraChecks('client/`completeMultipartUpload`'),\n },\n );\n\n// upload methods\n\nexport interface CommonUploadOptions {\n /**\n * A route that implements the `handleUpload` function for generating a client token.\n */\n handleUploadUrl: string;\n /**\n * Additional data which will be sent to your `handleUpload` route.\n */\n clientPayload?: string;\n}\n\n// client.upload()\n// This is a client-side wrapper that will fetch the client token for you and then upload the file\nexport type UploadOptions = ClientCommonPutOptions & CommonUploadOptions;\n/**\n * Uploads a blob into your store from the client.\n * Detailed documentation can be found here: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#client-uploads\n *\n * If you want to upload from your server instead, check out the documentation for the put operation: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#upload-a-blob\n *\n * @param pathname - The pathname to upload the blob to. This includes the filename.\n * @param body - The contents of your blob. This has to be a supported fetch body type https://developer.mozilla.org/en-US/docs/Web/API/fetch#body.\n * @param options - Additional options.\n */\nexport const upload = createPutMethod<UploadOptions>({\n allowedOptions: ['contentType'],\n extraChecks(options) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime check for DX.\n if (options.handleUploadUrl === undefined) {\n throw new BlobError(\n \"client/`upload` requires the 'handleUploadUrl' parameter\",\n );\n }\n\n if (\n // @ts-expect-error -- Runtime check for DX.\n options.addRandomSuffix !== undefined ||\n // @ts-expect-error -- Runtime check for DX.\n options.cacheControlMaxAge !== undefined\n ) {\n throw new BlobError(\n \"client/`upload` doesn't allow addRandomSuffix and cacheControlMaxAge. Configure these options at the server side when generating client tokens.\",\n );\n }\n },\n async getToken(pathname, options) {\n return retrieveClientToken({\n handleUploadUrl: options.handleUploadUrl,\n pathname,\n clientPayload: options.clientPayload ?? null,\n multipart: options.multipart ?? false,\n });\n },\n});\n\nasync function importKey(token: string): Promise<CryptoKey> {\n return globalThis.crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(token),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign', 'verify'],\n );\n}\n\nasync function signPayload(\n payload: string,\n token: string,\n): Promise<string | undefined> {\n // 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.\n if (!globalThis.crypto) {\n return crypto.createHmac('sha256', token).update(payload).digest('hex');\n }\n\n const signature = await globalThis.crypto.subtle.sign(\n 'HMAC',\n await importKey(token),\n new TextEncoder().encode(payload),\n );\n return Buffer.from(new Uint8Array(signature)).toString('hex');\n}\n\nasync function verifyCallbackSignature({\n token,\n signature,\n body,\n}: {\n token: string;\n signature: string;\n body: string;\n}): Promise<boolean> {\n // callback signature is signed using the server token\n const secret = token;\n // Browsers, Edge runtime and Node >=20 implement the Web Crypto API\n // 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.\n if (!globalThis.crypto) {\n // Node <20 falls back to the Node.js crypto module\n const digest = crypto\n .createHmac('sha256', secret)\n .update(body)\n .digest('hex');\n const digestBuffer = Buffer.from(digest);\n const signatureBuffer = Buffer.from(signature);\n\n return (\n digestBuffer.length === signatureBuffer.length &&\n crypto.timingSafeEqual(digestBuffer, signatureBuffer)\n );\n }\n\n const verified = await globalThis.crypto.subtle.verify(\n 'HMAC',\n await importKey(token),\n hexToArrayByte(signature),\n new TextEncoder().encode(body),\n );\n return verified;\n}\n\nfunction hexToArrayByte(input: string): Buffer {\n if (input.length % 2 !== 0) {\n throw new RangeError('Expected string to be an even number of characters');\n }\n const view = new Uint8Array(input.length / 2);\n\n for (let i = 0; i < input.length; i += 2) {\n view[i / 2] = parseInt(input.substring(i, i + 2), 16);\n }\n\n return Buffer.from(view);\n}\n\nexport type DecodedClientTokenPayload = Omit<\n GenerateClientTokenOptions,\n 'token'\n> & {\n validUntil: number;\n};\n\nexport function getPayloadFromClientToken(\n clientToken: string,\n): DecodedClientTokenPayload {\n const [, , , , encodedToken] = clientToken.split('_');\n const encodedPayload = Buffer.from(encodedToken ?? '', 'base64')\n .toString()\n .split('.')[1];\n const decodedPayload = Buffer.from(encodedPayload ?? '', 'base64').toString();\n return JSON.parse(decodedPayload) as DecodedClientTokenPayload;\n}\n\nconst EventTypes = {\n generateClientToken: 'blob.generate-client-token',\n uploadCompleted: 'blob.upload-completed',\n} as const;\n\ninterface GenerateClientTokenEvent {\n type: (typeof EventTypes)['generateClientToken'];\n payload: {\n pathname: string;\n callbackUrl: string;\n multipart: boolean;\n clientPayload: string | null;\n };\n}\ninterface UploadCompletedEvent {\n type: (typeof EventTypes)['uploadCompleted'];\n payload: {\n blob: PutBlobResult;\n tokenPayload?: string | null;\n };\n}\n\nexport type HandleUploadBody = GenerateClientTokenEvent | UploadCompletedEvent;\n\ntype RequestType = IncomingMessage | Request;\n\nexport interface HandleUploadOptions {\n body: HandleUploadBody;\n onBeforeGenerateToken: (\n pathname: string,\n clientPayload: string | null,\n multipart: boolean,\n ) => Promise<\n Pick<\n GenerateClientTokenOptions,\n | 'allowedContentTypes'\n | 'maximumSizeInBytes'\n | 'validUntil'\n | 'addRandomSuffix'\n | 'cacheControlMaxAge'\n > & { tokenPayload?: string | null }\n >;\n onUploadCompleted: (body: UploadCompletedEvent['payload']) => Promise<void>;\n token?: string;\n request: RequestType;\n}\n\nexport async function handleUpload({\n token,\n request,\n body,\n onBeforeGenerateToken,\n onUploadCompleted,\n}: HandleUploadOptions): Promise<\n | { type: GenerateClientTokenEvent['type']; clientToken: string }\n | { type: UploadCompletedEvent['type']; response: 'ok' }\n> {\n const resolvedToken = getTokenFromOptionsOrEnv({ token });\n\n const type = body.type;\n switch (type) {\n case 'blob.generate-client-token': {\n const { pathname, callbackUrl, clientPayload, multipart } = body.payload;\n const payload = await onBeforeGenerateToken(\n pathname,\n clientPayload,\n multipart,\n );\n const tokenPayload = payload.tokenPayload ?? clientPayload;\n\n // one hour\n const oneHourInSeconds = 60 * 60;\n const now = new Date();\n const validUntil =\n payload.validUntil ??\n now.setSeconds(now.getSeconds() + oneHourInSeconds);\n\n return {\n type,\n clientToken: await generateClientTokenFromReadWriteToken({\n ...payload,\n token: resolvedToken,\n pathname,\n onUploadCompleted: {\n callbackUrl,\n tokenPayload,\n },\n validUntil,\n }),\n };\n }\n case 'blob.upload-completed': {\n const signatureHeader = 'x-vercel-signature';\n const signature = (\n 'credentials' in request\n ? (request.headers.get(signatureHeader) ?? '')\n : (request.headers[signatureHeader] ?? '')\n ) as string;\n\n if (!signature) {\n throw new BlobError('Missing callback signature');\n }\n\n const isVerified = await verifyCallbackSignature({\n token: resolvedToken,\n signature,\n body: JSON.stringify(body),\n });\n\n if (!isVerified) {\n throw new BlobError('Invalid callback signature');\n }\n await onUploadCompleted(body.payload);\n return { type, response: 'ok' };\n }\n default:\n throw new BlobError('Invalid event type');\n }\n}\n\nasync function retrieveClientToken(options: {\n pathname: string;\n handleUploadUrl: string;\n clientPayload: string | null;\n multipart: boolean;\n abortSignal?: AbortSignal;\n}): Promise<string> {\n const { handleUploadUrl, pathname } = options;\n const url = isAbsoluteUrl(handleUploadUrl)\n ? handleUploadUrl\n : toAbsoluteUrl(handleUploadUrl);\n\n const event: GenerateClientTokenEvent = {\n type: EventTypes.generateClientToken,\n payload: {\n pathname,\n callbackUrl: url,\n clientPayload: options.clientPayload,\n multipart: options.multipart,\n },\n };\n\n const res = await fetch(url, {\n method: 'POST',\n body: JSON.stringify(event),\n headers: {\n 'content-type': 'application/json',\n },\n signal: options.abortSignal,\n });\n\n if (!res.ok) {\n throw new BlobError('Failed to retrieve the client token');\n }\n\n try {\n const { clientToken } = (await res.json()) as { clientToken: string };\n return clientToken;\n } catch (e) {\n throw new BlobError('Failed to retrieve the client token');\n }\n}\n\nfunction toAbsoluteUrl(url: string): string {\n // location is available in web workers too: https://developer.mozilla.org/en-US/docs/Web/API/Window/location\n return new URL(url, location.href).href;\n}\n\nfunction isAbsoluteUrl(url: string): boolean {\n try {\n return Boolean(new URL(url));\n } catch (e) {\n return false;\n }\n}\n\nexport async function generateClientTokenFromReadWriteToken({\n token,\n ...argsWithoutToken\n}: GenerateClientTokenOptions): Promise<string> {\n if (typeof window !== 'undefined') {\n throw new BlobError(\n '\"generateClientTokenFromReadWriteToken\" must be called from a server environment',\n );\n }\n\n const timestamp = new Date();\n timestamp.setSeconds(timestamp.getSeconds() + 30);\n const readWriteToken = getTokenFromOptionsOrEnv({ token });\n\n const [, , , storeId = null] = readWriteToken.split('_');\n\n if (!storeId) {\n throw new BlobError(\n token ? 'Invalid `token` parameter' : 'Invalid `BLOB_READ_WRITE_TOKEN`',\n );\n }\n\n const payload = Buffer.from(\n JSON.stringify({\n ...argsWithoutToken,\n validUntil: argsWithoutToken.validUntil ?? timestamp.getTime(),\n }),\n ).toString('base64');\n\n const securedKey = await signPayload(payload, readWriteToken);\n\n if (!securedKey) {\n throw new BlobError('Unable to sign client token');\n }\n return `vercel_blob_client_${storeId}_${Buffer.from(\n `${securedKey}.${payload}`,\n ).toString('base64')}`;\n}\n\nexport interface GenerateClientTokenOptions extends BlobCommandOptions {\n pathname: string;\n onUploadCompleted?: {\n callbackUrl: string;\n tokenPayload?: string | null;\n };\n maximumSizeInBytes?: number;\n allowedContentTypes?: string[];\n validUntil?: number;\n addRandomSuffix?: boolean;\n cacheControlMaxAge?: number;\n}\n\nexport { createFolder } from './create-folder';\n"]}