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

@ultimat3/storage

v28.0.0

Published

Named disks over Bun.file and Bun.s3: safe keys, signed URLs, sniffed uploads

Readme

@ultimat3/storage 🗄️

Named disks. Call sites name a disk, never a driver.

import { defineStorage, disk, localDriver, s3Driver, scopedKey } from '@ultimat3/storage';

defineStorage({
  disks: {
    uploads: localDriver({ root: '.storage/uploads' }),
    media: s3Driver({ bucket: 'media', endpoint: process.env.S3_ENDPOINT, forcePathStyle: true }),
  },
  default: 'uploads', // omit and the first disk wins
  shared: { media: ['brand/'] }, // the only un-scoped keys /_storage and /media serve (27.0.0)
});

await disk('media').put(scopedKey(orgId, 'avatars', 'a.png'), bytes, { contentType: 'image/png' });

Swapping local for s3 in app.config.ts changes no call site. x dev needs no S3 server.

shared is per disk, whole segments ending in /, never inside org/ (X_CONFIG_INVALID at boot). A key outside every org/<id>/ prefix and every shared prefix is refused by the served read routes with X_STORAGE_KEY_UNSHARED (404); storage.isShared(disk, key) is the question.

Drivers

| Driver | Backing | For | Signed URLs | |---|---|---|---| | localDriver | Bun.file/Bun.write, one root dir | dev, tests, single-node | HMAC + dev route | | s3Driver | Bun.s3, plus core's signAwsRequest over fetch for the requests Bun has no option for | prod: any S3-compatible endpoint — AWS, R2, a self-hosted gateway | provider presign | | memoryStorageDriver | a Map in this process | a TEST's disk — never a deployment's: a restart is every object gone | HMAC, the local disk's own |

memoryStorageDriver() is what a suite holds instead of a temp directory or a hand-written fake: it answers every method localDriver does, refuses what it refuses (an unsafe key, a wrong checksum, a body past maxPutBytes, serverSideEncryption), and signs under the same rule — the published development key in development and test only. objects() hands back a COPY of every stored object's bytes by key, for the assertion a test makes about the bucket itself.

import { afterEach, beforeEach } from 'bun:test';
import { defineStorage, disk, memoryStorageDriver, resetStorage } from '@ultimat3/storage';

// A fresh driver per test: `resetStorage()` forgets the REGISTRY, never a driver's objects, so a
// driver held across tests carries every earlier test's writes.
let sessions = memoryStorageDriver();
beforeEach(() => {
  sessions = memoryStorageDriver();
  defineStorage({ disks: { sessions } });
});
afterEach(() => resetStorage());

await disk('sessions').put('org/o1/session.json', new TextEncoder().encode('{"sealed":"x1.…"}'));
const stored = [...sessions.objects().values()].map((bytes) => new TextDecoder().decode(bytes));

Server-side only, like both other drivers: nothing an island imports reaches it.

region is what requests are signed for; unset, that is auto. An endpoint that expects another region refuses the first write, list or delete with X_CONFIG_INVALID, and the fix names the S3_REGION to set.

copy(from, to) is on the contract so a promotion is not a download-and-reupload: promoteAttachment used to get() the whole object into the app and put() it back, a gigabyte through the pod to rename a 500MB attachment. localDriver does a real file copy; s3Driver hands the source S3File to write() — Bun exposes no CopyObject, so the bytes still cross the network, but never this process's heap. Both arguments go through assertSafeKey.

One S3 driver covers all three backends — the difference is endpoint + forcePathStyle. Credentials are env var NAMES (accessKeyIdEnv, default S3_ACCESS_KEY_ID), never literals: a key in app.config.ts is a key in git. Missing ones throw X_ENV_MISSING. Endpoint, region and session token resolve ONCE for both transports, as Bun reads them: the option, then S3_ENDPOINT/AWS_ENDPOINT, S3_REGION/AWS_REGION, S3_SESSION_TOKEN/AWS_SESSION_TOKEN (or the one sessionTokenEnv names), from s3Driver({ env }) or process.env. s3Driver({ fetch, clock }) inject the signed path's transport and its clock (tests; default the global fetch and systemClock); S3FetchLike is the shape. localDriver keeps content type, etag, cacheControl and metadata in a <root>/.meta/ sidecar so get()/list() round-trip everything put() was handed; sidecars never appear in list(). s3Driver STORES both — a put() carrying cacheControl, metadata, retention or legalHold goes past the Bun client as one SigV4-signed PUT (x-amz-meta-*, Cache-Control, Content-MD5), against the same endpoint, region and credentials — but its get()/stat() read through Bun's HEAD, which exposes neither, so there they are absent rather than invented. A plain put() stays on the Bun client. A metadata name must be a header token and every value printable ASCII (X_INVARIANT otherwise): S3 carries them as headers.

