@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/fssNode.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.
metadataaccepts 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.devdoes 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 return429withRetry-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.
