npm.io
0.1.3 • Published 1 week ago

@ustorage/sdk

Licence
MIT
Version
0.1.3
Deps
0
Size
391 kB
Vulns
0
Weekly
0

@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:6900 in 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_URL or hardcoded base URL
  • USTORAGE_ACCESS_KEY, USTORAGE_SECRET_KEY
  • USTORAGE_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"
}

License

MIT