The local disk writes an object by rename, in an order no crash can make lie. An object there is two files, so a put() or copy() stages bytes, sidecar and a pending marker (the new etag) under <root>/.meta/.tmp/ and then: renames the marker to .meta/<key>.json.pending, renames the sidecar, renames the bytes, removes the marker. What a reader sees if the process dies:

| Died before | Fresh key | Overwrite | |---|---|---| | the marker or the sidecar rename | absent | the previous object, whole | | the bytes rename | absent (a sidecar alone is no object) | the previous BYTES; get/stat answer application/octet-stream and the bytes' own etag, list no type and etag: '' — never the new type | | clearing the marker | the new object to get/stat (re-checked against the bytes); list reports it untyped until the next put | same |

No row serves a content type or an etag the bytes were not checked against. A marker that cannot be cleared after the renames landed is not a failed put — the object is committed and the last row is what it leaves. stat(), list() and a copy's source read queue behind the key's writer exactly as get() does. Writers and get() of ONE key are also serialised inside a process, so two concurrent puts cannot interleave; across processes the marker is what covers the window. stream() hands back bytes with no metadata and checks nothing. The layout is otherwise unchanged — an existing root needs no migration.

A refused write is coded. EACCES, ENOSPC, EROFS or a root that is a file on the local disk, and a provider's refused PUT on s3, are X_STORAGE_PUT_FAILED from put() and copy(), with the errno or provider code in the cause. put() options are held to their types first (assertPutOptions: string contentType/cacheControl, a plain object of strings for metadata, X_INVARIANT otherwise) on all three drivers. A refused READ is X_STORAGE_READ_FAILED the same way. No driver method leaves with a bare throw for a wrong-typed argument either: a key that is not a string is X_STORAGE_PATH_UNSAFE, a body that is not bytes, a Blob or a stream and a non-string list() prefix or cursor are X_INVARIANT (driver-read-failed.test.ts sweeps every method on every disk). The one thing no code can cover is a stream() that fails after it was handed back: that is the stream's own error.

A key cannot be another key's path on the local disk — or on the memory disk standing in for it. A POSIX path is a file or a directory: put('a') then put('a/b') — or the reverse, a copy() onto either, or a beside a.json/b, which collide in the sidecar tree — is X_STORAGE_KEY_CONFLICT, refused before a byte moves, where it used to be a bare ENOTDIR / EISDIR. Its fix names the disk as defineStorage registered it, not the driver kind. memoryStorageDriver refuses the same keys with the same blocking (a suite on it stands in for x dev's disk); s3Driver holds both. driver-local-memory-parity.test.ts holds local and memory to one answer, driver-contract.test.ts pins the s3 divergence. delete() of a key that is only another key's directory (delete('c') beside c/d) is deleting an absent key: not an error, and nothing beneath it is touched.

stat(key) answers what a read would — size, type, etag, age — without the bytes, and undefined for nothing there. It is a required StorageDriver method: promoteAttachment measures with it, and a driver that cannot measure does not compile.

