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

@michaelthielemann/kestrel-media-default

v5.8.1

Published

Media uploads: files in blobstore@1, metadata in persistence@1.

Downloads

1,170

Readme

media/default

Uploads: bytes go to blobstore@1 under <prefix><folder>/<filename>, metadata (filename, folder, contentType, size, key, checksum, status, createdAt, provenance) into the persistence collection media_items. folder is a path-like label (2026/press), tracked in media_folders, with folder-tree operations (create/rename/remove) that move blobs along. alt, title, description are per-locale texts (plain text: max 2000 characters, no control characters except tab and newline, VALIDATION otherwise – they are not HTML and are never sanitized, which would rewrite legitimate text such as 5 < 6); width/height are read from the file header of PNG/JPEG/GIF/WebP uploads (no image library). provenance records who made the file (origin: human | ai | mixed | unknown, plus tool/model/at) and is exposed via X-Content-Provenance on download for every origin but human — the anchor for labelling obligations such as EU AI Act Art. 50 (not legal advice; labelling itself is the frontend's job). An upload without provenance is recorded as { origin: "unknown" }: a missing declaration is not evidence that a human made the file, and the caller (the admin UI, an import script) is the one that knows.

Three key families share the blobstore, with three owners:

| Key | Owner | What it is | |---|---|---| | media/<folder>/<file> | this module (prefix configurable) | the original bytes, one blob per media_items row | | media-variants/<id>/<size>.webp | images/default | the generated variants of a medium; this module ignores them in media.reconcile | | site/media/<folder>/<file>.<size>.webp | delivery-static | the copies made at publish time (prefix + media.target there), referenced by the rendered HTML for static delivery |

The row is the truth, the blobstore only the storage. media_items.key is unique: a blob key belongs to exactly one row, so two concurrent uploads of the same name in the same folder end as one item and one CONFLICT, never as two rows over one blob. An upload writes the row first (status: "uploading", checksum = sha256 of the bytes as hex), then the blob, then status: "ready"; a failing blob write leaves status: "failed" and returns the blobstore's failure, so the pipeline still fails. Only ready items are returned by media.get, media.list and media.download (NOT_FOUND / filtered out otherwise), and a failed row is replaced when the same name is uploaded again, so a broken attempt never blocks a filename. checksum and status are additive: rows written before them read as checksum: null / status: "ready". Deleting removes the row first and the blob after — but only once no other row points at that key — a failing blob delete is logged, not fatal, and media.reconcile finds what is left over. A rename moves the blob first and moves it back if the row update fails.

Uploading is idempotent: same folder, same filename and the same bytes return the item that is already there — no second row, no second blob write, no second variant folder — and media.upload then ends the pipeline itself (ctx.done, 200), so a following events.emit:media.uploaded does not announce an upload that stored nothing. The same name with different bytes stays a CONFLICT (409). For several files in one request, ids lists only what was newly stored; when nothing was, the step ends the pipeline the same way.

Why: a plain blobstore has no metadata, folders, locale texts or provenance tracking; this module adds the bookkeeping other modules (delivery-static, references-default) rely on, without depending on an image library.

Config: allowedTypes (image/*, application/pdf, *), deniedTypes (default image/svg+xml, text/html, application/xhtml+xml — they can carry scripts), maxBytes, prefix (default media/, must end with /), locales/defaultLocale.

Steps: media.upload (processes every file in ctx.files, in order, with the same checks; exactly one file returns the item unchanged, two or more return { items, errors, ids } with a per-file { filename, status, code, message } entry in errors and only the newly stored items in ids — a rejected or conflicting file doesn't stop the others, partial success is a 200; a TRANSIENT blobstore or database failure is not per-file and fails the whole request), media.get, media.list (folder, recursive, search, sort, ids, paging), media.listFolders, media.createFolder, media.renameFolder, media.listFolderItems (for references.guardAll:media), media.removeFolder, media.update, media.download, media.remove, media.reconcile, media.reconcileDelete, media.export:<dir>.

Error codes per step (every step can also answer TRANSIENT when the blobstore or the database is unavailable):

| Step | codes | |---|---| | media.upload | VALIDATION (no file, invalid folder or provenance), UNSUPPORTED (type not allowed), PAYLOAD_TOO_LARGE (over maxBytes), CONFLICT (filename taken by other bytes; details.field: "key") | | media.get | VALIDATION (unknown locale), NOT_FOUND | | media.list | VALIDATION (invalid folder, unknown locale) | | media.update | VALIDATION (name, folder, provenance or text), NOT_FOUND, CONFLICT (filename taken) | | media.download | NOT_FOUND | | media.remove | VALIDATION (missing id) | | media.createFolder | VALIDATION | | media.renameFolder | VALIDATION, NOT_FOUND, CONFLICT (target exists or is inside the source) | | media.folderItems | VALIDATION, NOT_FOUND, CONFLICT (folder not empty and no recursive) | | media.removeFolder | VALIDATION, NOT_FOUND | | media.listFolders, media.reconcile, media.reconcileDelete, media.export:<dir> | – |

media.reconcile lists the blobs under prefix and compares them with the media_items keys: { blobsWithoutRow, rowsWithoutBlob }. With delete: true it deletes the blobs nothing points at; media.reconcileDelete is the same step with the deletion fixed in the pipeline instead of the request, so a route can offer the report and the cleanup as two different endpoints; rows are never deleted, because a row is the only record that a file was ever meant to exist. Keys under media-variants/ are ignored — they belong to images/default, which this module cannot query. A row of an upload that is in flight right now shows up under rowsWithoutBlob, so run it from a cron trigger rather than after every write; no trigger is wired here, that is the consumer's job.

Not included: image resizing, tags, linking media to content documents.

Internals

Filename safety. The length cap is applied before a .meta.json ending is neutralised, so truncation can never re-create the sidecar suffix it removes; a filename that would otherwise land on a metadata-sidecar key is rewritten.

Subtree queries. persistence@1 takes one operator per field, so a folder subtree is not expressible as a single gte + lt range. Rows are queried with folder >= "<path>/" sorted ascending and read until the first row whose value leaves the <path>/ prefix — the sort keeps every matching row contiguous at the front, so no LIKE is involved. The upper marker is "<path>0": 0 (0x30) is the first codepoint after / (0x2F) among the characters safeFolder allows, so a row at or past it is neither <path> nor a descendant of it.

Search. media.list's q runs as SQL LIKE, which folds case for ASCII letters only and can therefore only narrow the candidate set; the exact match is applied in JS afterwards.

Write ordering. media.update builds and validates the whole patch, text fields included, before the blob is touched, so a rejected patch never leaves a moved blob without a matching row. A repeated move finds the source blob already gone; when the target is there, only the row update is left to do.

Migration. A folder or filename that safeFolder/safeName would themselves change or reject, and a key that would land on a metadata-sidecar suffix, are left for manual cleanup instead of being renamed or moved automatically.

Generated from the manifest

@michaelthielemann/kestrel-media-default – module media/default: provides no contract; requires blobstore@1, persistence@1.

| Config | Type | Required | Default | |---|---|---|---| | allowedTypes | array | no | ["image/*","application/pdf"] | | deniedTypes | array | no | ["image/svg+xml","text/html","application/xhtml+xml"] | | maxBytes | integer | no | 5242880 | | locales | array | no | [] | | defaultLocale | string | no | – | | prefix | string | no | "media/" |

| Step | Summary | Reads | Writes | Input | Output | Errors | |---|---|---|---|---|---|---| | media.upload | Upload one or more files (multipart field file, repeatable). A single file returns the item; multiple files return per-file results. Idempotent: same folder, same filename and same bytes return the item that is already there, and the step then ends the pipeline itself (200), so no following step – notably events.emit:media.uploaded – runs for an upload that stored nothing | files | result | { folder?: string, provenance?: string } | object | object | 400 no file or invalid folder/provenance (per-file for a multi-file request); 409 filename already exists in that folder with different bytes, details.field: "key" (per-file for a multi-file request); the same bytes again are idempotent, not a conflict; 413 file exceeds maxBytes (per-file for a multi-file request); 415 type not allowed (per-file for a multi-file request) | | media.update | Rename, move, set provenance or texts (alt/title/description per locale) | params.id | result | { filename?: string, folder?: string, provenance?: object, locale?: string, alt?: string | null, title?: string | null, description?: string | null } ?locale: string | { id: string, filename: string, folder: string, contentType: string, size: number, key: string, checksum: string | null, status: "uploading" | "ready" | "failed", createdAt: number, updatedAt: number, provenance: object, width?: number | null, height?: number | null, alt?: string | null, title?: string | null, description?: string | null, … } | 400 invalid name, folder or text (texts are plain text, max 2000 chars); 404 not found; 409 filename already exists in that folder | | media.get | Media metadata | params.id | result | ?locale: string | { id: string, filename: string, folder: string, contentType: string, size: number, key: string, checksum: string | null, status: "uploading" | "ready" | "failed", createdAt: number, updatedAt: number, provenance: object, width?: number | null, height?: number | null, alt?: string | null, title?: string | null, description?: string | null, … } | 400 unknown locale; 404 not found | | media.list | List media, newest first | – | result | ?folder: string, recursive: boolean, q: string, sort: string, limit: integer, offset: integer, ids: string, locale: string | { items?: object[], total?: number, … } | 400 invalid folder or unknown locale | | media.listFolders | Folders (persistent and implied) with item counts | – | result | – | object[] | – | | media.createFolder | Create a folder (idempotent: an existing folder is returned unchanged) | – | result | { path: string } | { folder: string, count: number, … } | 400 missing or invalid path | | media.renameFolder | Rename or move a folder, moving its blobs | params.path | result | { path: string } | { folder: string, moved: number, … } | 400 missing or invalid path; 404 folder not found; 409 target folder already exists | | media.listFolderItems | Item ids in a folder, for use with references.guardAll:media before removeFolder | params.path | result | ?recursive: boolean | { path: string, ids: string[], … } | 400 missing or invalid path; 404 folder not found; 409 folder not empty (without recursive) | | media.folderItems | Deprecated, use media.listFolderItems: Item ids in a folder, for use with references.guardAll:media before removeFolder | params.path | result | ?recursive: boolean | { path: string, ids: string[], … } | 400 missing or invalid path; 404 folder not found; 409 folder not empty (without recursive) | | media.removeFolder | Delete a folder and everything in it | params.path | result | – | { ok: boolean, removed: number, … } | 400 invalid path; 404 folder not found; 409 folder not empty or still referenced | | media.download | The file itself | params.id | result | – | – | 404 not found | | media.remove | Delete file and metadata | params.id | result | – | { ok: boolean, … } | 400 missing id | | media.reconcile | Compare the blobs under the media prefix with the media_items rows; with delete also removes blobs that no row points at (rows are never deleted) | – | result | { delete?: boolean } | { blobsWithoutRow: string[], rowsWithoutBlob: string[], … } | – | | media.reconcileDelete | Like media.reconcile, but always deletes the orphan blobs – the deletion is in the pipeline, not in the request | – | result | – | { blobsWithoutRow: string[], rowsWithoutBlob: string[], … } | – | | media.export:<arg> | Copy every media item to // | – | result | – | { written?: number, skipped?: number, missing?: number, conflicts?: number, … } | – |

Pipelines in examples/minimal using these steps:

  • createMediaFolder (POST /media/folders): authn.requireUserauthz.require:media.writemedia.createFolder
  • deleteMedia (DELETE /media/:id): authn.requireUserauthz.require:media.deletereferences.guard:mediaimages.removemedia.removeevents.emit:media.deleted
  • deleteMediaFolder (DELETE /media/folders/*path): authn.requireUserauthz.require:media.deletemedia.listFolderItemsreferences.guardAll:mediaimages.removeManymedia.removeFolder
  • downloadMedia (GET /media/:id/file): authn.identifyUserauthz.require:media.readmedia.download
  • exportMedia (POST /admin/media/export): authn.requireUserauthz.require:media.managemedia.export:./data/exportimages.export:./data/export
  • getMedia (GET /media/:id): authn.identifyUserauthz.require:media.readmedia.getimages.attach
  • listMedia (GET /media): authn.identifyUserauthz.require:media.readmedia.listimages.attach
  • listMediaFolders (GET /media/folders): authn.identifyUserauthz.require:media.readmedia.listFolders
  • renameMediaFolder (PATCH /media/folders/*path): authn.requireUserauthz.require:media.writemedia.renameFolder
  • updateMedia (PATCH /media/:id): authn.requireUserauthz.require:media.writemedia.updateevents.emit:media.updated
  • uploadMedia (POST /media): authn.requireUserauthz.require:media.writesanitize.svgmedia.uploadevents.emit:media.uploaded