@ustorage/sdk
Official TypeScript SDK for UStorage — chunked uploads, resume, and file URL resolution against the UStorage upload API.
Supports Node.js (≥20) and browser runtimes with separate entry points. Ships ESM and CommonJS with TypeScript declarations.
Install
npm install @ustorage/sdk
# or
pnpm add @ustorage/sdk
yarn add @ustorage/sdk
Entry points
| Import | Use when |
|---|---|
@ustorage/sdk |
Shared types, UploadApi, Uploader, errors — no runtime client |
@ustorage/sdk/node |
UStorageNodeClient — servers, scripts, workers |
@ustorage/sdk/browser |
UStorageBrowserClient — web apps (no secretKey) |
import type { CreateUploadSessionRequest, UploadObjectResult } from '@ustorage/sdk';
import { UStorageNodeClient } from '@ustorage/sdk/node';
import { UStorageBrowserClient } from '@ustorage/sdk/browser';
Requirements
- Node.js ≥ 20 (for
@ustorage/sdk/node) - A running UStorage upload service (
uploadBaseUrl, e.g.http://localhost:6900in development) - Credentials scoped to a workspace (see Authentication)
Quick start (Node)
Node clients can authenticate with access key + secret key or a Bearer token. Credentials are already bound to a workspace, so uploads use bucketName and key (not workspace/bucket UUIDs).
import { UStorageNodeClient } from '@ustorage/sdk/node';
const client = new UStorageNodeClient({
uploadBaseUrl: process.env.USTORAGE_UPLOAD_URL!,
auth: {
accessKey: process.env.USTORAGE_ACCESS_KEY!,
secretKey: process.env.USTORAGE_SECRET_KEY!,
},
});
const result = await client.putObject({
bucketName: 'videos',
key: 'campaigns/intro.mp4',
body: './video.mp4',
});
console.log(result.url, result.fileId);
Upload from a file path (key defaults to the basename of the path):
await client.uploadFile('./avatar.png', {
bucketName: 'assets',
key: 'users/u_123/avatar.png',
resume: true,
onProgress: (e) => console.log(`${e.progress}%`),
});
Buffer upload:
await client.putObject({
bucketName: 'assets',
key: 'data/report.json',
body: Buffer.from('{"ok":true}'),
contentType: 'application/json',
});
Bearer token (Node)
const client = new UStorageNodeClient({
uploadBaseUrl: process.env.USTORAGE_UPLOAD_URL!,
auth: {
bearerToken: () => fetchNewToken(), // string or async factory
},
});
Quick start (Browser)
Never pass secretKey in the browser. Mint a short-lived upload token on your backend, then pass it to the client.
import { UStorageBrowserClient } from '@ustorage/sdk/browser';
const uploadToken = await fetch('/api/upload-token', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ bucketName: 'assets', key: `uploads/${file.name}` }),
}).then((r) => r.text());
const client = new UStorageBrowserClient({
uploadBaseUrl: import.meta.env.VITE_USTORAGE_UPLOAD_URL,
uploadToken,
});
await client.uploadFile(file, {
bucketName: 'assets',
key: `uploads/${file.name}`,
onProgress: (event) => console.log(event.progress),
});
File / Blob via putObject:
await client.putObject({
bucketName: 'assets',
key: 'uploads/photo.jpg',
body: file,
});
Browser auth also supports bearerToken (same shape as Node) if your app already uses Bearer auth end-to-end.
Authentication
| Runtime | Method | Headers |
|---|---|---|
| Node | accessKey + secretKey |
x-access-key, x-secret-key |
| Node / Browser | bearerToken |
Authorization: Bearer … |
| Browser | uploadToken |
x-upload-token |
bearerToken and uploadToken accept a static string or () => string | Promise<string> for refresh.
Node credential auth uses raw access/secret headers because the API stores only a hash of secretKey; signed x-ustorage-* request auth is not available yet (nodeSigner.supported === false).
Minting upload tokens (backend)
Use client.uploads.createUploadToken on a Node client (or call POST /uploads/tokens directly):
const { token, expires_at } = await serverClient.uploads.createUploadToken({
bucket_name: 'assets',
key: 'uploads/demo.png',
mime_type: 'image/png',
max_file_size: 10 * 1024 * 1024,
expires_in: 900,
});
// Return `token` to the browser as `uploadToken`
Object keys and buckets
bucketName— bucket name in the credential’s workspace.key— S3-style object key: path segments are folders; the last segment is the filename. Missing folders are created by the backend.workspaceName— optional override when the API allows it.
Resolving URLs
// Public or private URL by bucket + key
const file = await client.getObjectUrl({
bucket: 'videos',
key: 'movies/demo.mp4',
});
// Signed / time-limited URL by file ID
const signed = await client.getSignedUrl({
fileId: 'file_uuid',
expiresIn: 3600,
});
console.log(file.url, signed.expiresAt);
getSignedUrl is an alias of getObjectUrl; both call POST /files/url.
Import from URL
Start a server-side import job from a remote URL:
const job = await client.importFromUrl({
url: 'https://example.com/video.mp4',
bucketName: 'videos',
key: 'imports/video.mp4',
visibility: 'private',
});
console.log(job.uploadId, job.fileId, job.sourceType);
Use the returned uploadId to track status via low-level API:
const status = await client.uploads.getStatus(job.uploadId);
console.log(status.status, status.progress);
importFromUrl calls POST /files/import-url and returns both camelCase convenience fields (uploadId, fileId, sourceType, expiresAt) and raw snake_case API fields.
Upload options
| Option | Default | Description |
|---|---|---|
bucketName |
— | Target bucket (required) |
key |
— | Object key (required for putObject; optional for uploadFile, derived from path/name) |
workspaceName |
— | Optional workspace name |
contentType |
inferred | MIME type; inferred from extension / file metadata when omitted |
metadata |
— | Arbitrary JSON metadata on the session |
overwrite |
true |
Replace existing object at the same key (backend soft-deletes the old object) |
visibility |
— | 'public' or 'private' |
checksum |
false |
false, 'sha256' (per-chunk hash), or { file?, chunks? } |
chunkSize |
8388608 (8 MiB) |
Bytes per chunk |
resume |
false |
Enable resume via resumeStore |
resumeKey |
fingerprint | Custom resume key; implies resume when set |
signal |
— | AbortSignal to cancel in-flight chunk uploads |
onProgress |
— | Callback after each chunk / status poll |
resolveUrlOnComplete |
false |
If complete does not return url, SDK resolves it via POST /files/url |
resolveUrlPollIntervalMs |
1200 |
Poll interval (ms) when waiting for file URL readiness after complete |
resolveUrlTimeoutMs |
45000 |
Max wait time (ms) for URL readiness before fallback stops polling |
Upload result
putObject / uploadFile resolve to UploadObjectResult:
{
uploadId: string;
fileId: string;
publicId?: string;
visibility?: 'public' | 'private' | string;
url?: string;
expiresAt?: string | null;
status: string;
// …plus snake_case fields from the API response
}
Nếu bạn muốn hành vi ổn định như S3 SDK (luôn cố gắng có URL sau upload), bật fallback:
const result = await client.uploadFile('./video.mp4', {
bucketName: 'videos',
key: 'imports/video.mp4',
resolveUrlOnComplete: true,
});
console.log(result.url);
When resolveUrlOnComplete is enabled, SDK only calls POST /files/url if POST /uploads/:id/complete does not include url.
If the file is not immediately ready (FILE_NOT_READY), SDK polls until URL is available or timeout is reached.
Resume
Resume is opt-in (resume: true or resumeKey). The SDK stores upload_id under a fingerprint of the source + key, polls missing chunks on retry, and clears the entry after completion.
Node — pass resumeStore on the client:
import { UStorageNodeClient, NodeFileResumeStore } from '@ustorage/sdk/node';
const client = new UStorageNodeClient({
uploadBaseUrl: '…',
auth: { … },
resumeStore: new NodeFileResumeStore('.ustorage-resume.json'),
});
Also available: NodeMemoryResumeStore (in-memory).
Browser — defaults to BrowserResumeStore (localStorage with prefix ustorage:upload:, falls back to memory). Override via resumeStore in client options.
Implement ResumeStore for custom persistence:
interface ResumeStore {
get(key: string): Promise<string | undefined> | string | undefined;
set(key: string, uploadId: string): Promise<void> | void;
delete(key: string): Promise<void> | void;
}
Low-level upload API
Both clients expose client.uploads (UploadApi) for direct control:
| Method | HTTP | Description |
|---|---|---|
createUploadToken |
POST /uploads/tokens |
Mint browser upload token |
createSession |
POST /uploads/sessions |
Start chunked session |
uploadChunk |
PUT /uploads/:id/chunks/:index |
Upload one chunk |
getStatus |
GET /uploads/:id/status |
Session status / missing chunks |
complete |
POST /uploads/:id/complete |
Finalize upload |
cancel |
DELETE /uploads/:id |
Cancel session |
createFileUrl |
POST /files/url |
Resolve CDN URL (video HLS may return playlist.m3u8) |
importFromUrl |
POST /files/import-url |
Create URL import job |
The high-level Uploader (used by putObject / uploadFile) orchestrates session creation, chunk loop, checksum headers (x-chunk-checksum), and completion.
Error handling
Failed API responses throw UStorageError:
import { UStorageError } from '@ustorage/sdk';
try {
await client.putObject({ … });
} catch (err) {
if (err instanceof UStorageError) {
console.error(err.code, err.status, err.requestId, err.details);
}
}
TypeScript
Types are exported from @ustorage/sdk (and re-exported from /node and /browser). Request/response DTOs use snake_case matching the HTTP API; client helpers like getObjectUrl accept camelCase (fileId, expiresIn, …).
Development
From the sdk package directory:
yarn install
yarn build # tsup → dist/
yarn typecheck
yarn test:upload # node test.js (requires env vars)
Environment variables used by examples/node-upload.ts and test.js:
USTORAGE_UPLOAD_URLor hardcoded base URLUSTORAGE_ACCESS_KEY,USTORAGE_SECRET_KEYUSTORAGE_BUCKET_NAME,USTORAGE_OBJECT_KEY(for tests)
Package exports
{
".": "./dist/core/index.js",
"./browser": "./dist/browser/index.js",
"./node": "./dist/node/index.js"
}
"type": "module" with dual ESM/CJS builds. Default main/module point at the Node build; import the subpath you need in application code.
Publish to npm
From the sdk directory (scoped package @ustorage must be published as public):
# 1. Login (once)
npm login
# 2. Build + dry-run — inspect tarball contents
npm run build
npm pack --dry-run
# 3. Publish (prepack rebuilds dist; prepublishOnly runs typecheck)
npm publish --access public
What gets published is controlled by package.json → "files": ["dist", "README.md", "LICENSE"]. Source, tests, and examples are excluded (see also .npmignore).
Before the first publish, set repository in package.json if you have a public Git URL:
"repository": {
"type": "git",
"url": "https://github.com/your-org/ustorage.git",
"directory": "sdk"
}