lastModified is optional on a listing entry and on a StorageObject: absent when the provider reported none, never epoch 0 — which sweepOrphans read as "older than any window" and deleted. A reader that needs an age handles undefined; the sweep spares it. On the local and memory disks it is the write's instant on the disk's injected clock: what put()/copy() returned is what every later stat/get/list reports (the local disk records it in the sidecar; an object written before that, or with no trusted sidecar, reports the file's mtime).

StorageListEntry.contentType is optional; StorageObject.contentType is not. S3's ListObjectsV2 returns no Content-Type, so a listed s3 object simply has none — reading the real value would cost one HeadObject per row, which is what list() exists to avoid. It used to report application/octet-stream, indistinguishable from an object that really is one, while the local driver read the truth out of its sidecar: a caller filtering a listing by content type got everything on local and nothing on s3. get() always answers a full StorageObject.

The etag follows the same rule: a listed object with no sidecar reports etag: '', because answering otherwise means reading and hashing the whole object — which is what the local list() used to do, once per sidecar-less row, sequentially. get() hashes out of bytes it already holds.

A listing returns every key the key rules allow, dot-prefixed segments included (.hidden.txt, org/o1/pending/.x.png) — the local glob was blind to them until 2026-08, so a disk answered exists() true and list() nothing for the same object. limit is a positive integer or a refusal (X_INVARIANT, at the ListOptions seam): limit: 0 used to read back as a complete, empty page on the local disk and as maxKeys: 0 on s3.

A listing is core's ONE Page — Page<StorageListEntry>, the shape an entity findMany and a query .page() answer: { rows, nextCursor, hasMore }, nextCursor a string exactly when hasMore is true. The cursor exists only when another object does — the local and memory disks look one key past the page, s3 reads IsTruncated and takes NextContinuationToken — so a full last page answers hasMore: false, never a cursor to an empty page. cursor: null is the first page, so a walk threads nextCursor straight back:

import type { Page } from '@ultimat3/core';
import { disk, type StorageListEntry } from '@ultimat3/storage';

const keys: string[] = [];
let cursor: string | null = null;
do {
  const page: Page<StorageListEntry> = await disk('uploads').list({ prefix: 'org/o1/', cursor });
  keys.push(...page.rows.map((object) => object.key));
  cursor = page.nextCursor;
} while (cursor !== null);

Before 25.0.0 it was ListPage { objects, truncated, cursor? }, its own shape beside core's.

put() is for objects that fit in memory

put() buffers the whole body — size and checksum have to be known before the object exists — so every disk enforces a ceiling, maxPutBytes, defaulting to DEFAULT_MAX_UPLOAD_BYTES (10MB). Past it is X_STORAGE_TOO_LARGE, raised before the bytes are resident: a Uint8Array and a Blob already know their length, and a ReadableStream is cancelled the moment the running total crosses the line. Without it a route piping a 4GB request body into disk.put(key, req.body) grew the heap by 4GB and the kubelet killed the pod.

User uploads never go through put(). They go direct to the disk through grantUpload, which is the architecture and not an optimisation — see the round trip below. Raise maxPutBytes only for a disk that really does write large objects server-side, and remember S3 caps a single PUT at 5GB whatever you set.

get() buffers too, so it has the same ceiling: maxGetBytes, on all three drivers, defaulting to that disk's maxPutBytes. An object past it is X_STORAGE_TOO_LARGE — decided on the disk's own size (a HEAD on s3) before a byte is read — and is read with stream(). It exists because an object's size is not the server's to choose: a presigned PUT lands in a bucket unmeasured, so a later get() of it was heap growth the uploader picked.

Write-once objects (S3 Object Lock)

import { systemClock } from '@ultimat3/core';
import { disk, isLocked } from '@ultimat3/storage';

const key = 'org/o1/ledger.csv';
const bytes = new TextEncoder().encode('a,b\n');
const now = systemClock.now();
await disk('ledger').put(key, bytes, {
  retention: { mode: 'COMPLIANCE', retainUntil: new Date(now.getTime() + 7 * 86_400_000) },
  legalHold: true,
});
const lock = await disk('ledger').retentionOf?.(key); // { retention: { mode, retainUntil }, legalHold: true }
isLocked(lock, now); // a hold, or a retention still ahead

| | s3 | local, memory | |---|---|---| | put({ retention, legalHold }) | x-amz-object-lock-mode / -retain-until-date (ISO) / -legal-hold (ON/OFF) on the signed PUT — the bucket must be CREATED with Object Lock | recorded in the sidecar (local) or beside the bytes (memory) | | retentionOf(key) | signed GET ?retention + ?legal-hold; a bucket without Object Lock, or an object with none, is { legalHold: false } | from what put() recorded | | delete / overwrite of a locked object | sent: S3 locks a VERSION, so a DELETE writes a delete marker and a PUT a newer version — the locked version stays | refused — X_STORAGE_OBJECT_LOCKED (ObjectLockedError, HTTP 409; .lock carries the lock) — until it lapses: one version per key, so refusing is the only way to keep the bytes |

retainUntil must be a valid instant in the future and mode GOVERNANCE or COMPLIANCE (X_INVARIANT before a byte moves). COMPLIANCE cannot be shortened by anyone; extending a retention, releasing a hold or bypassing GOVERNANCE is out of band: aws s3api put-object-retention / put-object-legal-hold. A copy() does not carry the source's lock — on S3 it is per version and set by the request. The local refusal is in-process (keyedQueue), like every other local write rule.

Server-side encryption, storage classes, lifecycle

PutOptions.serverSideEncryption exists so the gap is visible at the type level, and every shipped driver refuses it (X_NOT_IMPLEMENTED) with the out-of-band command in the fix. Bun.S3Client exposes acl, storageClass and type and nothing for x-amz-server-side-encryption*; a local disk writes plain files. A typed refusal an engineer meets at the call site beats a silent absence discovered during a security review.

Encryption at rest is therefore a bucket default, and so are lifecycle rules, storage classes and cross-region replication: all four are bucket-side configuration and belong to terraform, not to the framework (axiom 7 — zero platform primitives). Ultimate half-building any of them would be a second place to look for the same setting.

Keys

assertSafeKey() runs on every key before it reaches a driver. Rejected: .. segments, absolute keys, backslashes, NUL/control bytes, percent-encoded separators (%2e, %2f), empty segments, over 1024 chars, and a first segment of .meta (META_DIR) — the local driver's sidecar namespace, reserved on every driver so one key rule covers disk and S3 alike. Without it, put('.meta/a/b.json', …) overwrote the recorded content type of a/b and a route serving that object answered attacker HTML from the app's own origin. No sanitising — a key that needed fixing was built wrong. scopedKey('org-1', 'avatars', 'a.png') is org/org-1/avatars/a.png; guard every client-supplied key with isWithinOrg(key, ctx.actor.orgId) — a predicate that never throws: an org id that cannot be one (empty, or carrying a separator) contains nothing, so an actor with no org claim is false and not an X_STORAGE_PATH_UNSAFE blaming the key. A surface that serves objects pairs it with isTenantScoped(key): only a key already inside org/ is another tenant's to refuse, so disk().put('brand/logo.png', …) stays reachable while org/org-2/… never is. accept.ts asks the pair too. isTenantScoped folds case (Org/, ORG/) and isWithinOrg does not, so a case-variant prefix — one directory, not two, on APFS or NTFS — is refused rather than matched.

Signed URLs

The HMAC covers the constraints, not just the key — v2 \n basePath \n METHOD \n key \n expiresAt \n maxBytes \n contentType. A client that edits ?x-max= invalidates the signature — it cannot widen what it was granted.

basePath is the disk: the PATH of the base the URL was minted under (/_storage/<registered name>). Every local disk signs with the one STORAGE_SIGNING_SECRET, so without it a URL for uploads/<key> verified on private/<key> with only the path segment edited. The origin is not signed — the route that verifies is handed a path — and an absolute baseUrl (https://cdn.example.com/_storage/local) is compared by its pathname (signedUrlBasePath), where it used to make every URL minted under it malformed. canonicalRequest and signConstraints take the base path as their last argument.

The HMAC key is signingSecret, else STORAGE_SIGNING_SECRET, else the shipped DEV_SIGNING_SECRET — and only in development or test, read with a fallback of production: a process that names no environment at all (ULTIMATE_ENV and NODE_ENV both unset) is refused like a production one, As of 2026-09-23. Anywhere else localDriver refuses to construct (X_ENV_MISSING) unless one of those two is set to something that is not the shipped literal: the dev literal is published in this repo, so signing with it lets anyone mint a PUT for any key with a maxBytes and contentType of their choosing, which acceptSignedUpload then trusts over the app's own uploadPolicy. Setting STORAGE_SIGNING_SECRET=$DEV_SIGNING_SECRET, or pasting the literal into signingSecret, is refused exactly as an unset variable is. usesDevStorageSecret() is the x doctor probe for it, the twin of core's usesDevCursorSecret().

Both read one table, and env is how a caller says which. localDriver({ root, env }) reads the secret, the environment test and the environment the refusal names off that table; usesDevStorageSecret({ env }) reads it off the same one. Both default to process.env, so a bare call is unchanged — pass env wherever the boot's environment is not the process's (serveApp({ env }), a test fixture), or the guard answers about one process and the disk signs according to another. Verification is constant-time, checks the signature before the expiry (a forged URL never learns it was merely late), takes a Clock so tests freeze time, and returns { ok: false, reason } rather than throwing — malformed | unsafe-key | signature-mismatch | expired. S3 presign covers method, expiry and content type but not maxBytes: S3 has no header for it, so a bucket-backed disk's ceiling is a bucket rule or a post-upload object.size check, never the signature. s3Driver does not refuse maxBytes either — grantUpload supplies it on every grant, so refusing would break every s3 upload an app mints.

Uploads sniff the content type

Content-Type is attacker-controlled. A .png that is really an HTML document is stored XSS the moment a surface serves it back with the declared type. validateUpload() reads the magic bytes (PNG, JPEG, GIF, WebP, PDF, ZIP/OOXML, SVG, the ISO-BMFF family, HTML, plain text) and rejects any payload whose bytes contradict the declaration. Checks run cheapest-first: key → size → allowlist → sniff → checksum.

Every ISO base media file opens with the same ftyp box, so the box alone says "container" and the major brand after it decides (iso-bmff.ts): avif/avis → image/avif, heic/heix… → image/heic, qt → video/quicktime, M4A /M4B → audio/mp4, 3gp* → video/3gpp, anything else → video/mp4. Two brands name only a container and stand in as application/zip does for OOXML: video/mp4 for a declared audio/mp4 (an audio-only MP4 carries isom), and image/heif (mif1) for image/heic and image/avif. audio/x-m4a and video/x-m4v — what a browser reports for a picked file — normalise to audio/mp4 and video/mp4, and uploadPolicy({ allowedContentTypes }) normalises its list the same way, so an alias in an allowlist names the type it means.

validateUpload({ key, declaredContentType, bytes }, uploadPolicy({ maxBytes: 5e6 }))

The direct-upload round trip

Three calls, one per hop. The client never names the key — it asks for a grant and is told one — so it cannot aim an upload at another tenant, at a row it does not own, or at a size the policy never allowed.

// 1. server, inside an action's handle: mint the grant
const grant = await grantUpload({
  disk: disk('uploads'),
  orgId: ctx.actor.orgId,            // the ACTOR's org, never one read off the request
  request: { filename, contentType, size },
  policy: uploadPolicy({ maxBytes: 5e6 }),
  // target: { entity: 'post', id, field: 'cover' } — omit it and the key lands under `pending/`
});

// 2. browser: PUT at it, with real progress, and get the key back
const { key } = await uploadFile({ file, grant: (request) => api.requestUpload(request), onProgress });

// 3. server, in the route mounted at `/_storage`: take it back, or refuse
const object = await acceptSignedUpload({
  url: request.url,                  // baseUrl defaults to `disk.signedUrlBase` — the very base
  secret,                            // the driver signed under, `/_storage/<disk name>`. Pass one
  disk: disk('uploads'),             // only for a route mounted somewhere else.
  orgId: ctx.actor.orgId,
  bytes, declaredContentType: request.headers.get('content-type') ?? undefined,
  policy: uploadPolicy({ maxBytes: 5e6 }),
});

acceptSignedUpload refuses on any of: a signature that does not verify, an expired grant, a PUT grant replayed as a GET, a key outside the actor's org, more bytes than the signature granted, a Content-Type the signature does not cover, or magic bytes that contradict it. readSignedObject is the GET half and applies the same verification and the same org check — which is the isTenantScoped/isWithinOrg pair, so an app's own un-scoped brand/logo.png is readable through a URL it signed and org/org-2/… still is not. uploadPolicy({ requireChecksum: true }) needs the request's declared hash, which travels as checksum exactly as the content type travels as declaredContentType: a header the route reads and hands over, hashed again here and refused on any disagreement. Neither owns a Request, a Response or a status number — mounting is the host's job, and @ultimat3/http is the only layer that turns an X_* code into a status.

Step 3 ships. As of 2026-09-24 @ultimat3/cli mounts PUT /_storage/:disk/*key beside the GET in x dev and runRole (#523), so an app writes steps 1 and 2 only. The route calls signedUploadConstraints({ url, disk, orgId }) first. That is the verified PUT constraints with no bytes, and the signed maxBytes caps the body read. Then it calls acceptSignedUpload with a policy built from the grant's own signed type and ceiling. The routes serve definedStorage(), the process's one registry, which is the app's defineStorage when it declared one (#524).

Attachments and orphans

An upload happens before the row it belongs to exists, so it lands at org/<orgId>/pending/<uploadId><ext> and is promoted once there is an id:

| Call | Key | |---|---| | pendingKey(orgId, uploadName(id, filename)) | org/o1/pending/u-1.png | | quarantineKey(orgId, name) | org/o1/pending/quarantine/u-1.png | | attachmentKey(orgId, { entity, id, field }, name) | org/o1/post/p-1/cover/u-1.png | | promoteAttachment({ disk, key, orgId, target, policy }) | stat, copy, then delete — never the reverse. policy is required: the object is measured here (stat().size over policy.maxBytes is X_STORAGE_TOO_LARGE, and it stays under pending/ for the sweep), because an s3 presign bounds no size. A call after the source is gone, with the attached key present, is X_STORAGE_ALREADY_PROMOTED (409, meta.attachedKey): an earlier call moved the bytes, so this one answers the row that holds them and never deletes that key. The key must be a pending one (isPendingKey): an attached key a client sent back is another row's file, and moving it would delete the victim's copy (X_STORAGE_NOT_PENDING) | | releaseQuarantine({ disk, key, orgId }) | quarantine → pending; returns the released key | | sweepOrphans({ disk, orgId, olderThanMs }) | { deleted, failed } for stale pending/ keys. olderThanMs is a whole number of 0 or more, refused otherwise (X_INVARIANT) — NaN read as "everything is old enough" and deleted an upload made a moment ago |

The filename contributes nothing but its extension, and only if it matches .[a-z0-9]{1,12}. sweepOrphans can only reach the pending/ prefix of one org — a sweep that could touch an attached key is a job that deletes production data the first time an app forgets to promote.

Two lists, because one cannot be wrong. sweepOrphans answers { deleted, failed }: a refused delete goes in failed with the disk's own words and the sweep keeps going. A single array of "deleted" keys put every refusal in it, so an erasure sweep over a bucket whose policy had lost s3:DeleteObject reported 200 objects gone that were all still there.

Quarantine is the mechanism; the scanner is your app's

Magic-byte sniffing closes stored XSS. It is not malware scanning, and application/zip is accepted on purpose as the OOXML container, so a macro-laden .docx passes validateUpload exactly as a clean one does. A scanner is a business decision with a vendor, a licence and a latency budget (axiom 8), so what ships is the place to put one — one more segment in a convention that already existed:

const grant = await grantUpload({ disk, orgId, request, quarantine: true });
// → org/<orgId>/pending/quarantine/<uploadId><ext>

// promoteAttachment on that key throws X_STORAGE_QUARANTINED. Your scan job decides:
const released = await releaseQuarantine({ disk, key, orgId });   // clean
await promoteAttachment({ disk, key: released, orgId, target });

Inside pending/ deliberately: an upload nobody ever scanned is still an orphan, so sweepOrphans collects it with no second prefix to walk.

Errors

| Code | Fires when | |---|---| | X_STORAGE_DISK_UNKNOWN | disk(name) is not in storage.disks; cause lists the real ones | | X_STORAGE_NOT_FOUND | get/stream on a key that does not exist | | X_STORAGE_PATH_UNSAFE | traversal, absolute key, backslash, NUL, %2e, empty segment | | X_STORAGE_TOO_LARGE | payload over the policy maxBytes (at validation, or measured by promoteAttachment), or over a disk's maxPutBytes / maxGetBytes | | X_STORAGE_TYPE_REJECTED | declared type off the allowlist, or contradicted by magic bytes | | X_STORAGE_CHECKSUM_MISMATCH | supplied base64 SHA-256 does not describe the bytes | | X_STORAGE_URL_INVALID | a signed request that does not match what was signed — edited constraint, wrong base, wrong method, contradicting Content-Type | | X_STORAGE_URL_EXPIRED | the grant's window closed; the signature was fine | | X_STORAGE_ORG_MISMATCH | the key is well-formed and unforged, and belongs to another org | | X_STORAGE_UPLOAD_FAILED | client half: the presigned PUT answered non-2xx or never landed | | X_STORAGE_DELETE_FAILED | the disk REFUSED a delete — denied s3:DeleteObject, a throttle, an expired credential, a read-only mount. An absent key is still not an error | | X_STORAGE_LIST_FAILED | the disk REFUSED a listing — denied s3:ListBucket, a throttle, an unreadable root. An empty disk is still not an error | | X_STORAGE_QUARANTINED | promoteAttachment on a key nothing has released from pending/quarantine/ | | X_STORAGE_NOT_PENDING | promoteAttachment on a key outside the org's pending/ prefix — most often another row's attached key | | X_STORAGE_ALREADY_PROMOTED | promoteAttachment on a pending key an earlier call already moved (a retried or doubled confirm); meta.attachedKey names where the bytes are | | X_STORAGE_PUT_FAILED | the disk REFUSED a put()/copy() — EACCES, ENOSPC, EROFS, a provider's refused PUT | | X_STORAGE_OBJECT_LOCKED | local, memory: a delete, overwrite or copy onto a key under retention or a legal hold; the cause names the key, the mode and retainUntil or the hold, and the bytes are unchanged. Never raised by s3, where the provider keeps the locked version | | X_STORAGE_READ_FAILED | the disk REFUSED a get/stat/exists/stream or the read half of a copy — a denied s3:GetObject, a throttle, an unreadable file. An absent object is still X_STORAGE_NOT_FOUND, including one deleted between the existence check and the read | | X_STORAGE_KEY_CONFLICT | local and memory disks (never s3): the key's path is another key's directory, or the reverse (a and a/b) | | X_NOT_IMPLEMENTED | serverSideEncryption on any driver | | X_ENV_MISSING | core's: S3 credential env vars, or a localDriver built outside development where neither signingSecret nor STORAGE_SIGNING_SECRET holds a secret other than the published DEV_SIGNING_SECRET | | X_IMAGE_UNSUPPORTED | core's: an avif encode, a source no built-in decoder reads, or a variantKey format no variant can carry | | X_IMAGE_DECODE_FAILED | core's: truncated or corrupt image bytes |

Error classes

Every error class src/index.ts exports, for instanceof inside one process. Across a wire or a job boundary the class is gone and the code is what survives — match on that.

| Class | Code | Declared in | |---|---|---| | StorageError | any StorageErrorCode — STORAGE_ERROR_CODES | src/errors.ts |

Images

variantKey(), srcsetDescriptors(), fitDimensions() are pure — @ultimat3/seo builds srcset from them without decoding a byte.

A variant key is the WHOLE source key plus the transform: photos/hero.png at width 640 is photos/[email protected]. The source extension stays in, so hero.png and hero.jpg are two cache identities — it was cut, and both minted photos/[email protected]. width and height are whole numbers of at least 1 on variantKey and fitDimensions (X_INVARIANT otherwise — no wNaN, no w1.5). fitDimensions never answers a zero edge (1000×1 at width 100 is 100×1) and never upscales except under fit: 'cover', which is the exact box asked for.

VARIANT_FORMATS / VariantFormat are what a variant KEY can carry, As of 2026-08 — avif, webp, jpeg, png — and this package exports no IMAGE_FORMATS and no ImageFormat. Those two names are @ultimat3/core's, over the six formats it can PROBE (png, jpeg, webp, avif, gif, svg). Until 9.0.0 both packages exported both names over different sets, so a caller narrowing on storage's held a type saying gif and svg could not occur and a probeImage() value that was one. isVariantFormat(format) takes a string, so a probed format narrows through it with no cast, and variantKey() refuses anything else with core's X_IMAGE_UNSUPPORTED instead of minting photos/[email protected].

transformImage() and blurPlaceholder() are real, over @ultimat3/core's pipeline, which is Bun.Image — no sharp, no dependency. It encodes png, jpeg and webp — so the default format and the default .webp key extension finally agree, and a srcset entry can be served rather than only named. avif remains key and srcset math: it needs an OS codec the portable backend never uses, so asking for its bytes rejects with core's X_IMAGE_UNSUPPORTED naming the three that work; produce it through a CDN or a custom ImageTransformDriver. png and webp are the outputs that keep alpha. The encoded size is always exactly fitDimensions(), so the width/height @ultimat3/seo already wrote into the tag match the bytes — contain fits inside the box, it does not letterbox to it. blurPlaceholder() returns a ThumbHash PNG data: URI, at most 32px on its long edge. bun test from packages/storage.