Skip to content
github-actions[bot] edited this page Oct 4, 2026 · 2 revisions

S3

Typed client for AWS S3 and S3-compatible object storage (Cloudflare R2, MinIO, DigitalOcean Spaces, self-hosted gateways) for Deno, Bun, Node.js and Cloudflare Workers, signing every request with AWS Signature Version 4. Object CRUD and listing, plus streamed multipart uploads and downloads. A lightweight, Web-API-only alternative to @aws-sdk/client-s3 for the operations it covers.

JSR JSR Score

Overview

S3 provides putObject, getObject, deleteObject, listObjects, and headObject — the object-storage interface shared across this repo's storage connects. It uses RESTler for transport and Guardian for runtime response validation. Every request is signed with SigV4 entirely via Web Crypto (crypto.subtle) — no node:crypto — so signing works unmodified on Deno, Bun, Node, Cloudflare Workers, and in the browser.

Testing your code

Your tests don't need to fake HTTP. Have your code take an S3 instance and pass in a stand-in that returns the shapes from @tundraconnect/s3/schemas or throws a real S3Error:

import { S3Error } from '@tundraconnect/s3/errors';

const outage = new S3Error('SERVICE_UNAVAILABLE', { status: 503 });
console.log(outage.code);

To exercise the client itself against a fake transport, subclass it and reassign the protected _fetch.

Documentation

Topic Description
API Client configuration and endpoint methods
Errors Error codes and diagnostic metadata
Schemas Public Guardian schemas and inferred types

Upstream

Installation

Deno:

deno add jsr:@tundraconnect/s3

Bun:

bunx jsr add @tundraconnect/s3

Node.js:

npx jsr add @tundraconnect/s3

Quick Start

import { S3 } from '@tundraconnect/s3';

const client = new S3({
  auth: {
    type: 'CUSTOM',
    accessKeyId: 'AKIA...',
    secretAccessKey: '...',
    region: 'us-east-1',
  },
});

await client.putObject({
  bucket: 'my-bucket',
  key: 'hello.txt',
  body: 'Hello!',
});

const obj = await client.getObject({ bucket: 'my-bucket', key: 'hello.txt' });
console.log(await obj.body.text());

Cloudflare R2 / MinIO (path-style addressing)

A custom baseURL automatically switches to path-style addressing (a bucket subdomain won't resolve against most non-AWS endpoints):

import { S3 } from '@tundraconnect/s3';

const r2 = new S3({
  baseURL: 'https://<account-id>.r2.cloudflarestorage.com',
  auth: {
    type: 'CUSTOM',
    accessKeyId: '...',
    secretAccessKey: '...',
    region: 'auto',
  },
});

DigitalOcean Spaces (virtual-hosted-style addressing)

Unlike R2/MinIO, Spaces addresses buckets by subdomain — the same virtual-hosted-style AWS S3 itself uses — even though it also needs a custom baseURL. That combination is exactly what the forcePathStyle auto-detection heuristic above doesn't expect, so set it to false explicitly. The signing region is the literal Spaces region code (e.g. nyc3, sfo3, ams3, sgp1, fra1), not an AWS region:

import { S3 } from '@tundraconnect/s3';

const spaces = new S3({
  baseURL: 'https://nyc3.digitaloceanspaces.com',
  forcePathStyle: false,
  auth: {
    type: 'CUSTOM',
    accessKeyId: '...', // Spaces access key
    secretAccessKey: '...', // Spaces secret key
    region: 'nyc3',
  },
});

await spaces.putObject({
  bucket: 'my-space',
  key: 'hello.txt',
  body: 'Hello!',
});

This resolves requests against https://my-space.nyc3.digitaloceanspaces.com/hello.txt. Reads can instead be served through the Spaces CDN by pointing baseURL (and, for signed requests, only the read path) at https://nyc3.cdn.digitaloceanspaces.com — putObject/deleteObject/ listObjects still need the origin endpoint above, since the CDN mirror is read-only.

Large files

import { S3 } from '@tundraconnect/s3';

const client = new S3({
  auth: {
    type: 'CUSTOM',
    accessKeyId: 'AKIA...',
    secretAccessKey: '...',
    region: 'us-east-1',
  },
});

// Upload from a stream as a multipart upload — one 8 MiB part in memory
// at a time. Small bodies fall back to a single PUT automatically.
const file = await Deno.open('backup.tar');
await client.putObjectStream({
  bucket: 'backups',
  key: 'backup.tar',
  body: file.readable,
});

// Download as a stream — nothing buffered.
const { body } = await client.getObjectStream({
  bucket: 'backups',
  key: 'backup.tar',
});
await body.pipeTo((await Deno.create('restored.tar')).writable);

Works unchanged against DigitalOcean Spaces, Cloudflare R2 and MinIO. See API → Streaming.

License

MIT. Part of Tundra Connect, typed vendor API clients for Deno, Bun, Node.js and Cloudflare Workers.

Clone this wiki locally