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

mikser-io-drive

v11.4.0

Published

WebDAV for mikser-io. Exposes working-folder directories as WebDAV endpoints via Nephele, authenticated through mikser-io-auth's htpasswd identity — so content can be edited from Finder, Explorer or any DAV client and the build picks the change up.

Readme

mikser-io-drive

Your content folders, as a drive. Mount them and edit; the site rebuilds. Or let an agent put files there without giving it a route to your server.

What you get

Edit content the way you already work. Mount an endpoint in Finder or Explorer and the folder is just a folder — drag a photograph in, rename a directory, open a document in whatever app you like. Every change is a content change: the watcher sees it and the site rebuilds. No upload form, no admin UI to learn, no second copy of your content to keep in step.

Give an agent file access that costs almost nothing. Four MCP tools carry bytes over the connection an agent already has, so it works from a sandbox with no network route to your host and from a desktop client with no shell. Ten photographs land in one call and one rebuild, each coming back with the string to paste into a page.

Deletes and moves that know what they would break. Removing a file that three documents still point at is refused, and it names all three — entity and field, including the one buried at cta.cycle[6]. Moving one can repoint them for you. The alternative is a green build and three broken images nobody sees until a customer does.

Nothing disappears quietly. An existing file is refused rather than overwritten unless you say so. A delete moves to trash rather than unlinking, so a wrong one is a move back. A batch that has one bad file in it writes none of them, so you are never left guessing what landed.

Everyone sees only their own folders. Access is per endpoint and per person: a reviewer reads content and cannot write it; an editor writes content and cannot touch layouts. The same grants apply whether someone is in Finder, a script, or an agent.

Set it up

import { drive } from 'mikser-io-drive'
import { auth }  from 'mikser-io-auth'

const identity = auth({
    capabilities: {
        editors:   ['drive:content', 'drive:content:write'],
        reviewers: ['drive:content'],          // read-only, by grant
    },
})

export default async () => ({
    plugins: [
        identity,
        drive({
            endpoints: {
                content: { folder: 'documents' },
                media:   { folder: 'files/media' },
                data:    { folder: 'data', readOnly: true },
            },
            auth: identity,
        }),
    ],
})

Then Finder → Go → Connect to Server → https://cms.example.com/drive/content, or point rclone or any DAV client at the same URL. Capabilities are derived from the endpoint name — drive:<name> to read it, drive:<name>:write to write — so the endpoint list above is the only place a folder is named.

For an agent

mikser_drive_add({ endpoint, files: [{ name, base64, mime }], folder?, overwrite?, dryRun? })
mikser_drive_read({ path })
mikser_drive_move({ moves: [{ from, to }], rewriteRefs?, dryRun? })
mikser_drive_delete({ paths: [path], force?, dryRun? })

Every writing tool takes a whole batch, because a build cycle is the unit of cost: three files sent together are one cycle and one rebuild, where three calls are three of each. A batch is all-or-nothing — if any entry cannot be applied, none are, and every reason is listed.

add returns for each file the reference to paste into a document plus any derived variants a preset produced. read gives back an image an agent can actually look at. move and delete refuse while something still references the file.

Bytes cost roughly 1.4 tokens each, so a page of photographs is cheap and a video is not: per file 2MB, per batch 8MB, above which they refuse and point at a mount, where the same folders are reachable and the bytes cost nothing.

Page text stays on mikser_update_entity — it is checksum-guarded, previews what an edit would invalidate, and returns the build report. These tools never write documents.

How it works

A Nephele WebDAV server per endpoint, mounted on mikser's Express app, serving directories from the working folder. Because those directories are mikser sources, a PUT is a content change — which is the whole point, and also where the sharp edges are.

One server per endpoint at <base>/<name>, the same shape as api/mcp/ forms. Nephele can multi-mount several adapters under one server with a virtual root and the URLs come out identical, but a per-endpoint server keeps each mount's auth, capabilities and read-only flag independent.

Working-folder hazards

These are properties of exposing live sources over a network filesystem, not bugs, but they will bite if nobody said them out loud.

  • Expose sources, never the output folder. DAV locks are advisory and mikser does not honour them, so a locked file the renderer rewrites makes the lock a lie.
  • File-manager litter is filtered by the engine, from mikser-io 9.6.0. This was measured, and the earlier claim here was wrong in an instructive way: the macOS files (.DS_Store, ._*) were already invisible, because globby defaults to dot: false and the watcher ignores leading dots. The Windows ones are not dotfiles — Thumbs.db and desktop.ini were both scanned and watched, and became entities. Core now filters a conservative OS/file-manager list on both paths; junk: false in config turns it off.
  • Upload size is bounded by the request timeout. Node caps a request at 5 minutes, which for uploads is a size limit expressed in seconds. Raise it with server.requestTimeout in mikser.config.js (mikser-io 9.5.0+).

