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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@thetechfossil/upfiles

v1.0.9

Published

Lightweight client and React components for Upfiles Plugin API (presigned S3)

Readme

@thetechfossil/upfiles

Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.

Documentation

📚 Full documentation available at: https://ttf-upfiles-docs.netlify.app/

For detailed API reference, guides, examples, and more information, visit the documentation site.

  • Client: get presigned URLs and PUT to S3
  • React <Uploader />: drag/drop, progress, single/multiple, accepts/types, size limits
  • React <ProjectFilesWidget />: list all files for a project (Plugin API), pick one, and optionally auto-save to your app

Installation

# Bun (recommended)
bun add @thetechfossil/upfiles

# npm
npm install @thetechfossil/upfiles

# pnpm
pnpm add @thetechfossil/upfiles

# yarn
yarn add @thetechfossil/upfiles

Quickstart

import { Uploader, ConnectProjectDialog } from '@thetechfossil/upfiles';
import { useState } from 'react';

export default function Page() {
  const [open, setOpen] = useState(false);
  const [apiKey, setApiKey] = useState<string | null>(null);

  return (
    <div className="space-y-6">
      <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
        Connect to Upfiles
      </button>

      <Uploader
        clientOptions={{
          baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
          apiKey: apiKey ?? undefined, // set after connecting
          apiKeyHeader: 'authorization',
        }}
        multiple
        dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
        buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
      />

      <ConnectProjectDialog
        baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
        open={open}
        onOpenChange={setOpen}
        onConnected={(key) => setApiKey(key)}
      />
    </div>
  );
}

Server prerequisites

Your Upfiles app must expose the Plugin API presign endpoint:

  • Method: POST
  • Path: /api/plugin/upload/presigned-url (default; configurable)
  • Body: { fileName, fileType, fileSize, folderPath?, projectId? } (fileSize is required)
  • Response: { presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }

This repository already provides a Next.js route at src/app/api/plugin/upload/presigned-url/route.ts.

For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.

Basic usage (React)

import { Uploader } from '@thetechfossil/upfiles';

export default function Page() {
  return (
    <Uploader
      multiple
      accept={["image/*", "application/pdf"]}
      maxFileSize={100 * 1024 * 1024}
      onComplete={(files) => {
        console.log('uploaded:', files);
      }}
      onError={(err) => {
        console.error(err);
      }}
      dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
      buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
      // Optional: pass clientOptions for cross-origin + API key auth
      // clientOptions={{
      //   baseUrl: 'https://YOUR_APP_HOST',
      //   // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
      //   apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
      //   apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
      // }}
    />
  );
}

Basic usage (vanilla JS)

import { UpfilesClient } from '@thetechfossil/upfiles';

// Same-origin by default; only set these when needed:
const client = new UpfilesClient({
  // presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
  // baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
  // presignPath: '/api/plugin/upload/presigned-url', // default
  // withCredentials: true, // for session routes (not typical for Plugin API)
  // Provide API key for Plugin API auth:
  // apiKey: 'upk_...'
  // apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
});

async function upload(file) {
  const { presignedUrl, publicUrl } = await client.getPresignedUrl({
    fileName: file.name,
    fileType: file.type,
    fileSize: file.size, // required
    folderPath: 'my-plugin/uploads/',
  });
  const res = await client.uploadToS3(presignedUrl, file);
  if (!res.ok) throw new Error('Upload failed');
  return publicUrl;
}

API

  • new UpfilesClient(options)

    • presignUrl (optional): full absolute URL to presign endpoint. Highest priority.
    • baseUrl (optional): API base origin. Used with presignPath.
    • presignPath (optional): relative path, default /api/plugin/upload/presigned-url.
    • headers (optional): extra headers to send.
    • apiKey (optional): API key value (e.g., upk_...).
    • apiKeyHeader (optional): one of 'authorization' | 'x-api-key' | 'x-up-api-key' (default 'authorization'). If 'authorization', Bearer prefix is auto-added for upk_....
    • withCredentials (optional): true to send cookies.
    • Defaults: same-origin requests to /api/plugin/upload/presigned-url.
  • client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })

  • client.uploadToS3(presignedUrl, file)

  • client.upload(file, { projectId?, folderPath?, fetchThumbnails? }) → returns { publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }

  • client.getThumbnails(fileKey) → returns Thumbnail[]

  • client.getProjectFiles({ folderPath? }) → returns FileListItem[]

  • <Uploader /> props

    • clientOptions (optional): same as UpfilesClient options (only needed for cross-origin or auth customization)
    • multiple (default true)
    • accept (array of MIME patterns)
    • maxFileSize (bytes, default 100MB)
    • maxFiles (default 10)
    • fetchThumbnails (boolean): if true, fetch thumbnails after upload via Plugin API
    • onComplete(files) and onError(error)
    • className, buttonClassName, dropzoneClassName
    • children: custom dropzone inner content

