npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@procedures/s3

v0.1.0

Published

AWS S3 as typed procedures: upload, download with safe part files, list, delete and multipart uploads.

Readme

@procedures/s3

npm

License: MIT TypeScript Node.js

AWS S3 operations as RPC procedures. Upload, download, list, delete, and perform multipart uploads via client.call().

Overview

client-s3 provides comprehensive AWS S3 access through the client procedure ecosystem with:

  • Standard operations: Upload, download, list, delete objects
  • Multipart uploads: Large file uploads with resumable parts
  • Environment-based config: AWS credentials from environment variables
  • Zod validation: Runtime type safety on all inputs
  • MinIO/LocalStack support: Custom S3 endpoints for testing

The package auto-registers 9 procedures:

  • Standard: s3.upload, s3.download, s3.list, s3.listAll, s3.delete
  • Multipart: s3.multipart.init, s3.multipart.upload, s3.multipart.complete, s3.multipart.abort

Architecture

graph TB
    subgraph "Client Ecosystem"
        Client["@procedures/client"]
        Registry["Procedure Registry"]
    end

    subgraph "client-s3 Package"
        Register["register.ts<br/>(Auto-registers procedures)"]
        Types["types.ts<br/>(Zod Schemas + Types)"]
        S3Client["s3-client.ts<br/>(Singleton S3Client)"]

        subgraph "Procedure Categories"
            StandardOps["Standard Operations<br/>upload, download, list, delete"]
            MultipartOps["Multipart Operations<br/>init, upload, complete, abort"]
        end
    end

    subgraph "AWS SDK"
        AWSS3["@aws-sdk/client-s3"]
        Commands["PutObjectCommand<br/>GetObjectCommand<br/>ListObjectsV2Command<br/>DeleteObjectCommand<br/>CreateMultipartUploadCommand<br/>UploadPartCommand<br/>CompleteMultipartUploadCommand<br/>AbortMultipartUploadCommand"]
    end

    subgraph "Configuration"
        EnvVars["Environment Variables<br/>AWS_ACCESS_KEY_ID<br/>AWS_SECRET_ACCESS_KEY<br/>AWS_REGION<br/>S3_ENDPOINT"]
    end

    subgraph "S3 Storage"
        S3["AWS S3<br/>or<br/>MinIO/LocalStack"]
    end

    Register --> Client
    Register --> Registry

    StandardOps --> S3Client
    MultipartOps --> S3Client

    S3Client --> EnvVars
    S3Client --> AWSS3
    AWSS3 --> Commands
    Commands --> S3

    Types -.validates.-> StandardOps
    Types -.validates.-> MultipartOps

    style Register fill:#e1f5ff
    style S3Client fill:#fff4e1
    style Types fill:#f0f0f0

Package Dependency Flow

graph LR
    App["Your Application"]
    ClientS3["@procedures/s3"]
    Client["@procedures/client"]
    Zod["zod"]
    AWSSDK["@aws-sdk/client-s3"]

    App --> ClientS3
    ClientS3 --> Client
    ClientS3 --> Zod
    ClientS3 --> AWSSDK

    App -.client.call.-> Client
    Client -.executes.-> ClientS3
    ClientS3 -.validates.-> Zod
    ClientS3 -.S3 ops.-> AWSSDK

Installation

npm install @procedures/s3

Peer Dependencies

{
  "@procedures/client": "github:mark1russell7/client#main",
  "@aws-sdk/client-s3": "^3.705.0",
  "zod": "^3.24.0"
}

Configuration

Environment Variables

# Static credentials (optional — omit to use an IAM role / SSO / profile)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_SESSION_TOKEN=your-session-token  # For STS/SSO temporary credentials

# Optional
AWS_REGION=us-east-1  # Default: us-east-1
S3_ENDPOINT=http://localhost:9000  # For MinIO/LocalStack

The package supplies explicit static credentials only when both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set (adding AWS_SESSION_TOKEN when present). If they are not set, it falls back to the AWS SDK default credential provider chain — SSO, IAM roles/instance profiles (IMDS), and shared config profiles all work with no code changes. Never hardcode credentials in your code.