Client compliance, measured

Tested against webdav, a third-party client that builds its own PROPFIND bodies and parses its own multistatus responses — so it disagrees where the implementation is wrong rather than where the tests are.

Three protocol facts worth knowing, all asserted in test/protocol.test.js:

| | | | --- | --- | | default (emulate) | DAV: 1, 3, 2class 2, which macOS requires for a read-write mount | | locks: 'disallow' | drops class 2 and LOCK from Allow. Finder will refuse a read-write mount. A trap, because it is invisible until someone tries | | emulate vs meta-files | both are real locking now (11.4.0): the <activelock> is in the LOCK body and in a later PROPFIND, and a held resource answers 423. They differ in where the lock lives — memory, or a sidecar |

Until 11.4.0 emulate stored no lock at all — a token in the header and an empty <lockdiscovery/>, which this table used to describe as survivable because "clients read the header". The Windows WebDAV redirector does not: it needs the <activelock> RFC 4918 §9.10.1 requires, and without it a mapped drive lists and reads perfectly and cannot be written to. Every save fails with "The parameter is incorrect" and leaves a zero-byte file, because Windows PUTs an empty file, LOCKs it, PROPFINDs it, finds no lock, and loops until it gives up. It reads like a permissions problem and is not.

emulate now keeps locks in memory, so the folder stays free of sidecars and the locks are real. They do not survive a restart — the same observable as every lock timing out at once, which is why locks carry timeouts — and there is no second process to share them with, since one mikser instance owns a working folder.

Choose meta-files if you need dead properties to persist. That is the one thing emulate still does not do, deliberately: a PROPPATCH answers 207 and the property is gone by the next PROPFIND (measured). Properties are meant to last, and an in-memory store would lose them on restart while a same-session read made them look durable — not storing is the honest answer. In practice what Windows PROPPATCHes is its Win32 timestamps, so on emulate a file keeps the server's mtime rather than the client's. It does not affect the write: the PUT has already succeeded by then.

One more, because it surprises people: LOCK on a path that does not exist creates an empty file (RFC 4918 §9.10.4). A client that locks before writing — Finder's Save As does — leaves an empty document behind even if the write never arrives.

A domain of its own, and what Windows needs

Explorer cannot map a share that lives at <base>/<name> on a site whose / is a static site. The Microsoft WebDAV redirector establishes its session against the server root before it touches the path: it asks /, gets the site's answer, and stops — the mount being correct makes no difference. Measured on a live host, OPTIONS /drive/SkinCheck/ answered DAV: 1, 3, 2 while dir \\host@SSL\DavWWWRoot\drive\SkinCheck\ still failed, and it failed at the root.

Give the drive a domain and / is the drive:

drive({
    host: 'drive.example.com',
    auth: identity,
    endpoints: {
        SkinCheck: { folder: 'skincheck' },
        Reports:   { folder: 'reports' },
    },
})

\\drive.example.com@SSL\ now mounts, and / is a collection whose children are the endpoints that caller may read — filtered by the same drive:<name> capability that guards the endpoint itself, so two people mapping the same drive see different folders in it. Entering one lands on that endpoint's own server with its own authenticator; the root is a listing, not a proxy.

OPTIONS / is answered without credentials, because that is what the redirector asks before it has any to offer — it discloses that a DAV server is here and nothing about what is in it. The listing is the gated part.

Scoped to the Host header, and that is not optional: mounted at / for every host, a drive would shadow every page on the site. Requests for any other host fall straight through.

The <base>/<name> surface is unchanged and still answers on every host. This adds a way in; it does not move the old one.

What the folder is called

The endpoint key is the name, and the mount reports it as displayname:

drive({
    endpoints: {
        SkinCheck:  { folder: 'skincheck' },              // shows as "SkinCheck"
        reports:    { folder: 'data', displayName: 'Q3 reports' },
    },
})