Thumbnails usage

Your Upfiles app exposes GET /api/plugin/thumbnails?fileKey=... for plugins. Enable fetchThumbnails to auto-fetch after upload, or call client.getThumbnails(fileKey) yourself.

Example with client.upload:

const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
console.log(result.thumbnails);

List project files (Plugin API)

Your Upfiles app exposes GET /api/plugin/files?folderPath=... for plugins (API key auth + CORS). The client can call it via getProjectFiles:

import { UpfilesClient } from '@thetechfossil/upfiles';

const client = new UpfilesClient({
  baseUrl: 'https://YOUR_APP_HOST',
  apiKey: 'upk_...',
  thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
});

const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
// [{ key, originalName, size, contentType, uploadedAt, url }, ...]

<ProjectFilesWidget />

Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.

import { ProjectFilesWidget } from '@thetechfossil/upfiles';

export default function PickFile() {
  return (
    <ProjectFilesWidget
      clientOptions={{
        baseUrl: 'https://YOUR_APP_HOST',
        apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
        apiKeyHeader: 'authorization',
        thumbnailsPath: '/api/plugin/thumbnails',
      }}
      // Optional: filter within a folder
      // folderPath="my-plugin/"

      onSelect={(f) => {
        // { name, key, url, size, contentType }
        console.log('Selected:', f.name, f.key);
      }}

      // Optional: built-in save
      // saveUrl="/api/files/save" // Your app route to persist selection
      // onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
      // onSaved={(result) => console.log('Saved!', result)}
    />
  );
}