S3 Client Configuration

The package uses a singleton S3Client that's automatically configured from environment variables:

import { getS3Config, getS3Client, resetS3Client } from "@procedures/s3";

// Get current config (from environment)
const config = getS3Config();
console.log(config.region);

// Get singleton S3 client
const s3 = getS3Client();

// Reset client (useful for testing or credential refresh)
resetS3Client();

API Reference

Standard Operations

s3.upload

Upload content to an S3 bucket.

const result = await client.exec(
  ["s3", "upload"],
  {
    bucket: "my-bucket",
    key: "files/document.pdf",
    body: fileContent,  // String or base64 encoded
    contentType: "application/pdf",
    base64: true,  // If body is base64 encoded
    metadata: {
      uploadedBy: "user123",
      timestamp: "2024-03-15T10:30:00Z"
    }
  }
);

console.log(result.etag);
console.log(result.location);  // s3://my-bucket/files/document.pdf

Input:

interface S3UploadInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key (path in bucket)
  body: string;                      // Content (string or base64)
  contentType?: string;              // MIME type
  base64?: boolean;                  // If true, body is base64 encoded
  metadata?: Record<string, string>; // Custom metadata
}

Output:

interface S3UploadOutput {
  etag: string;                      // Object ETag
  versionId?: string;                // Version ID (if versioning enabled)
  location: string;                  // Full S3 URI
}

s3.download

Download content from an S3 bucket.

const result = await client.exec(
  ["s3", "download"],
  {
    bucket: "my-bucket",
    key: "files/document.pdf",
    encoding: "utf8",  // Omit for binary (returns base64)
    versionId: "abc123"  // Optional specific version
  }
);

console.log(result.body);  // Content as string or base64
console.log(result.contentType);
console.log(result.contentLength);

Input:

interface S3DownloadInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  encoding?: string;                 // Text encoding (utf8, ascii, etc.)
                                     // Omit for binary (returns base64)
  versionId?: string;                // Specific version to download
}

Output:

interface S3DownloadOutput {
  body: string;                      // Content (string if encoding specified, base64 otherwise)
  contentLength: number;             // Size in bytes
  contentType?: string;              // MIME type
  etag?: string;                     // Object ETag
  lastModified?: string;             // ISO timestamp
}

s3.list

List objects in an S3 bucket.

const result = await client.exec(
  ["s3", "list"],
  {
    bucket: "my-bucket",
    prefix: "files/",
    maxKeys: 100,
    continuationToken: undefined,  // For pagination
    delimiter: "/"  // For hierarchical listing
  }
);

console.log(`Found ${result.keyCount} objects`);
result.contents.forEach(obj => {
  console.log(`${obj.key} - ${obj.size} bytes`);
});

if (result.isTruncated) {
  console.log("More results available with token:", result.nextContinuationToken);
}

Input:

interface S3ListInput {
  bucket: string;                    // S3 bucket name
  prefix?: string;                   // Filter by prefix
  maxKeys?: number;                  // Max objects to return (default: 1000)
  continuationToken?: string;        // Pagination token
  delimiter?: string;                // Delimiter for hierarchical listing (e.g., "/")
}

Output:

interface S3ListOutput {
  contents: S3ListObject[];          // List of objects
  commonPrefixes: string[];          // Common prefixes (when using delimiter)
  isTruncated: boolean;              // More results available
  nextContinuationToken?: string;    // Token for next page
  keyCount: number;                  // Number of keys returned
}

interface S3ListObject {
  key: string;                       // Object key
  size: number;                      // Size in bytes
  lastModified: string;              // ISO timestamp
  etag: string;                      // Object ETag
  storageClass?: string;             // Storage class (STANDARD, GLACIER, etc.)
}

s3.listAll

List every object under a bucket/prefix, following continuationToken automatically. Use this when a bucket/prefix may contain more than 1000 keys — s3.list returns at most one page, whereas s3.listAll loops until the result is no longer truncated and returns the aggregated contents.

