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

@ircg/fss

v1.28.2

Published

File Storage Service SDK for IRCG

Downloads

526

Readme

@ircg/fss

TypeScript/JavaScript SDK for IRCG File Storage Service (FSS). It uploads, lists, retrieves, streams, inspects, changes the visibility of, and schedules the deletion of service files.

See ARCHITECTURE.md for the storage design, public and private delivery paths, retention, and billing. Update it whenever service behavior changes.

Installation

pnpm add @ircg/fss

Node.js usage

import { openAsBlob } from 'node:fs'
import { FSSClient } from '@ircg/fss'

const fss = new FSSClient({
	apiKey: process.env.IRCG_FSS_API_KEY!,
	// baseUrl defaults to https://ircg.dev
})

const file = await openAsBlob('./invoice.pdf', { type: 'application/pdf' })

const uploaded = await fss.upload({
	file,
	fileName: 'invoice.pdf',
	// Reuse this value if your job retries the same logical upload.
	idempotencyKey: 'invoice-acme-2026-08',
	visibility: 'private',
	fields: ['fileId', 'originalName', 'sizeBytes', 'visibility'],
})

if (uploaded.error) {
	console.error(uploaded.error.status, uploaded.error.message)
	process.exitCode = 1
} else {
	console.log(uploaded.file.fileId)
}

Browser usage

FSS does not enable CORS for direct browser integrations. Send the file to an endpoint in your own backend and use FSSClient there so the API key remains on the server. Never embed an FSS API key in public JavaScript.

Dry run

Dry-run mode returns typed synthetic responses without making HTTP requests or consuming credits:

const fss = new FSSClient({ apiKey: 'unused', dryRun: true })
const result = await fss.upload({ file, fileName: 'invoice.pdf', fields: ['fileId', 'sizeBytes'] })

if (!result.error) console.log(result.file.fileId) // "dry-run-file"

All remote operations are simulated locally, including Unsafe variants, multipart uploads, visibility changes, signatures, downloads, and header requests. Listings return an empty array. The client retains each multipart upload's name, type, size, metadata, and visibility in memory until the client instance ends; it neither retains the content nor persists this state. Synthetic downloads support simple and suffix ranges and reject multiple, invalid, or unsatisfiable ranges with status 416, just like the service. Responses include synthetic Content-Disposition and Cache-Control: no-store headers. This mode does not validate authentication, general resource existence, or every server-side limit.

List, retrieve, change visibility, sign, download, and delete

The getAll() and getById() metadata reads require an explicit fields array. Mutations such as updateVisibility() return their fixed response shape and do not accept fields.

const page = await fss.getAll({
	amount: 50,
	fields: ['fileId', 'originalName', 'sizeBytes', 'createdAt'],
})
if (!page.error) {
	for (const file of page.files) console.log(file.originalName, file.sizeBytes)
	// `cursor` is opaque; pass it unchanged to the next call.
	if (page.cursor) console.log('next cursor:', page.cursor)
}

const selected = await fss.getById({
	fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
	fields: ['fileId', 'originalName', 'visibility', 'publicUrl'],
})
if (!selected.error) console.log(selected.file)

const published = await fss.updateVisibility({
	fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
	visibility: 'public',
})
if (!published.error) console.log(published.file.publicUrl)

download() can be returned directly from a server route without buffering the file. For example, in SvelteKit:

export const GET = async () => {
	// download() first requests a signed URL, then opens the stream from media.ircg.dev.
	const download = await fss.download({ fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851' })
	if (download.error) return new Response(download.error.message, { status: download.error.status })

	// Preserve the original stream, status, and headers; avoid arrayBuffer() for large files.
	return download.response
}
const navigation = await fss.getUrl({ fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851' })
if (!navigation.error) console.log(navigation.signedUrl, navigation.exp)

// Without Range, Content-Length is the complete file size.
const head = await fss.head({ fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851' })
if (!head.error) console.log(head.response.headers.get('Content-Length'))

const deleted = await fss.delete({ fileId: 'd290f1ee-6c54-4b01-90e6-d701748f0851' })
if (deleted.error) console.error(deleted.error.message)

Methods without an Unsafe suffix return { error } instead of throwing. Most remote operations also have an Unsafe variant for exception-based control flow. download() and head() make two requests: they create a signed URL and then request the content. There is no separate streaming endpoint authenticated by API key.

Changing a file to public returns its publicUrl; changing it back to private removes public access. Visibility affects reads only and never enables anonymous writes.

Multipart uploads

For files larger than 95 MB, start an upload, send consecutive 95 MB parts (the final part may be smaller), and complete it with the returned ETags. Retain uploadId and the ETags in durable application storage so an upload can be resumed. At most four parts may be in flight concurrently, and authorization expires after 24 hours. When a part upload receives a transient 404 immediately after the upload starts, the SDK retries it up to twice with backoff, for at most three total attempts.

const started = await fss.startMultipartUpload({
	contentType: file.type,
	fileName: file.name,
	sizeBytes: file.size,
})
if (started.error) throw new Error(started.error.message)

const upload = started.upload
const firstPart = file.slice(0, upload.partSizeBytes)
const part = await fss.uploadMultipartPart({ body: firstPart, partNumber: 1, uploadId: upload.uploadId })
if (part.error) throw new Error(part.error.message)

const completed = await fss.completeMultipartUpload({
	fields: ['fileId'],
	parts: [part.part],
	uploadId: upload.uploadId,
})
if (completed.error) throw new Error(completed.error.message)
console.log(completed.file.fileId)

Call abortMultipartUpload({ uploadId }) when the user cancels. It is the only remote mutation without an Unsafe variant; it always reports failure through its safe result. Expired uploads are aborted automatically, with storage lifecycle cleanup as a fallback.

Limits and metadata

  • Single-part uploads support up to 95 MB. Multipart uploads use parts of up to 95 MB. The default maximum is 50 GB per file; an organization-specific limit of up to 950 GB can be approved with the current 10,000-part transport.
  • metadata accepts up to 50 text pairs. Each key may contain up to 128 characters, each value up to 2,048 characters, and the complete set up to 8 KiB.
  • To retry a single-part upload, retain and reuse its idempotencyKey (8–128 visible ASCII characters). A replay returns the original file without duplicating the object, activity, or credits.
  • MIME types are initially accepted without a closed allowlist. The original file name is not part of the storage key.
  • Listings use cursor pagination and do not expose an exact total.

FSS has two independent rate-limit layers:

  • Authenticated API operations default to 120 requests per 60 seconds and 3,000 per 3,600 seconds per API key. Approved organization-specific API limits may differ.
  • Delivery from media.ircg.dev does not use the API key quota. Its defensive limits are 300 requests per 60 seconds per organization and client address, 600 per file, and 1,500 per organization. These approximate limits apply per delivery location and may temporarily return 429 with Retry-After: 60.

Consequently, sign() uses the API limit, while each subsequent GET or HEAD made with the resulting URL uses the independent delivery limits.

Privacy, deletion, and billing

Files are private by default. An authorized API key or session creates a reusable one-hour URL, and content is delivered exclusively from https://media.ircg.dev/f/<fileId>. Public files use the same unsigned path. Use publicUrl, sign(), or getUrl() instead of constructing URLs manually.

PDFs and supported raster image formats are delivered inline. HTML, SVG, and any type not explicitly supported inline are delivered as an attachment. Storage remains private, and changing visibility never enables anonymous writes.

delete() hides the file immediately and does not refund credits from the current cycle. The object is retained for 30 days for auditing and does not consume storage credits during that retention period.

Uploads do not consume request credits, but they record storage for the current cycle. Storage costs 1 credit per whole MB in each cycle in which the file exists. Every content download (GET) costs 1 credit regardless of file size; HEAD does not consume download credits.