From dd77d892971af80aaa704fda41ccf9760e1ba372 Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Mon, 24 Aug 2026 14:02:38 +0530 Subject: [PATCH 1/3] feat(sdk): expose requestUploadUrl, confirmUpload and uploadToPresignedUrl in storage module --- sdks/urbackend-sdk/src/modules/storage.ts | 75 ++++++++++++++++++++--- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/sdks/urbackend-sdk/src/modules/storage.ts b/sdks/urbackend-sdk/src/modules/storage.ts index 85b6a1883..37f2334e9 100644 --- a/sdks/urbackend-sdk/src/modules/storage.ts +++ b/sdks/urbackend-sdk/src/modules/storage.ts @@ -105,11 +105,7 @@ export class StorageModule { } // step 1 — ask server for a signed URL - const { signedUrl, filePath } = await this.client.request<{ signedUrl: string; filePath: string }>( - "POST", - "/api/storage/upload-request", - { body: { filename: resolvedName, contentType, size: fileSize } } - ); + const { signedUrl, filePath } = await this.requestUploadUrl(resolvedName, contentType, fileSize); // step 2 — upload directly to cloud, server not involved const putResponse = await fetch(signedUrl, { @@ -123,12 +119,77 @@ export class StorageModule { } // step 3 — tell server we're done so it can verify + update quota + return this.confirmUpload(filePath, fileSize); + } + + /** + * Requests a pre-signed upload URL for direct-to-cloud client uploads. + * Useful when delegating uploads to a frontend client without exposing the secret key. + * + * @param {string} filename - The name of the file + * @param {string} contentType - The MIME type of the file + * @param {number} size - The size of the file in bytes + * @returns {Promise<{ signedUrl: string; filePath: string; token?: string }>} The pre-signed URL and file path + */ + public async requestUploadUrl(filename: string, contentType: string, size: number): Promise<{ signedUrl: string; filePath: string; token?: string }> { + return this.client.request<{ signedUrl: string; filePath: string; token?: string }>( + "POST", + "/api/storage/upload-request", + { body: { filename, contentType, size } } + ); + } + + /** + * Confirms a direct-to-cloud upload to finalize it and update quotas. + * + * @param {string} filePath - The path returned by requestUploadUrl + * @param {number} size - The size of the uploaded file + * @returns {Promise} The upload confirmation details + */ + public async confirmUpload(filePath: string, size: number): Promise { return this.client.request( "POST", "/api/storage/upload-confirm", - { body: { filePath, size: fileSize } } + { body: { filePath, size } } ); -} + } + + /** + * Uploads a file directly to a pre-signed URL. + * Useful in client-side applications (like React) where the signed URL is provided by your secure backend. + * + * @param {unknown} file - The file to upload (File, Blob, or Buffer) + * @param {string} signedUrl - The pre-signed URL obtained from your backend + * @param {string} [contentType] - The MIME type (optional, auto-detected from File/Blob) + * @returns {Promise} Resolves when the upload completes successfully + */ + public static async uploadToPresignedUrl(file: unknown, signedUrl: string, contentType?: string): Promise { + let fileData: BodyInit; + let type = contentType || "application/octet-stream"; + + if (typeof File !== "undefined" && file instanceof File) { + type = contentType || file.type || type; + fileData = file; + } else if (file instanceof Blob) { + type = contentType || file.type || type; + fileData = file; + } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(file)) { + fileData = file as unknown as BodyInit; + } else { + throw new Error("Unsupported file type. Pass a File, Blob, or Buffer."); + } + + const putResponse = await fetch(signedUrl, { + method: "PUT", + headers: { "Content-Type": type }, + body: fileData, + }); + + if (!putResponse.ok) { + throw new Error(`Direct upload to cloud failed: ${putResponse.status} ${putResponse.statusText}`); + } + } + /** * Deletes a file from storage by its path * From e61a11101b7d8be4f81c4bacae105de07f8312ce Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Mon, 24 Aug 2026 14:20:59 +0530 Subject: [PATCH 2/3] docs: add presigned URL upload methods to storage SDK docs --- mintlify/docs/sdk/storage.mdx | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/mintlify/docs/sdk/storage.mdx b/mintlify/docs/sdk/storage.mdx index 11541d28d..7a3b49fea 100644 --- a/mintlify/docs/sdk/storage.mdx +++ b/mintlify/docs/sdk/storage.mdx @@ -52,6 +52,58 @@ console.log(provider); // 'internal' --- +## Presigned URLs (Direct Client Uploads) + +To allow frontend users to upload files directly without passing them through your server, use the presigned URL flow. + +### 1. requestUploadUrl (Server) + +Generates a temporary upload URL. Requires your **secret key**. + +```typescript +requestUploadUrl(filename: string, contentType: string, size: number): Promise<{ signedUrl: string, filePath: string }> +``` + +### 2. uploadToPresignedUrl (Client) + +A static helper to push the file to the generated URL from the frontend. + +```typescript +import { StorageModule } from '@urbackend/sdk'; + +StorageModule.uploadToPresignedUrl(file: File | Blob | Buffer, signedUrl: string): Promise +``` + +### 3. confirmUpload (Server) + +Confirms the upload is complete and registers it in your storage quota. + +```typescript +confirmUpload(filePath: string, size: number): Promise +``` + +**Full Example (React + Node.js):** + +```typescript +// 1. Server: Generate link +app.post('/api/upload-link', async (req, res) => { + const link = await urbackend.storage.requestUploadUrl('cat.png', 'image/png', 500000); + res.json(link); +}); + +// 2. Client (React): Upload directly +const { signedUrl, filePath } = await fetch('/api/upload-link').then(r => r.json()); +await StorageModule.uploadToPresignedUrl(file, signedUrl); + +// 3. Server: Confirm +app.post('/api/upload-confirm', async (req, res) => { + const result = await urbackend.storage.confirmUpload(req.body.filePath, 500000); + res.json(result); +}); +``` + +--- + ## deleteFile Delete a previously uploaded file by its storage path. From 5e2bdd84594f191f4f947a5b143db5a969a1c0da Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Mon, 24 Aug 2026 14:26:11 +0530 Subject: [PATCH 3/3] fix(sdk): guard Blob instanceof checks for environments without Blob --- sdks/urbackend-sdk/src/modules/storage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/urbackend-sdk/src/modules/storage.ts b/sdks/urbackend-sdk/src/modules/storage.ts index 37f2334e9..739bde381 100644 --- a/sdks/urbackend-sdk/src/modules/storage.ts +++ b/sdks/urbackend-sdk/src/modules/storage.ts @@ -92,7 +92,7 @@ export class StorageModule { contentType = file.type || contentType; fileSize = file.size; fileData = file; - } else if (file instanceof Blob) { + } else if (typeof Blob !== "undefined" && file instanceof Blob) { contentType = file.type || contentType; fileSize = file.size; fileData = file; @@ -170,7 +170,7 @@ export class StorageModule { if (typeof File !== "undefined" && file instanceof File) { type = contentType || file.type || type; fileData = file; - } else if (file instanceof Blob) { + } else if (typeof Blob !== "undefined" && file instanceof Blob) { type = contentType || file.type || type; fileData = file; } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(file)) {