const result = await client.exec(
  ["s3", "listAll"],
  {
    bucket: "my-bucket",
    prefix: "snapshots/",
    maxKeys: 1000,   // Optional: page size per underlying request
    delimiter: "/"   // Optional: hierarchical listing
  }
);

console.log(`Found ${result.keyCount} objects across ${result.pageCount} pages`);

Input:

interface S3ListAllInput {
  bucket: string;                    // S3 bucket name
  prefix?: string;                   // Filter by prefix
  maxKeys?: number;                  // Page size per request (default/max 1000)
  delimiter?: string;                // Delimiter for hierarchical listing (e.g., "/")
}

Output:

interface S3ListAllOutput {
  contents: S3ListObject[];          // All objects (not capped at 1000)
  commonPrefixes: string[];          // Deduplicated common prefixes across pages
  keyCount: number;                  // Total number of keys returned
  pageCount: number;                 // Number of ListObjectsV2 requests made
}

Warning: s3.listAll buffers the full key set in memory and issues one request per 1000 keys. For very large buckets prefer paging with s3.list + continuationToken.

s3.delete

Delete an object from an S3 bucket.

const result = await client.exec(
  ["s3", "delete"],
  {
    bucket: "my-bucket",
    key: "files/old-document.pdf",
    versionId: "abc123"  // Optional specific version
  }
);

console.log(result.deleted);  // true

Input:

interface S3DeleteInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  versionId?: string;                // Specific version to delete
}

Output:

interface S3DeleteOutput {
  deleted: boolean;                  // Whether deletion was successful
  deleteMarker?: boolean;            // Delete marker created (if versioning enabled)
  versionId?: string;                // Version ID of delete marker
}

Multipart Upload Operations

For large files (>5MB), use multipart upload:

s3.multipart.init

Initialize a multipart upload.

const result = await client.exec(
  ["s3", "multipart", "init"],
  {
    bucket: "my-bucket",
    key: "large-files/video.mp4",
    contentType: "video/mp4",
    metadata: {
      originalName: "vacation-video.mp4",
      uploadedBy: "user123"
    }
  }
);

console.log(result.uploadId);  // Save this for subsequent part uploads

Input:

interface S3MultipartInitInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  contentType?: string;              // MIME type
  metadata?: Record<string, string>; // Custom metadata
}

Output:

interface S3MultipartInitOutput {
  uploadId: string;                  // Upload ID for subsequent operations
  bucket: string;                    // Bucket name
  key: string;                       // Object key
}

s3.multipart.upload

Upload a part in a multipart upload.

const result = await client.exec(
  ["s3", "multipart", "upload"],
  {
    bucket: "my-bucket",
    key: "large-files/video.mp4",
    uploadId: uploadId,  // From init
    partNumber: 1,  // Part number (1-10000)
    body: partData  // Base64 encoded part data
  }
);

console.log(result.etag);  // Save etag for complete
console.log(result.partNumber);

Input:

interface S3MultipartUploadInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  uploadId: string;                  // Upload ID from init
  partNumber: number;                // Part number (1-10000)
  body: string;                      // Part content (base64 encoded)
}

Output:

interface S3MultipartUploadOutput {
  etag: string;                      // Part ETag (required for complete)
  partNumber: number;                // Part number
}

s3.multipart.complete

Complete a multipart upload by assembling parts.

const result = await client.exec(
  ["s3", "multipart", "complete"],
  {
    bucket: "my-bucket",
    key: "large-files/video.mp4",
    uploadId: uploadId,
    parts: [
      { partNumber: 1, etag: "etag1" },
      { partNumber: 2, etag: "etag2" },
      { partNumber: 3, etag: "etag3" }
    ]
  }
);

console.log(result.location);  // Final object location
console.log(result.etag);      // Final object ETag

Input:

interface S3MultipartCompleteInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  uploadId: string;                  // Upload ID
  parts: Array<{                     // Uploaded parts (ordered by partNumber)
    etag: string;
    partNumber: number;
  }>;
}

Output:

interface S3MultipartCompleteOutput {
  location: string;                  // Object location URL
  bucket: string;                    // Bucket name
  key: string;                       // Object key
  etag: string;                      // Final object ETag
}