Without it the label is whatever the client derives from the URL, and that is not controllable: Express matches routes case-insensitively, so /drive/SkinCheck and /drive/skincheck reach the same mount and the casing shown is whichever the client asked for first — an endpoint renamed to SkinCheck kept reading skincheck in Explorer, and remapping did not change it. @nephele/adapter-file-system does not implement displayname (it is commented out of its live-property list with a TODO), so a PROPFIND for it answered 404 The displayname property was not found and the client had nothing to go on.

The default is the key, not summary: displayname is a name — RFC 4918 §15.2, "suitable for presentation to a user" — and the key is already the URL segment and what the mount log prints. summary is a sentence and reads badly as a folder label. displayName overrides the key where it is not presentable.

Only the mount root is named. Everything below it is a real file or directory whose name the client reads from the path.

When a client refuses and the server is right

WebDAV clients cache their discovery verdict — not the file listing, the answer to "is this a WebDAV share at all". Fix the server and the client can go on refusing, with nothing wrong at either end and nothing in any log to say so.

This endpoint answers Cache-Control: no-cache on OPTIONS so its own answer is never the stale one. The client's memory of a PREVIOUS answer is not something the server can reach, and flushing it is not guessable:

| | | | --- | --- | | Windows | Restart-Service WebClient -Force (elevated) | | macOS | unmount and remount; Finder caches per session | | Linux | gio mount -u <url>, or restart gvfs-dav |

Windows is the one that bites, because the Microsoft redirector decides from the DAV: header whether a URL is a share at all, and reports a cached refusal as 0x80070043 — "The network name cannot be found". It fails before prompting for credentials, so it reads as a broken URL rather than a cached verdict. An afternoon was lost to exactly that: curl verified the server correct while Explorer kept refusing.

The default adapter advertises max-age=604800 here. Seven days is a strange thing to promise about capabilities that are configuration: locks: 'disallow' drops class 2 and LOCK from Allow, and a client can keep acting on the old answer for a week. So this plugin overrides it. One cheap request per client session buys the ability to change a mount and be believed.

readOnly is not an example of that, though it reads like one: measured, a read-only mount reports the same DAV: 1, 3, 2 and the same Allow list as a writable one — PUT and DELETE included — and refuses the write when it arrives. nephele's read-only plugin gates requests, not the discovery response.

OPTIONS /drive and OPTIONS / answer a plain CORS 204 with no DAV: header, and that is correct. There is no resource at the base path: one Nephele server per endpoint, no virtual root, so PROPFIND /drive/ is a 404 by design and the site root is a static site, not a share. Only /drive/<endpoint>/ is a share. Noted because those 204s look like the bug fixed in 11.0.4 and are not.

litmus compliance

Scored with litmus 0.13, the WebDAV compliance suite from the neon project. Two endpoints, because the shipped default deliberately does not store what it is asked to store:

| suite | emulate (default) | meta-files | | --- | --- | --- | | basic | 16/16 | 16/16 | | copymove | 11/13 | 11/13 | | props | 20/30 | 27/30 | | locks | 9/13 | 37/41 | | http | 4/4 | 4/4 |

The emulate column is the trade working as intended: it reports success for dead properties and locks without storing them, so litmus reads them back and finds nothing. Choose meta-files if compliance matters more than a clean content folder.

The meta-files column is the fair measure of the dependency, and it has four real gaps — all upstream in nephele, all pinned in test/protocol.test.js so an upgrade that fixes them fails loudly rather than changing behaviour quietly:

  • COPY/MOVE with Overwrite: F returns 207, not 412 (RFC 4918 §9.8.5). The safe half holds — the destination is not clobbered — but the client is told the operation succeeded. Measured with a real client: copyFile(src, dst, { overwrite: false }) resolves, and the destination is unchanged. Scripts that copy-if-absent and then read the destination expecting the source's content will be wrong.
  • A malformed PROPFIND body answers 500, not 400.
  • propget loses a dead property in a foreign namespace.
  • UNLOCK accepts a bogus lock token, so one client can release another's lock. DAV locks are advisory here anyway — mikser's renderer does not honour them — but it means locking is not a concurrency control you can lean on.

Two warnings litmus raises that are worth knowing rather than fixing: DELETE with a fragment in the Request-URI removes the collection, and COPY into a non-existent collection answers 404 where 409 is specified.

Why writes are staged

Measured, not assumed. @nephele/adapter-file-system writes like this:

const handle = await fsp.open(this.absolutePath, 'w')   // truncates NOW
input.pipe(handle.createWriteStream())

Against a 512KB upload delivered in eight slow chunks:

| | adapter as-is | staged | | --- | --- | --- | | sizes seen at the destination mid-upload | 65536, 131072, … 524288 | never exists | | a 1600-byte file whose overwrite is interrupted | 196608 bytes of the new content | 1600 bytes, unchanged |

The first matters because these folders are mikser sources — the watcher can import a half-written file and render a truncated page. The second is data loss: not the old file, not an error, a corrupted file and no indication.

Staging to a sibling .part file and renaming fixes both, because rename(2) within a directory is atomic — the file appears complete or not at all, and a failed upload never opens the original. A sibling rather than the OS temp directory, because rename is only atomic within one filesystem and /tmp is usually a different mount.

Both rows above are asserted in test/atomic-writes.test.js.

Why Nephele, and not the alternative

The Node WebDAV server field is two live projects. Both were scored with the same litmus suites, each on a fresh server:

| suite | Nephele 1.0.0-alpha.67 | webdav-server 2.6.3 | | --- | --- | --- | | basic | 16/16 | 15/16 | | copymove | 11/13 | 12/13 | | props | 27/30 | 7/30 | | locks | 37/41 | 11/24 (aborted) | | http | 4/4 | 4/4 |

webdav-server has more stars (282 vs 108), is Express-mountable, and has a path-based privilege manager that would map neatly onto per-folder access. It also woke up on 2026-08-04 after six and a half years of silence — dropping v1, adding unit tests — so it is reviving rather than dead. And it gets the Overwrite: F case right, which is Nephele's most client-visible gap.

But 7/30 on properties and a locks run that aborts partway is a different class of problem from Nephele's four known gaps, and it carries 65 open issues against Nephele's zero. Nephele is also Apache-2.0 and load-bearing for its author's own product, which is the maintenance signal that matters most.

Writing it here was considered and is not close. RFC 4918 is about 4,650 lines in Nephele — 2,994 of method handlers and 1,662 of machinery — and the weight is not where it looks:

| | lines | | | --- | --- | --- | | Method.js | 930 | the base class every method inherits | | — checkIfHeader | 234 | the If: header alone | | — checkConditionalHeaders | 77 | | | LOCK + UNLOCK | 442 | plus 81 lock references threaded through Method.js | | MOVE + COPY | 438 | Depth and Overwrite semantics | | PROPFIND | 276 | | | GET/HEAD | 286 | ranges, etags |

Listing files is the cheap part. If: is a grammar rather than a value — tagged and untagged lists, Not, lock tokens and etags mixed, evaluated against resources other than the request target — and getting it wrong means Finder's Save As either walks over somebody's lock or refuses forever. Locking is not isolated in LOCK.js either: provisional locks, timeouts, per-user lock permission and token extraction run on every write method.

And the litmus column above is what would be given up: 95 of 104 on meta-files, with four known gaps named and pinned. A hand-written server starts at zero and earns each point by a client breaking in someone's hands.

The read-only case is genuinely different — OPTIONS + PROPFIND + GET with no locking and no conditionals is a few hundred lines, and the drive root in lib/root-listing.js is already that shape. It is not the case anyone is asking for.

The sharper version of the question is the ADAPTER, not the engine. Three things have been worked around here, and all three are in @nephele/adapter-file-system rather than in the protocol: displayname commented out of its live-property list with a TODO, a flat max-age=604800 on the OPTIONS response, and a read-only mount advertising PUT in Allow. The adapter answers "what is at this path and what are its properties", which is the thin half. If it keeps costing, replace it and keep the 4,650 lines that matter.

Staying on Nephele. If its gaps ever become the binding constraint, the serious alternative is not another npm package — it is Apache mod_dav, the implementation litmus was written to test, which reads htpasswd and htgroup natively with AuthUserFile / AuthGroupFile / Require group. That would cost the Express integration, the capability model, per-request read-only, atomic staged writes and the route inventory — a separate process serving the same folder, with its own idea of who may do what. Worth it only if strict compliance outranks all of that.

Nephele is pre-1.0

[email protected]. It implements RFC 4918 fully, but the version is what it says, so the dependency is pinned exactly. This is precisely the cadence argument in mikser's ADR-0006 for shipping as a plugin rather than in the engine.

One thing worth knowing if you extend this: Nephele's conditional-plugins hook cannot see the authenticated user. createServer mounts loadPlugins before authenticate, so response.locals.user is always undefined there — including in the README example that tests it. Per-user decisions have to happen in the authenticator, which is where the write gate lives.