@vercel/blob
Advanced tools
Comparing version 0.11.0 to 0.12.0
@@ -1,90 +0,6 @@ | ||
import { Readable } from 'node:stream'; | ||
import { IncomingMessage } from 'node:http'; | ||
import { P as PutCommandOptions, a as PutBlobResult, B as BlobCommandOptions } from './put-6f84b94d.js'; | ||
export { b as BlobAccessError, c as BlobError, d as BlobUnknownError } from './put-6f84b94d.js'; | ||
import * as stream from 'stream'; | ||
declare class BlobError extends Error { | ||
constructor(message: string); | ||
} | ||
declare class BlobAccessError extends Error { | ||
constructor(); | ||
} | ||
declare class BlobUnknownError extends Error { | ||
constructor(); | ||
} | ||
interface GenerateClientTokenOptions extends BlobCommandOptions { | ||
pathname: string; | ||
onUploadCompleted?: { | ||
callbackUrl: string; | ||
metadata?: string | null; | ||
}; | ||
maximumSizeInBytes?: number; | ||
allowedContentTypes?: string[]; | ||
validUntil?: number; | ||
addRandomSuffix?: boolean; | ||
cacheControlMaxAge?: number; | ||
} | ||
declare function generateClientTokenFromReadWriteToken({ token, ...args }: GenerateClientTokenOptions): Promise<string>; | ||
declare function verifyCallbackSignature({ token, signature, body, }: { | ||
token?: string; | ||
signature: string; | ||
body: string; | ||
}): Promise<boolean>; | ||
type DecodedClientTokenPayload = Omit<GenerateClientTokenOptions, 'token'> & { | ||
validUntil: number; | ||
}; | ||
declare function getPayloadFromClientToken(clientToken: string): DecodedClientTokenPayload; | ||
declare const EventTypes: { | ||
readonly generateClientToken: "blob.generate-client-token"; | ||
readonly uploadCompleted: "blob.upload-completed"; | ||
}; | ||
interface GenerateClientTokenEvent { | ||
type: (typeof EventTypes)['generateClientToken']; | ||
payload: { | ||
pathname: string; | ||
callbackUrl: string; | ||
}; | ||
} | ||
interface BlobUploadCompletedEvent { | ||
type: (typeof EventTypes)['uploadCompleted']; | ||
payload: { | ||
blob: HeadBlobResult; | ||
metadata?: string; | ||
}; | ||
} | ||
type HandleBlobUploadBody = GenerateClientTokenEvent | BlobUploadCompletedEvent; | ||
type RequestType = IncomingMessage | Request; | ||
interface HandleBlobUploadOptions { | ||
body: HandleBlobUploadBody; | ||
onBeforeGenerateToken: (pathname: string) => Promise<Pick<GenerateClientTokenOptions, 'allowedContentTypes' | 'maximumSizeInBytes' | 'validUntil' | 'addRandomSuffix' | 'cacheControlMaxAge'> & { | ||
metadata?: string; | ||
}>; | ||
onUploadCompleted: (body: BlobUploadCompletedEvent['payload']) => Promise<void>; | ||
token?: string; | ||
request: RequestType; | ||
} | ||
declare function handleBlobUpload({ token, request, body, onBeforeGenerateToken, onUploadCompleted, }: HandleBlobUploadOptions): Promise<{ | ||
type: GenerateClientTokenEvent['type']; | ||
clientToken: string; | ||
} | { | ||
type: BlobUploadCompletedEvent['type']; | ||
response: 'ok'; | ||
}>; | ||
interface BlobCommandOptions { | ||
token?: string; | ||
} | ||
interface PutCommandOptions extends BlobCommandOptions { | ||
access: 'public'; | ||
contentType?: string; | ||
handleBlobUploadUrl?: string; | ||
addRandomSuffix?: boolean; | ||
cacheControlMaxAge?: number; | ||
} | ||
interface PutBlobResult { | ||
url: string; | ||
pathname: string; | ||
contentType: string; | ||
contentDisposition: string; | ||
} | ||
declare function put(pathname: string, body: string | Readable | Blob | ArrayBuffer | FormData | ReadableStream | File, options: PutCommandOptions): Promise<PutBlobResult>; | ||
declare const put: (pathname: string, body: string | stream.Readable | Blob | ArrayBuffer | FormData | ReadableStream<any> | File, options?: PutCommandOptions | undefined) => Promise<PutBlobResult>; | ||
declare function del(url: string[] | string, options?: BlobCommandOptions): Promise<void>; | ||
@@ -118,2 +34,2 @@ interface HeadBlobResult { | ||
export { BlobAccessError, BlobCommandOptions, BlobError, BlobUnknownError, BlobUploadCompletedEvent, GenerateClientTokenOptions, HandleBlobUploadBody, HandleBlobUploadOptions, HeadBlobResult, ListBlobResult, ListCommandOptions, PutBlobResult, PutCommandOptions, del, generateClientTokenFromReadWriteToken, getPayloadFromClientToken, handleBlobUpload, head, list, put, verifyCallbackSignature }; | ||
export { HeadBlobResult, ListBlobResult, ListCommandOptions, PutBlobResult, del, head, list, put }; |
@@ -0,231 +1,16 @@ | ||
import { | ||
BlobAccessError, | ||
BlobError, | ||
BlobUnknownError, | ||
createPutMethod, | ||
getApiUrl, | ||
getApiVersionHeader, | ||
getTokenFromOptionsOrEnv | ||
} from "./chunk-YDXL6NRJ.js"; | ||
// src/index.ts | ||
import { fetch } from "undici"; | ||
// src/helpers.ts | ||
function getToken(options) { | ||
if (typeof window !== "undefined") { | ||
if (!(options == null ? void 0 : options.token)) { | ||
throw new BlobError('"token" is required'); | ||
} | ||
if (!options.token.startsWith("vercel_blob_client")) { | ||
throw new BlobError("client upload only supports client tokens"); | ||
} | ||
} | ||
if (options == null ? void 0 : options.token) { | ||
return options.token; | ||
} | ||
if (!process.env.BLOB_READ_WRITE_TOKEN) { | ||
throw new Error( | ||
"BLOB_READ_WRITE_TOKEN environment variable is not set. Please set it to your write token." | ||
); | ||
} | ||
return process.env.BLOB_READ_WRITE_TOKEN; | ||
} | ||
var BlobError = class extends Error { | ||
constructor(message) { | ||
super(`Vercel Blob: ${message}`); | ||
} | ||
}; | ||
var BlobAccessError = class extends Error { | ||
constructor() { | ||
super( | ||
"Vercel Blob: Access denied, please provide a valid token for this resource" | ||
); | ||
} | ||
}; | ||
var BlobUnknownError = class extends Error { | ||
constructor() { | ||
super("Vercel Blob: Unknown error, please visit https://vercel.com/help"); | ||
} | ||
}; | ||
// src/client-upload.ts | ||
import * as crypto from "crypto"; | ||
async function generateClientTokenFromReadWriteToken({ | ||
token, | ||
...args | ||
}) { | ||
var _a; | ||
if (typeof window !== "undefined") { | ||
throw new Error( | ||
'"generateClientTokenFromReadWriteToken" must be called from a server environment' | ||
); | ||
} | ||
const timestamp = /* @__PURE__ */ new Date(); | ||
timestamp.setSeconds(timestamp.getSeconds() + 30); | ||
const blobToken = getToken({ token }); | ||
const [, , , storeId = null] = blobToken.split("_"); | ||
if (!storeId) { | ||
throw new Error( | ||
token ? 'Invalid "token" parameter' : "Invalid BLOB_READ_WRITE_TOKEN" | ||
); | ||
} | ||
const payload = Buffer.from( | ||
JSON.stringify({ | ||
...args, | ||
validUntil: (_a = args.validUntil) != null ? _a : timestamp.getTime() | ||
}) | ||
).toString("base64"); | ||
const securedKey = await signPayload(payload, blobToken); | ||
if (!securedKey) { | ||
throw new Error("Unable to sign client token"); | ||
} | ||
return `vercel_blob_client_${storeId}_${Buffer.from( | ||
`${securedKey}.${payload}` | ||
).toString("base64")}`; | ||
} | ||
async function importKey(token) { | ||
return globalThis.crypto.subtle.importKey( | ||
"raw", | ||
new TextEncoder().encode(getToken({ token })), | ||
{ name: "HMAC", hash: "SHA-256" }, | ||
false, | ||
["sign", "verify"] | ||
); | ||
} | ||
async function signPayload(payload, token) { | ||
if (!globalThis.crypto) { | ||
return crypto.createHmac("sha256", token).update(payload).digest("hex"); | ||
} | ||
const signature = await globalThis.crypto.subtle.sign( | ||
"HMAC", | ||
await importKey(token), | ||
new TextEncoder().encode(payload) | ||
); | ||
return Buffer.from(new Uint8Array(signature)).toString("hex"); | ||
} | ||
async function verifyCallbackSignature({ | ||
token, | ||
signature, | ||
body | ||
}) { | ||
const secret = getToken({ token }); | ||
if (!globalThis.crypto) { | ||
const digest = crypto.createHmac("sha256", secret).update(body).digest("hex"); | ||
const digestBuffer = Buffer.from(digest); | ||
const signatureBuffer = Buffer.from(signature); | ||
return digestBuffer.length === signatureBuffer.length && crypto.timingSafeEqual(digestBuffer, signatureBuffer); | ||
} | ||
const verified = await globalThis.crypto.subtle.verify( | ||
"HMAC", | ||
await importKey(token), | ||
hexToArrayByte(signature), | ||
new TextEncoder().encode(body) | ||
); | ||
return verified; | ||
} | ||
function hexToArrayByte(input) { | ||
if (input.length % 2 !== 0) { | ||
throw new RangeError("Expected string to be an even number of characters"); | ||
} | ||
const view = new Uint8Array(input.length / 2); | ||
for (let i = 0; i < input.length; i += 2) { | ||
view[i / 2] = parseInt(input.substring(i, i + 2), 16); | ||
} | ||
return Buffer.from(view); | ||
} | ||
function getPayloadFromClientToken(clientToken) { | ||
const [, , , , encodedToken] = clientToken.split("_"); | ||
const encodedPayload = Buffer.from(encodedToken != null ? encodedToken : "", "base64").toString().split(".")[1]; | ||
const decodedPayload = Buffer.from(encodedPayload != null ? encodedPayload : "", "base64").toString(); | ||
return JSON.parse(decodedPayload); | ||
} | ||
var EventTypes = { | ||
generateClientToken: "blob.generate-client-token", | ||
uploadCompleted: "blob.upload-completed" | ||
}; | ||
async function handleBlobUpload({ | ||
token, | ||
request, | ||
body, | ||
onBeforeGenerateToken, | ||
onUploadCompleted | ||
}) { | ||
var _a, _b, _c; | ||
const type = body.type; | ||
switch (type) { | ||
case "blob.generate-client-token": { | ||
const { pathname, callbackUrl } = body.payload; | ||
const payload = await onBeforeGenerateToken(pathname); | ||
return { | ||
type, | ||
clientToken: await generateClientTokenFromReadWriteToken({ | ||
...payload, | ||
token, | ||
pathname, | ||
onUploadCompleted: { | ||
callbackUrl, | ||
metadata: (_a = payload.metadata) != null ? _a : null | ||
} | ||
}) | ||
}; | ||
} | ||
case "blob.upload-completed": { | ||
const signatureHeader = "x-vercel-signature"; | ||
const signature = "credentials" in request ? (_b = request.headers.get(signatureHeader)) != null ? _b : "" : (_c = request.headers[signatureHeader]) != null ? _c : ""; | ||
if (!signature) { | ||
throw new Error("Invalid callback signature"); | ||
} | ||
const isVerified = await verifyCallbackSignature({ | ||
signature, | ||
body: JSON.stringify(body) | ||
}); | ||
if (!isVerified) { | ||
throw new Error("Invalid callback signature"); | ||
} | ||
await onUploadCompleted(body.payload); | ||
return { type, response: "ok" }; | ||
} | ||
default: | ||
throw new Error("Invalid event type"); | ||
} | ||
} | ||
// src/index.ts | ||
var BLOB_API_VERSION = 2; | ||
async function put(pathname, body, options) { | ||
if (!pathname) { | ||
throw new BlobError("pathname is required"); | ||
} | ||
if (!body) { | ||
throw new BlobError("body is required"); | ||
} | ||
if (!options || options.access !== "public") { | ||
throw new BlobError('access must be "public"'); | ||
} | ||
const token = shouldFetchClientToken(options) ? await retrieveClientToken({ | ||
handleBlobUploadUrl: options.handleBlobUploadUrl, | ||
pathname | ||
}) : getToken(options); | ||
const headers = { | ||
...getApiVersionHeader(), | ||
authorization: `Bearer ${token}` | ||
}; | ||
if (options.contentType) { | ||
headers["x-content-type"] = options.contentType; | ||
} | ||
if (options.addRandomSuffix !== void 0) { | ||
headers["x-add-random-suffix"] = options.addRandomSuffix ? "1" : "0"; | ||
} | ||
if (options.cacheControlMaxAge !== void 0) { | ||
headers["x-cache-control-max-age"] = options.cacheControlMaxAge.toString(); | ||
} | ||
const blobApiResponse = await fetch(getApiUrl(`/${pathname}`), { | ||
method: "PUT", | ||
body, | ||
headers, | ||
// required in order to stream some body types to Cloudflare | ||
// currently only supported in Node.js, we may have to feature detect this | ||
duplex: "half" | ||
}); | ||
if (blobApiResponse.status !== 200) { | ||
if (blobApiResponse.status === 403) { | ||
throw new BlobAccessError(); | ||
} else { | ||
throw new BlobUnknownError(); | ||
} | ||
} | ||
const blobResult = await blobApiResponse.json(); | ||
return blobResult; | ||
} | ||
var put = createPutMethod({ | ||
allowedOptions: ["cacheControlMaxAge", "addRandomSuffix", "contentType"] | ||
}); | ||
async function del(url, options) { | ||
@@ -236,3 +21,3 @@ const blobApiResponse = await fetch(getApiUrl("/delete"), { | ||
...getApiVersionHeader(), | ||
authorization: `Bearer ${getToken(options)}`, | ||
authorization: `Bearer ${getTokenFromOptionsOrEnv(options)}`, | ||
"content-type": "application/json" | ||
@@ -259,3 +44,3 @@ }, | ||
...getApiVersionHeader(), | ||
authorization: `Bearer ${getToken(options)}` | ||
authorization: `Bearer ${getTokenFromOptionsOrEnv(options)}` | ||
} | ||
@@ -291,3 +76,3 @@ }); | ||
...getApiVersionHeader(), | ||
authorization: `Bearer ${getToken(options)}` | ||
authorization: `Bearer ${getTokenFromOptionsOrEnv(options)}` | ||
} | ||
@@ -308,10 +93,2 @@ }); | ||
} | ||
function getApiUrl(pathname = "") { | ||
let baseUrl = null; | ||
try { | ||
baseUrl = process.env.VERCEL_BLOB_API_URL || process.env.NEXT_PUBLIC_VERCEL_BLOB_API_URL; | ||
} catch { | ||
} | ||
return `${baseUrl || "https://blob.vercel-storage.com"}${pathname}`; | ||
} | ||
function mapBlobResult(blobResult) { | ||
@@ -323,42 +100,2 @@ return { | ||
} | ||
function isAbsoluteUrl(url) { | ||
try { | ||
return Boolean(new URL(url)); | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
async function retrieveClientToken(options) { | ||
const { handleBlobUploadUrl, pathname } = options; | ||
const url = isAbsoluteUrl(handleBlobUploadUrl) ? handleBlobUploadUrl : `${window.location.origin}${handleBlobUploadUrl}`; | ||
const res = await fetch(url, { | ||
method: "POST", | ||
body: JSON.stringify({ | ||
type: EventTypes.generateClientToken, | ||
payload: { pathname, callbackUrl: url } | ||
}) | ||
}); | ||
if (!res.ok) { | ||
throw new BlobError("Failed to retrieve the client token"); | ||
} | ||
try { | ||
const { clientToken } = await res.json(); | ||
return clientToken; | ||
} catch (e) { | ||
throw new BlobError("Failed to retrieve the client token"); | ||
} | ||
} | ||
function shouldFetchClientToken(options) { | ||
return Boolean(!options.token && options.handleBlobUploadUrl); | ||
} | ||
function getApiVersionHeader() { | ||
let versionOverride = null; | ||
try { | ||
versionOverride = process.env.VERCEL_BLOB_API_VERSION_OVERRIDE || process.env.NEXT_PUBLIC_VERCEL_BLOB_API_VERSION_OVERRIDE; | ||
} catch { | ||
} | ||
return { | ||
"x-api-version": `${versionOverride != null ? versionOverride : BLOB_API_VERSION}` | ||
}; | ||
} | ||
export { | ||
@@ -369,10 +106,6 @@ BlobAccessError, | ||
del, | ||
generateClientTokenFromReadWriteToken, | ||
getPayloadFromClientToken, | ||
handleBlobUpload, | ||
head, | ||
list, | ||
put, | ||
verifyCallbackSignature | ||
put | ||
}; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@vercel/blob", | ||
"version": "0.11.0", | ||
"version": "0.12.0", | ||
"description": "The Vercel Blob JavaScript API client", | ||
@@ -14,2 +14,14 @@ "homepage": "https://vercel.com/storage/blob", | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.js", | ||
"require": "./dist/index.cjs" | ||
}, | ||
"./client": { | ||
"types": "./dist/client.d.ts", | ||
"import": "./dist/client.js", | ||
"require": "./dist/client.cjs" | ||
} | ||
}, | ||
"main": "./dist/index.cjs", | ||
@@ -16,0 +28,0 @@ "module": "./dist/index.js", |
@@ -16,3 +16,3 @@ # 🍙 @vercel/blob | ||
1. [Server uploads](https://vercel.com/docs/storage/vercel-blob/quickstart#server-uploads): This is the most common way to upload files. The file is first sent to your server and then to Vercel Blob. It's straightforward to implement, but you are limited to the request body your server can handle. Which in case of a Vercel-hosted website is 4.5 MB. **This means you can't upload files larger than 4.5 MB on Vercel when using this method.** | ||
2. [Browser uploads](https://vercel.com/docs/storage/vercel-blob/quickstart#browser-uploads): This is a more advanced solution for when you need to upload larger files. The file is securely sent directly from the browser to Vercel Blob. This requires a bit more work to implement, but it allows you to upload files up to 500 MB. | ||
2. [Client uploads](https://vercel.com/docs/storage/vercel-blob/quickstart#client-uploads): This is a more advanced solution for when you need to upload larger files. The file is securely sent directly from the client (a browser for example) to Vercel Blob. This requires a bit more work to implement, but it allows you to upload files up to 500 MB. | ||
@@ -35,7 +35,5 @@ ## API | ||
// or using Vercel Blob outside of Vercel | ||
// During browser uploads, a token is automatically generated by calling the route from the `handleBlobUploadUrl` option | ||
token?: string, | ||
// Browser uploads: this is the URL that will be called to generate a secure token before sending the file to Vercel Blob | ||
handleBlobUploadUrl?: string, | ||
cacheControlMaxAge?: number, // optional, a duration in seconds to configure the edge and browser caches. Defaults to one year. Can only be configured server side (either on server side put or during client token generation). | ||
addRandomSuffix?: boolean; // optional, allows to disable or enable random suffixes (defaults to `true`) | ||
cacheControlMaxAge?: number, // optional, a duration in seconds to configure the edge and browser caches. Defaults to one year for browsers and 5 minutes for edge cache. Can only be configured server side (either on server side put or during client token generation). The Edge cache maximum value is 5 minutes. | ||
}): Promise<{ | ||
@@ -104,16 +102,51 @@ pathname: string; | ||
### handleBlobUpload(options) | ||
### client/`upload(pathname, body, options)` | ||
Handles the requests to generate a client token and respond to the upload completed event. This is useful when [uploading from browsers](#browser-upload) to circumvent the 4.5 MB limitation of going through a Vercel-hosted route. | ||
The `upload` method is dedicated to client uploads. It fetches a client token using the `handleUploadUrl` before uploading the blob. | ||
Read the [client uploads](https://vercel.com/docs/storage/vercel-blob/quickstart#client-uploads) documentation to know more. | ||
```ts | ||
async function handleBlobUpload(options?: { | ||
async function upload( | ||
pathname: string, | ||
body: ReadableStream | String | ArrayBuffer | Blob | File // All fetch body types are supported: https://developer.mozilla.org/en-US/docs/Web/API/fetch#body | ||
options: { | ||
access: 'public', // mandatory, as we will provide private blobs in the future | ||
contentType?: string, // by default inferred from pathname | ||
// `token` defaults to process.env.BLOB_READ_WRITE_TOKEN on Vercel | ||
// and can be configured when you connect more stores to a project | ||
// or using Vercel Blob outside of Vercel | ||
handleUploadUrl?: string, // A string specifying the route to call for generating client tokens for client uploads | ||
clientPayload?: string, // A string that will be passed to the `onUploadCompleted` callback as `tokenPayload`. It can be used to attach data to the upload, like `JSON.stringify({ postId: 123 })`. | ||
}): Promise<{ | ||
pathname: string; | ||
contentType: string; | ||
contentDisposition: string; | ||
url: string; | ||
}> {} | ||
``` | ||
### client/`handleUpload(options)` | ||
This is a server-side route helper to manage client uploads, it has two responsibilities: | ||
1. Generate tokens for client uploads | ||
2. Listen for completed client uploads, so you can update your database with the URL of the uploaded file for example | ||
Read the [client uploads](https://vercel.com/docs/storage/vercel-blob/quickstart#client-uploads) documentation to know more. | ||
```ts | ||
async function handleUpload(options?: { | ||
token?: string; // default to process.env.BLOB_READ_WRITE_TOKEN | ||
request: IncomingMessage | Request; | ||
onBeforeGenerateToken: (pathname: string) => Promise<{ | ||
onBeforeGenerateToken: ( | ||
pathname: string, | ||
clientPayload?: string | ||
) => Promise<{ | ||
allowedContentTypes?: string[]; // optional, defaults to no restriction | ||
maximumSizeInBytes?: number; // optional, defaults and maximum is 500MB (524,288,000 bytes) | ||
validUntil?: number; // optional, timestamp in ms, by default now + 30s (30,000) | ||
addRandomSuffix?: boolean; // optional, allows to disable or enable random suffixes | ||
metadata?: string; | ||
addRandomSuffix?: boolean; // see `put` options | ||
cacheControlMaxAge?: number; // see `put` options | ||
tokenPayload?: string; // optional, defaults to whatever the client sent as `clientPayload` | ||
}>; | ||
@@ -124,3 +157,3 @@ onUploadCompleted: (body: { | ||
blob: PutBlobResult; | ||
metadata?: string; | ||
tokenPayload?: string; | ||
}; | ||
@@ -133,3 +166,3 @@ }) => Promise<void>; | ||
blob: PutBlobResult; | ||
metadata?: string; | ||
tokenPayload?: string; | ||
}; | ||
@@ -139,3 +172,7 @@ } | ||
type: 'blob.generate-client-token'; | ||
payload: { pathname: string; callbackUrl: string }; | ||
payload: { | ||
pathname: string; | ||
callbackUrl: string; | ||
clientPayload: string; | ||
}; | ||
}; | ||
@@ -148,32 +185,6 @@ }): Promise< | ||
Note: This method should be called server-side, not client-side. | ||
### generateClientTokenFromReadWriteToken(options) | ||
Generates a single-use token that can be used from within the client. This method is called internally by `handleBlobUpload`. | ||
Once created, a client token is valid by default for 30 seconds (can be customized by configuring the `validUntil` field). This means you have 30 seconds to initiate an upload with this token. | ||
```ts | ||
async function generateClientTokenFromReadWriteToken(options?: { | ||
token?: string; | ||
pathname?: string; | ||
onUploadCompleted?: { | ||
callbackUrl: string; | ||
metadata?: string; | ||
}; | ||
maximumSizeInBytes?: number; | ||
allowedContentTypes?: string[]; | ||
validUntil?: number; // optional, timestamp in ms, by default now + 30s (30,000) | ||
addRandomSuffix?: boolean; // see `put` options | ||
cacheControlMaxAge?: number; // see `put` options | ||
}): string {} | ||
``` | ||
Note: This is a server-side method. | ||
## Examples | ||
- [Next.js App Router examples](../../test/next/src/app/vercel/blob/) | ||
- [https.get, axios, and got](../../test/next/src/app/vercel/blob/script.ts) | ||
- [https.get, axios, and got](../../test/next/src/app/vercel/blob/script.mts) | ||
@@ -232,3 +243,3 @@ ## How to list all your blobs | ||
When transferring a file to a Serverless or Edge Functions route on Vercel, then the request body is limited to 4.5 MB. If you need to send larger files then use the [browser-upload](#browser-upload) method. | ||
When transferring a file to a Serverless or Edge Functions route on Vercel, then the request body is limited to 4.5 MB. If you need to send larger files then use the [client-upload](#client-upload) method. | ||
@@ -235,0 +246,0 @@ ## Running examples locally |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
127817
23
288
12
1114
6