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

@nodeholder/pdata

v0.8.0

Published

PData service for secure file management

Readme

@nodeholder/pdata

A Plan 9-inspired multi-tenant file management system with capability-based security, virtual filesystem namespaces, and audit logging. It gives an Express app secure, isolated per-user workspaces via a token-based capability model, with no database — everything is CSV + filesystem.

The "P" is for Provisional. PData is a pragmatic, file-based identity and storage layer meant for rapid deployment, not a permanent identity authority. It intentionally does not know about roles, tiers, sessions, or any particular host framework beyond what it needs to hold files and check capabilities. A host application is expected to own the durable identity/role model and treat PData's own user/role tables as a local, provisional default — see TECHNICAL_DESCRIPTION.md for the full design rationale.

Two things, two names: the vault and the mount

PData is two jobs behind one constructor, and it pays to name them:

  • The vault — the account store at rest. A directory carrying pdata.json + users.csv + roles.csv: a login verifier and a role per user, journaled and atomically written, pure filesystem. No secret. Open it by path with UserManager.open(dir); steward it from the terminal with pdata-usermgr (or pdata users in tetra). One of many, portable (cp -r reproduces it whole), sealable at rest.
  • The mount — the vault bound into a running host with the namespace engine on top: virtual mounts, capability globs, HMAC session tokens. That is new PData(...), and that is what needs PDATA_SECRET.

Vault and mount are two states of one store, the way a disk image is either sealed on disk or mounted live. If stewarding users ever demands a token-signing secret, the two have been fused again.

Using just the vault (no secret, no server)

import { UserManager } from '@nodeholder/pdata/users';   // or from the package root

const vault = UserManager.open('/abs/path/to/vault', { bootstrap: true }); // bootstrap: create if absent
await vault.addUser('alice', 'a-good-password', ['admin']);
vault.validateUser('alice', 'a-good-password');   // true
vault.listUsersWithRoles();                       // { alice: ['admin'] }
UserManager.describe('/abs/path/to/vault');       // { isVault, marker, files, users, roles }

Absent is not empty: opening a directory with no users.csv throws unless you pass bootstrap: true (or set PDATA_BOOTSTRAP=1). A store is created by an explicit act, never as a side effect of failing to read one.

From the terminal:

export PD_DIR=/abs/path/to/vault
pdata-usermgr init                # create an empty vault (marker + tables)
pdata-usermgr add alice admin     # prompts for the password (no echo); never on argv
pdata-usermgr list
pdata-usermgr passwd alice
pdata-usermgr setrole alice user dev
pdata-usermgr delete alice        # prior record journaled in users.journal.jsonl
pdata-usermgr status

A password arrives on $PDATA_PASS_FD (a descriptor — how tetra's pdata users hands over tetra_read_pass input), or at a muted tty prompt. A piped password is refused: a pipe is not a person.

Sealing a vault at rest (encryption under an operator-held passphrase) is a ceremony, not a library concern — pdata vault seal|open|verify in tetra (tetra/bash/pdata), reusing the escrow cipher and the one passphrase prompt. A running mount reads plaintext, protected by 0600/0700.

Install

npm install @nodeholder/pdata

Peer dependencies (express, and optionally @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner for S3-backed storage) are not bundled — install what your app needs.

Binding a second vault into a running mount (mount, 0.8.0)

A running host can bind an additional vault by path, under the alias ~<name>, without restarting with different constructor config:

const rec = pdata.mount('arcade', '/abs/path/to/other-vault', {
  roles: ['admin'],   // who gets the alias in their token (default: admin)
  access: 'ro',       // 'ro' = list+read, 'rw' adds write (default: ro)
});
// rec = { name, alias: '~arcade', root, roles, access, boundAt, users, marker }
pdata.listMounts();          // every bound mount
pdata.unmount('arcade');     // reverses containment, mountManager, and the record

mount refuses anything that is not a vault (UserManager.describe(dir).isVault — the pdata.json marker or a users.csv), a reserved system alias, a malformed name, a relative path, an unknown role, and the host's own root (that is already ~system). The alias and its caps are grafted into tokens minted after the bind for the roles named; a token minted before is a snapshot and never learns of it, and a principal outside those roles has no alias at all — the path cannot even resolve. What a bind exposes is an account store, so the default is admin-only, read-only.

The binding is runtime-only: mount is to constructor hostMounts what mount(8) is to fstab. A restart forgets it. Over HTTP the same three verbs are GET|POST /api/admin/mounts and DELETE /api/admin/mounts/:name in pbase, and pdata mount <path> in tetra is their CLI face.

Quick start

import { PData, createPDataRoutes } from '@nodeholder/pdata';
import express from 'express';

const pdata = new PData({
  // systemRoots: {},   // optional additional mount points
  // audit: { enabled: true, logLevel: 'info' },
});

// Authentication
const token = await pdata.createToken('alice', 'password123');
const payload = pdata.validateToken(token);

// User management
await pdata.addUser('bob', 'secret', ['user']);
await pdata.setUserRoles('bob', ['user', 'dev']);

// File operations, scoped to the caller's mount namespace
await pdata.writeFile('alice', 'file.txt', 'content');
const content = await pdata.readFile('alice', 'file.txt');
await pdata.listDirectory('alice', '~/data/users/alice');
await pdata.deleteFile('alice', 'file.txt');

// Mount as HTTP routes (requires Passport authentication middleware upstream)
const app = express();
app.use('/api/pdata', createPDataRoutes(pdata));

Environment variables

| Variable | Required | Purpose | |---|---|---| | PD_DIR | yes | Absolute path to PData's data root (the vault) | | PDATA_SECRET (or SESSION_SECRET) | mount only | Secret key for HMAC-SHA256 token signing — needed by new PData(...), never by UserManager.open | | PDATA_BOOTSTRAP | no | 1 = create an empty store if absent (the explicit act; same as open(dir,{bootstrap:true})) | | PDATA_PASS_FD | no | pdata-usermgr only: a descriptor to read a password from (never argv, never a pipe) | | PDATA_AUDIT | no | Set false to disable audit logging | | PDATA_AUDIT_LEVEL | no | debug/info/warn/error | | PDATA_AUDIT_CONSOLE | no | Set false to disable console audit output | | PDATA_AUDIT_LOG | no | Custom audit log path | | TSM_LOGS_DIR | no | Tetra TSM log directory, for LogAdapter integration |

Core components

  • PData — orchestrator; wires up the managers below and owns token lifecycle
  • UserManager — PBKDF2-SHA512 auth, CSV-backed users/roles
  • FileManager — virtual path resolution, symlinks, capability-gated CRUD
  • CapabilityManager — CSV-defined role → capability expansion (r:/w:/d:/l:/x: glob expressions)
  • AuthSrv — HMAC-signed token issuance/validation, mount resolution
  • AuditLogger — Redux-style event hooks (console, file, or custom sinks) with a query interface
  • MountManager — Plan 9-style three-tier mount namespaces (~data/~log/~cache, per-user ~/data/users/<name>)

Security notes

Protects against path traversal, null-byte injection, token forgery/expiry, capability escalation, cross-user namespace access, and CSV/username injection. It does not provide rate limiting, timing-attack-safe password comparison, or protection if PDATA_SECRET itself leaks — treat that secret and PD_DIR's permissions (700) as required deployment hardening, and put PData behind TLS in production.

Testing

npm test              # full suite
npm run test:watch
npm run test:coverage
npm run test:api      # API routes only

More

Full architecture, threat model, virtual filesystem layout, and every environment/API detail live in TECHNICAL_DESCRIPTION.md. See CHANGELOG.md for release history.

License

ISC