Notes

  • Dev with Vite: configure a proxy so /api/* maps to your backend (e.g., http://localhost:4000), then <Uploader /> works with no config.
  • Ensure CORS where applicable (S3 bucket CORS; and set PLUGIN_ALLOWED_ORIGINS on your Upfiles app for cross-origin Plugin API calls).
  • For cross-origin with cookies (NextAuth), set withCredentials: true and configure your server to allow credentials. Plugin API typically uses API keys instead of cookies.

Plugin endpoints provided by this repo

  • POST /api/plugin/upload/presigned-url – generate presigned S3 URL for upload
  • GET /api/plugin/thumbnails?fileKey=... – list thumbnails for a file
  • GET /api/plugin/files?folderPath=... – list project files (API key scoped), optional folder prefix

Connect a project to get an API key

This package ships a ready-made dialog built with Radix UI + Tailwind classes to help developers connect their project to your main Upfiles app and obtain an API key.

Install

Your app must have Tailwind configured. The dialog uses Radix primitives (shadcn/ui compatible). Ensure the peer dependency is available in the host app:

bun add @thetechfossil/upfiles @radix-ui/react-dialog
# or
npm install @thetechfossil/upfiles @radix-ui/react-dialog

ConnectProjectDialog (UI modal)

Props:

  • baseUrl: your Upfiles app origin (e.g., https://upfiles.example.com).
  • open, onOpenChange: control the dialog.
  • onConnected(apiKey, { projectId?, source }): receives the plaintext API key and metadata.

It presents three options:

  • Connect to existing project → lists projects via GET /api/projects, then creates a key via POST /api/projects/:id/keys.
  • Add API key manually → text input to paste an existing key.
  • Create new project → POST /api/projects, then POST /api/projects/:id/keys.
import { useState } from 'react';
import { ConnectProjectDialog } from '@thetechfossil/upfiles';

export default function ConnectKeyButton() {
  const [open, setOpen] = useState(false);
  const [apiKey, setApiKey] = useState<string | null>(null);

  return (
    <div>
      <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
        Connect Upfiles
      </button>

      <ConnectProjectDialog
        baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
        open={open}
        onOpenChange={setOpen}
        onConnected={(key, meta) => {
          setApiKey(key);
          // Persist the key in your own DB as needed (see below)
          console.log('Connected via', meta.source, 'projectId', meta.projectId);
        }}
      />
    </div>
  );
}

Store the key in your own DB

You control persistence. Example Next.js route handler to store an encrypted key for the current user:

// app/api/integrations/upfiles/key/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions as any);
  const userId = (session as any)?.user?.id as string | undefined;
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const { apiKey } = await req.json();
  if (!apiKey) return NextResponse.json({ error: 'apiKey required' }, { status: 400 });

  // TODO: encrypt before saving
  await prisma.user.update({ where: { id: userId }, data: { upfilesApiKey: apiKey } });
  return NextResponse.json({ ok: true });
}

From onConnected, call this route to persist:

await fetch('/api/integrations/upfiles/key', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ apiKey })
});

Utility functions (no UI)

If you prefer to build a custom UI, use the exported functions:

import {
  listProjects,
  createProject,
  connectProject,
  addApiKeyManually,
  createClientWithKey,
} from '@thetechfossil/upfiles';

const baseUrl = 'https://upfiles.example.com';

// 1) List existing projects (requires session cookie on baseUrl)
const projects = await listProjects({ baseUrl });

// 2) Create a new project and key
const { apiKey, projectId } = await createProject({ baseUrl, name: 'My App' });

// 3) Create a key for an existing project
const key2 = await connectProject({ baseUrl, projectId: 'proj_123' });

// 4) Manual
const manual = addApiKeyManually('upk_...');

// 5) Build an upload client with the key
const client = createClientWithKey(baseUrl, apiKey);

<ImageManager /> props

type ImageManagerProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  clientOptions: UpfilesClientOptions;
  projectId?: string;
  folderPath?: string;
  title?: string;
  description?: string;
  className?: string;
  gridClassName?: string;
  onSelect: (image: {
    url: string;
    key: string;
    originalName: string;
    size: number;
    contentType: string;
    thumbnails?: { id: string; key: string; url: string; size: number; sizeType?: string }[];
  }) => void;
  onDelete?: (key: string) => Promise<void>;
  deleteUrl?: string;
  autoRecordToDb?: boolean;
  fetchThumbnails?: boolean;
  maxFileSize?: number;
  maxFiles?: number;
  mode?: 'full' | 'browse' | 'upload'; // default: 'full'
  showDelete?: boolean; // default: true
};

Image picker modal (Browse Mode)

You can use ImageManager in browse mode to let users select existing images without the upload tab:

import { useState } from 'react';
import { ImageManager } from '@thetechfossil/upfiles';

export default function PickImage() {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
        Pick image
      </button>
      <ImageManager
        open={open}
        onOpenChange={setOpen}
        mode="browse"
        clientOptions={{
          baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
          apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
          apiKeyHeader: 'authorization',
        }}
        // folderPath="my-plugin/images/" // optional
        onSelect={async (img) => {
          // Persist original image URL in your DB
          await fetch('/api/media/save', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ url: img.url, key: img.key }),
          });
        }}
      />
    </div>
  );
}

Reusing across multiple Next.js projects

  • Install @thetechfossil/upfiles and @radix-ui/react-dialog in each project.
  • Set an environment variable like NEXT_PUBLIC_UPFILES_APP_URL to point all consumers to your main Upfiles app.
  • Each consumer stores its own copy of the API key in its DB, obtained via ConnectProjectDialog.

Troubleshooting

  • 401 Unauthorized when listing/creating projects: The projects and keys endpoints require a signed-in session on the main app domain. Open the dialog from a page that can send cookies to {baseUrl} or run from the same origin; otherwise configure auth/CORS accordingly.
  • CORS errors calling main app: Ensure the main app allows your consumer origin in CORS and, if you use cookies, sets the correct Access-Control-Allow-Credentials headers and SameSite attributes.
  • Uploads fail: Verify S3 bucket CORS and that POST /api/plugin/upload/presigned-url is accessible from the consumer app.
  • API key header: Default is Authorization: Bearer upk_.... If your server expects a different header, set apiKeyHeader: 'x-api-key' | 'x-up-api-key' in UpfilesClient options.