s3.multipart.abort

Abort a multipart upload and clean up parts.

const result = await client.exec(
  ["s3", "multipart", "abort"],
  {
    bucket: "my-bucket",
    key: "large-files/video.mp4",
    uploadId: uploadId
  }
);

console.log(result.aborted);  // true

Input:

interface S3MultipartAbortInput {
  bucket: string;                    // S3 bucket name
  key: string;                       // Object key
  uploadId: string;                  // Upload ID to abort
}

Output:

interface S3MultipartAbortOutput {
  aborted: boolean;                  // Whether abort was successful
}

Usage Examples

Basic Upload and Download

import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/s3"; // registers its procedures

const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));

// Upload text content
await client.exec(
  ["s3", "upload"],
  {
    bucket: "my-bucket",
    key: "data/config.json",
    body: JSON.stringify({ env: "production" }),
    contentType: "application/json"
  }
);

// Upload binary file (base64 encoded)
const fileBuffer = await fs.promises.readFile("./image.png");
const base64Content = fileBuffer.toString("base64");

await client.exec(
  ["s3", "upload"],
  {
    bucket: "my-bucket",
    key: "images/photo.png",
    body: base64Content,
    base64: true,
    contentType: "image/png",
    metadata: {
      uploadedBy: "user123",
      originalName: "vacation-photo.png"
    }
  }
);

// Download text content
const { body } = await client.exec(
  ["s3", "download"],
  {
    bucket: "my-bucket",
    key: "data/config.json",
    encoding: "utf8"
  }
);

const config = JSON.parse(body);

// Download binary content (returns base64)
const { body: imageBase64 } = await client.exec(
  ["s3", "download"],
  {
    bucket: "my-bucket",
    key: "images/photo.png"
  }
);

const imageBuffer = Buffer.from(imageBase64, "base64");

List and Delete Objects

// List all objects in a prefix
const { contents } = await client.exec(
  ["s3", "list"],
  {
    bucket: "my-bucket",
    prefix: "data/",
    maxKeys: 1000
  }
);

console.log(`Found ${contents.length} objects`);

// List with pagination
let continuationToken: string | undefined;
do {
  const result = await client.exec(
    ["s3", "list"],
    {
      bucket: "my-bucket",
      prefix: "logs/",
      maxKeys: 100,
      continuationToken
    }
  );

  result.contents.forEach(obj => {
    console.log(`${obj.key} - ${obj.size} bytes`);
  });

  continuationToken = result.nextContinuationToken;
} while (continuationToken);

// Delete objects
for (const obj of contents) {
  await client.exec(
    ["s3", "delete"],
    {
      bucket: "my-bucket",
      key: obj.key
    }
  );
}

Hierarchical Listing (Folders)

// List "folders" in a bucket
const { contents, commonPrefixes } = await client.exec(
  ["s3", "list"],
  {
    bucket: "my-bucket",
    prefix: "data/",
    delimiter: "/"
  }
);

console.log("Files in data/:");
contents.forEach(obj => {
  console.log(`  ${obj.key}`);
});

console.log("Subdirectories:");
commonPrefixes.forEach(prefix => {
  console.log(`  ${prefix}`);
});

Multipart Upload (Large Files)

const PART_SIZE = 5 * 1024 * 1024;  // 5MB parts

// 1. Initialize upload
const { uploadId } = await client.exec(
  ["s3", "multipart", "init"],
  {
    bucket: "my-bucket",
    key: "videos/large-file.mp4",
    contentType: "video/mp4"
  }
);

