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

s3kit

v0.1.1

Published

Secure, server-driven, framework-agnostic S3 file manager (core + HTTP adapters + client helper).

Readme

s3kit-demo

s3kit

A secure, server-driven, framework-agnostic S3 file manager with a React UI.

Package modules:

  • core: S3 operations (virtual folders, pagination, presigned uploads, previews)
  • http: thin HTTP handler (maps requests to core)
  • adapters/*: framework adapters (Express, Next.js, Fetch/Remix)
  • client: browser helper (typed API calls + multi-file upload orchestration)

Install

npm i s3kit

Quickstart (local testing)

This repo includes a working Next.js example with a customizer UI and live preview.

1) Set up the Next.js example

cd examples/nextjs-app

# Copy environment template and configure
cp .env.example .env

Edit .env with your S3 credentials:

AWS_REGION=us-east-1
S3_BUCKET=your-bucket-name
S3_ROOT_PREFIX=dev  # optional, keeps test files under dev/

Then run:

npm install
npm run dev

Open http://localhost:3000

2) Configure S3 CORS (required for browser uploads)

Uploads use presigned PUT URLs, which means the browser uploads directly to S3.

Your bucket must allow CORS from your UI origin (example: http://localhost:3000) with:

  • PUT (uploads)
  • GET (previews)
  • HEAD (often used by browsers)

Example CORS configuration:

[
  {
    "AllowedOrigins": ["http://localhost:3000"],
    "AllowedMethods": ["GET", "PUT", "HEAD", "OPTIONS"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"]
  }
]

If uploads still fail with a CORS error:

  • Confirm the UI origin matches exactly (scheme, host, and port).
  • Ensure the bucket CORS rules are applied to the correct bucket.
  • Include OPTIONS and PUT in AllowedMethods (preflight + upload).
  • If you set custom headers in prepareUploads, include them in AllowedHeaders.

Credentials and security

  • S3 credentials must be configured on the server (Node).
  • Do not put credentials in the browser app.
  • Credentials are read from environment variables or AWS SDK defaults.

Server-side (core)

import { S3Client } from '@aws-sdk/client-s3';
import { S3FileManager } from 's3kit/core';

const s3 = new S3Client({ region: process.env.AWS_REGION });

const manager = new S3FileManager(s3, {
  bucket: process.env.S3_BUCKET!,
  rootPrefix: 'uploads',
  authorizationMode: 'deny-by-default',
  lockFolderMoves: true,
  lockPrefix: '.s3kit/locks',
  lockTtlSeconds: 60 * 15,
  hooks: {
    authorize: ({ ctx }) => Boolean(ctx.userId),
    allowAction: ({ action, path }) => {
      if (action === 'file.delete') return false;
      if (path && path.startsWith('private/')) return false;
      return true;
    }
  }
});

authorize returning false responds with a 401. allowAction returning false responds with a 403. authorizationMode defaults to deny-by-default.

Authorization hooks (agnostic)

Use authorize for auth checks and allowAction for per-action rules. Both hooks are optional, but with the default deny-by-default, you must provide at least one.

Example: API key auth (framework-agnostic)

const manager = new S3FileManager(s3, {
  bucket: process.env.S3_BUCKET!,
  authorizationMode: 'deny-by-default',
  hooks: {
    authorize: ({ ctx }) => ctx.apiKey === process.env.FILE_MANAGER_API_KEY
  }
});

S3-compatible endpoints (MinIO, LocalStack, R2, Spaces, Wasabi, ...)

You can point the AWS SDK client at an S3-compatible endpoint:

const s3 = new S3Client({
  region: process.env.AWS_REGION,
  endpoint: process.env.S3_ENDPOINT,
  forcePathStyle: process.env.S3_FORCE_PATH_STYLE === '1'
});

Virtual folder listing + pagination

const page1 = await manager.list({ path: '', limit: 100 }, { userId: '123' });
const page2 = await manager.list({ path: '', cursor: page1.nextCursor, limit: 100 }, { userId: '123' });

HTTP layer

The HTTP handler expects JSON POST requests.

Routes:

  • POST /list
  • POST /search
  • POST /folder/create
  • POST /folder/delete
  • POST /folder/lock/get
  • POST /files/delete
  • POST /files/copy
  • POST /files/move
  • POST /upload/prepare
  • POST /preview
  • POST /file/attributes/get
  • POST /file/attributes/set

Notes:

  • POST /search returns file entries in stable, S3 listing order (lexicographic by key/path) so pagination via cursor is deterministic.
  • cursor is the underlying S3 continuation token; pass nextCursor from the previous response to fetch the next page.
  • Conflict detection: pass ifMatch (ETag) for file copy, move, and delete to avoid overwriting if the object changed. A failed precondition returns 409 conflict.
  • Conditional uploads: pass ifNoneMatch: "*" in upload/prepare items to enforce "only if not exists".
  • Folder lock status: POST /folder/lock/get returns lock metadata (or null) for folder rename operations.
  • File attributes: POST /file/attributes/get returns content headers, metadata, and expiresAt; POST /file/attributes/set updates them.

Next.js (App Router) adapter

// app/api/s3/[...path]/route.ts
import { createNextRouteHandlerFromEnv } from 's3kit/adapters/next';

export const POST = createNextRouteHandlerFromEnv({
  basePath: '/api/s3',
  authorization: {
    mode: 'allow-by-default'
  },
  env: {
    region: 'AWS_REGION',
    bucket: 'S3_BUCKET',
    rootPrefix: 'S3_ROOT_PREFIX',
    endpoint: 'S3_ENDPOINT',
    forcePathStyle: 'S3_FORCE_PATH_STYLE',
    requireUserId: 'REQUIRE_USER_ID'
  }
});

This helper reads env values from the map above. At minimum, region and bucket must point to defined env vars. For production, replace the example allow-by-default with your own authorize / allowAction hooks.

Express adapter

import express from 'express';
import { createExpressS3FileManagerHandler } from 's3kit/adapters/express';

const app = express();
app.use(express.json({ limit: '2mb' }));

app.use(
  '/api/s3',
  createExpressS3FileManagerHandler({
    manager,
    getContext: (req) => ({ userId: req.header('x-user-id') ?? undefined }),
    api: { basePath: '/api/s3' }
  })
);

app.listen(3001);

Fetch/Remix adapter

import { createFetchHandler } from 's3kit/adapters/fetch';

export const handler = createFetchHandler({
  manager,
  getContext: async (req) => ({ userId: req.headers.get('x-user-id') ?? undefined }),
  api: { basePath: '/api/s3' }
});

Client helper

import { S3FileManagerClient } from 's3kit/client';

const client = new S3FileManagerClient({
  apiUrl: '/api/s3'
});

const listing = await client.list({ path: '' });

const preview = await client.getPreviewUrl({ path: 'docs/readme.pdf', inline: true });

await client.uploadFiles({
  files: [
    { file: someFile, path: `docs/${someFile.name}` },
    { file: otherFile, path: `docs/${otherFile.name}` }
  ],
  hooks: {
    onUploadProgress: ({ path, loaded, total }) => {
      console.log(path, loaded, total);
    }
  }
});

const lock = await client.getFolderLock({ path: 'docs/' });

Alternative client config

If you prefer splitting origin + mount path:

const client = new S3FileManagerClient({
  baseUrl: 'http://localhost:3000',
  basePath: '/api/s3'
});

Multiple S3 configs (multiple API endpoints)

For multi-bucket / multi-environment setups, keep configs on the server and expose them as separate API endpoints.

  • The client simply points to the right apiUrl (e.g. /api/s3 vs /api/s3-media).
  • The server routes each endpoint to its own S3FileManager instance.

Example (framework-agnostic http handler):

import { createS3FileManagerHttpHandler } from 's3kit/http';

export const s3Handler = createS3FileManagerHttpHandler({
  getManager: () => managers.default,
  api: { basePath: '/api/s3' }
});

export const mediaHandler = createS3FileManagerHttpHandler({
  getManager: () => managers.media,
  api: { basePath: '/api/s3-media' }
});

React embed examples

Common UI props

  • theme: 'light' | 'dark' | 'system'
  • mode: 'viewer' | 'picker' | 'manager'
  • selection: 'single' | 'multiple'
  • toolbar: { search, breadcrumbs, viewSwitcher, sort }
  • labels: text overrides for buttons and placeholders
  • viewMode: 'grid' | 'list'

The React UI is styled with CSS variables + CSS modules to keep styles scoped.

1) Viewer (read-only file browser)

import { FileManager } from 's3kit/react';

export function FileViewer() {
  return (
    <FileManager
      apiUrl="/api/s3"
      mode="viewer"
      allowActions={{ upload: false, createFolder: false, delete: false, rename: false, move: false, copy: false, restore: false }}
    />
  );
}

2) Picker (select one or many files)

import { FilePicker } from 's3kit/react';

export function FileField() {
  return (
    <FilePicker
      apiUrl="/api/s3"
      selection="single"
      onConfirm={(entries) => {
        const file = entries[0];
        console.log('Selected:', file);
      }}
      onSelectionChange={(entries) => {
        console.log('Current selection:', entries);
      }}
      confirmLabel="Use file"
      allowActions={{ upload: true, createFolder: true }}
    />
  );
}

3) Manager (full CRUD)

import { FileManager } from 's3kit/react';

export function FileManagerAdmin() {
  return (
    <FileManager
      apiUrl="/api/s3"
      mode="manager"
      selection="multiple"
      allowActions={{
        upload: true,
        createFolder: true,
        delete: true,
        rename: true,
        move: true,
        copy: true,
        restore: true
      }}
    />
  );
}

UI customization (toolbar + labels)

import { FileManager } from 's3kit/react';

export function FileManagerCustomized() {
  return (
    <FileManager
      apiUrl="/api/s3"
      toolbar={{ search: false, breadcrumbs: true, viewSwitcher: true, sort: false }}
      labels={{
        upload: 'Add files',
        newFolder: 'Create folder',
        delete: 'Remove',
        deleteForever: 'Remove forever',
        restore: 'Restore',
        emptyTrash: 'Clear trash',
        confirm: 'Select',
        searchPlaceholder: 'Search...'
      }}
      viewMode="grid"
    />
  );
}

Example

  • examples/nextjs-app: Next.js App Router example with a customizer panel and live preview

Security checklist

  • Keep S3 credentials on the server only.
  • Require auth on the API route (session/JWT/API key).
  • Use deny-by-default and implement authorize / allowAction.
  • Enforce least-privilege IAM policies for the bucket.
  • Validate inputs (paths, allowed actions) and consider rate limiting.