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

@codefusion-cc/attachments

v0.1.2

Published

Files people attach: paste, drop and picker in the page with a policy of types and sizes, uploads with progress and retry, and on the Worker each upload streamed into R2 once its bytes show a type the policy takes, served back so no browser runs anything

Readme

@codefusion-cc/attachments

Files people attach to something they send: a screenshot pasted into a form, a log dropped on it, a video picked from the phone. In the page it collects them from paste, drop and a file picker, checks each against the app's policy of types and sizes, and uploads it at once with progress, retry and a preview. On the Worker it streams each upload into R2 once its first bytes show a type the policy takes, and serves it back so that no browser runs anything in it. No dependencies; React is an optional peer for ./react.

npm install @codefusion-cc/attachments

The policy

One policy, shared by the page and the Worker, says which types an app takes and how large:

import { attachmentPolicy } from '@codefusion-cc/attachments'   // or '/browser'

export const POLICY = attachmentPolicy({ image: 20e6, video: 95e6, pdf: 20e6, text: 2e6 })
  • Groups: image (PNG, JPEG, GIF, WebP, AVIF, HEIC), video (MP4, QuickTime, WebM), pdf, text (plain, Markdown, CSV, JSON). A type named on its own ('image/gif': 5e6) wins over its group. A type not in the policy is refused.
  • Never SVG, HTML or anything else a browser could run: the package knows only types it can verify from their bytes.
  • acceptAttribute(policy) is the file input's accept; checkAttachment(policy, file) says before any upload whether a file is taken ({ ok: true, type }) or why not: unsupported-type, too-large (with maxBytes), empty.
  • declaredType(name, mime) is what a file claims to be: its MIME type with its other names folded (image/jpg), else its extension (.log and .md files often come without a type). The Worker decides from the bytes.

In the Worker

import { attachmentResponse, storeAttachment } from '@codefusion-cc/attachments'

// PUT /api/files/<uuid>: the page made the id, so a retry stores under the same key.
const result = await storeAttachment(request, { bucket: env.FILES, key: `files/${id}`, policy: POLICY, metadata: { by: member.id } })
if (!result.ok) return Response.json({ error: copy[result.refusal], maxBytes: result.maxBytes }, { status: result.status })
await db.insert(result.attachment)   // { key, type, size, name }

// GET /api/files/<uuid>
return attachmentResponse(env.FILES, `files/${id}`, request)

storeAttachment resolves, never throws, for anything an upload can bring:

| Refusal | Status | When | | --- | --- | --- | | length-required | 411 | No Content-Length: the size must be known before anything is stored. | | too-large | 413 | Over the limit of the type its bytes show, with maxBytes. A body over every limit is refused before a byte is read, and cancelled. | | unsupported-type | 415 | Its first bytes show no type the policy takes, and it isn't declared as text. A page renamed .png is not a PNG. | | not-text | 415 | Declared as text, but not UTF-8 or holding NUL bytes. | | empty | 400 | No bytes. | | incomplete | 400 | The upload broke off, or brought another number of bytes than it declared. Nothing is stored. |

  • The type stored is the one the bytes show, whatever the request said: a PNG sent as image/jpeg is stored as a PNG.
  • Binary files stream straight into R2 (through workerd's FixedLengthStream, as R2 needs a known length); text is read whole and stored only once every byte has been checked, so a text type's limit is also what one upload may hold in memory.
  • The name is the one uploadAttachment sent (the NAME_HEADER, x-attachment-name, %-encoded; one that isn't a %-encoding is ignored), or name when the app gives one. It is cleaned (cleanName: no folders, control characters or reordering marks, at most 200 characters with its extension kept) and kept in the object's metadata as name.
  • The key is the app's to choose, and must never come from a person: use an id the server checks (a UUID).

attachmentResponse(bucket, key, request, options) answers GET and HEAD:

  • 404 when it is not there, whatever the request's conditions.
  • Then the preconditions, in RFC 9110's order: 412 when If-Match names no current tag (a weak tag never matches, * always does) or, without If-Match, the file changed after If-Unmodified-Since; 304 when If-None-Match names the current tag (weak or strong, or *) or, without If-None-Match, the file has not changed since If-Modified-Since. HEAD answers them as GET does. A date that is not an HTTP-date is ignored.
  • Then, for GET, 206 for one byte range (bytes=10-19, bytes=90-, bytes=-5), so video seeks; 416 for a range outside the file; the whole file for several ranges or a malformed one, and for an If-Range that names another version (a date counts only when it is exactly Last-Modified, and from a second after it).
  • ETag, and Last-Modified from the object's uploaded. A bucket of your own that gives no uploaded sends none, and its date conditions are ignored.
  • One read of R2: get for GET (only the range's bytes for a range), head for HEAD. The conditions are decided from what it brings, not by R2, and the bytes of a 304, 412 or 416 are cancelled unsent. A failed If-Range reads the file again whole, and so do bytes other than the range asked (by the range R2 reports); a range R2 refuses with an error is answered from the object's head.
  • Content-Type is the stored one only when it is a type the package knows, else application/octet-stream; text is ; charset=utf-8.
  • Always X-Content-Type-Options: nosniff, Content-Security-Policy: default-src 'none'; …; sandbox, Cross-Origin-Resource-Policy: same-origin (cross-origin with public: true) and Referrer-Policy: no-referrer.
  • Content-Disposition names the file (an ASCII form and the real one, RFC 6266): inline for pictures, video and text, attachment otherwise. PDFs download by default, since browsers' PDF viewers don't open under sandbox; inline: (type) => … changes that.
  • cache: 'immutable' (default) keeps it a year, for keys whose bytes never change; 'revalidate' asks each time. Any other value rejects with a TypeError.
  • public: true is for files anyone may have (a shop's product photos): every cache may keep it (public, so a CDN and the Worker's Cache API too, which refuses a 206: put only a 200 there) and other sites may show it (Cross-Origin-Resource-Policy: cross-origin; no CORS, so their scripts cannot read it). By default only the browser keeps it (private) and only this site shows it.
  • Check who may read it before calling: the package serves whatever key it is given.

In the page

import { filesFromTransfer, carriesFiles, uploadAttachment, formatBytes } from '@codefusion-cc/attachments/browser'

const result = await uploadAttachment(`/api/files/${id}`, file, { signal, onProgress: (f) => bar.value = f })
if (!result.ok) show(result.reason)            // 'network' | 'aborted' | 'timeout'
else if (result.status !== 200) show(result.body)
  • uploadAttachment sends the file as the whole body (PUT by default, so a retry replaces) with XMLHttpRequest, for its progress, and its name for storeAttachment (a File's own, or name); it never rejects. No progress for idleTimeoutMs (60 s) ends it as timeout.
  • filesFromTransfer(clipboardData or dataTransfer) is what a paste or drop brought, reading items where a browser leaves files empty for a pasted screenshot. carriesFiles(dataTransfer) tells a drag of files from one of text, so a drop zone lets text through.

In React

import { useAttachments } from '@codefusion-cc/attachments/react'

const files = useAttachments({
  policy: POLICY,
  maxFiles: 10,
  upload: async (file, { id, signal, onProgress }) => {
    const r = await uploadAttachment(`/api/files/${id}`, file, { signal, onProgress })
    return r.ok && r.status === 200 ? { ok: true } : { ok: false, message: 'Upload failed' }
  },
  onRemove: (item) => void fetch(`/api/files/${item.id}`, { method: 'DELETE' }),
})

<form {...files.dropTarget} onPaste={files.onPaste} data-dragging={files.dragging}>
  <input type="file" multiple accept={acceptAttribute(POLICY)} onChange={(e) => { files.add(e.target.files ?? []); e.target.value = '' }} />
  {files.items.map((item) => <Chip key={item.id} item={item} onRemove={() => files.remove(item.id)} onRetry={() => files.retry(item.id)} />)}
  <button disabled={files.uploading}>Send</button>
</form>
// after sending: files.clear()
  • Each item: id, name, type, size, state (uploading, ready, failed with error, refused with refusal and maxBytes), progress (0 to 1, in whole percents) and preview (an object URL for PNG, JPEG, GIF, WebP and AVIF).
  • A file the policy refuses stays in the list, marked, until removed, so the person sees why; files past maxFiles are refused as too-many.
  • retry(id) uploads a failed file again under its id. Only the newest try of a file may change it: a late answer or progress from an earlier try is ignored.
  • remove(id) stops its upload, frees its preview, and calls onRemove once its upload had started (done, running, or failed: a failed upload may have reached the server). clear() forgets everything once the form is sent, without onRemove. Leaving the page stops every upload.
  • dropTarget counts nested drag events, reacts only to files, and sets dropEffect to copy; onPaste takes pasted files and leaves pasted text to the field.
  • The package writes no words: refusals are codes, and the app says them in its own language. formatBytes(n, locale) writes a size (3.2 MB, 1,5 MB in Polish).

In an app's tests

import { sampleBytes, sampleFile, SAMPLE_TYPES } from '@codefusion-cc/attachments/testing'

sampleFile('image/png')                                 // a real 1×1 PNG, named screenshot.png
sampleFile('image/png', { padTo: 20e6 + 1 })            // one byte over a 20 MB limit
sampleFile('image/png', { name: 'x.jpg', declare: 'image/jpeg' })

Each sample is the smallest file the package's sniffing takes as its type, so no test writes magic numbers of its own.