try {
  // 2. Upload parts
  const file = await fs.promises.readFile("./large-file.mp4");
  const parts = [];

  for (let i = 0; i < file.length; i += PART_SIZE) {
    const partNumber = Math.floor(i / PART_SIZE) + 1;
    const partData = file.slice(i, Math.min(i + PART_SIZE, file.length));
    const base64Part = partData.toString("base64");

    const { etag } = await client.exec(
      ["s3", "multipart", "upload"],
      {
        bucket: "my-bucket",
        key: "videos/large-file.mp4",
        uploadId,
        partNumber,
        body: base64Part
      }
    );

    parts.push({ partNumber, etag });
    console.log(`Uploaded part ${partNumber}`);
  }

  // 3. Complete upload
  const result = await client.exec(
    ["s3", "multipart", "complete"],
    {
      bucket: "my-bucket",
      key: "videos/large-file.mp4",
      uploadId,
      parts
    }
  );

  console.log(`Upload complete: ${result.location}`);

} catch (error) {
  // 4. Abort on error
  await client.exec(
    ["s3", "multipart", "abort"],
    {
      bucket: "my-bucket",
      key: "videos/large-file.mp4",
      uploadId
    }
  );
  throw error;
}

Backup Script Example

import { glob } from "fast-glob";

// Backup directory to S3
const files = await glob("./data/**/*", { onlyFiles: true });

for (const file of files) {
  const content = await fs.promises.readFile(file);
  const s3Key = file.replace("./data/", "backups/");

  await client.exec(
    ["s3", "upload"],
    {
      bucket: "my-backup-bucket",
      key: s3Key,
      body: content.toString("base64"),
      base64: true,
      metadata: {
        backupDate: new Date().toISOString(),
        originalPath: file
      }
    }
  );

  console.log(`Backed up: ${file} -> s3://my-backup-bucket/${s3Key}`);
}

Using with MinIO or LocalStack

# Set custom endpoint for local S3-compatible storage
export S3_ENDPOINT=http://localhost:9000
export AWS_ACCESS_KEY_ID=minioadmin
export AWS_SECRET_ACCESS_KEY=minioadmin
export AWS_REGION=us-east-1
// Works the same way with MinIO/LocalStack
await client.exec(
  ["s3", "upload"],
  {
    bucket: "test-bucket",
    key: "test.txt",
    body: "Hello MinIO!"
  }
);

Integration with Ecosystem

Bundle Packages

client-s3 can be included in bundle packages:

// In bundle-data/src/register.ts
import "@procedures/s3/register.js";
import "@procedures/mongo"; // registers its procedures
import "@procedures/fs"; // registers its procedures

Dependencies

Runtime Dependencies

  • @procedures/client - Core client procedure system
  • @aws-sdk/client-s3 (^3.705.0) - AWS SDK for S3
  • zod (^3.24.0) - Runtime type validation

Development Dependencies

  • @mark1russell7/cue - Configuration and type utilities
  • typescript (^5.9.3) - TypeScript compiler
  • @types/node (^22.0.0) - Node.js type definitions

Package Structure

client-s3/
├── src/
│   ├── types.ts                      # Zod schemas + TypeScript types
│   ├── s3-client.ts                  # S3Client singleton factory
│   ├── register.ts                   # Auto-registration
│   ├── index.ts                      # Main entry point
│   ├── procedures/
│   │   └── s3/
│   │       ├── index.ts              # Procedure exports
│   │       ├── upload.ts             # s3.upload
│   │       ├── download.ts           # s3.download
│   │       ├── list.ts               # s3.list
│   │       ├── delete.ts             # s3.delete
│   │       ├── multipart-init.ts     # s3.multipart.init
│   │       ├── multipart-upload.ts   # s3.multipart.upload
│   │       ├── multipart-complete.ts # s3.multipart.complete
│   │       └── multipart-abort.ts    # s3.multipart.abort
├── dist/                             # Compiled output
├── package.json
├── tsconfig.json
└── README.md

Development

Build

npm run build

Type Check

npm run typecheck

Clean

npm run clean

Security

  • Never hardcode AWS credentials in code
  • Prefer IAM roles / instance profiles when running on AWS infrastructure: leave AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY unset and the SDK default credential provider chain resolves role credentials automatically
  • Use static AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary SSO/STS credentials) only where an IAM role is not available
  • Restrict S3 bucket policies to minimum required permissions
  • Enable S3 bucket versioning for important data
  • Consider S3 bucket encryption at rest

License

MIT

Author

Mark Russell [email protected]