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

@williamthorsen/toolbelt.secrets

v0.3.0

Published

Utilities for storing and retrieving secrets in the OS credential store

Downloads

296

Readme

@williamthorsen/toolbelt.secrets

Utilities for storing and retrieving secrets in the OS credential store.

Release notes — v0.3.0 (2026-09-15)

🎉 Features

  • 🚨 Breaking: Move createTempTree from toolbelt.filesystem to toolbelt.testing (#313)

    • Moves CreateTempTreeOptions and the TempTree handle to @williamthorsen/toolbelt.testing/candidate along with createTempTree.

    Migration: Import createTempTree, CreateTempTreeOptions, and TempTree from @williamthorsen/toolbelt.testing/candidate, and declare @williamthorsen/toolbelt.testing in the manifest that declared @williamthorsen/toolbelt.filesystem for them.

Installation

pnpm add @williamthorsen/toolbelt.secrets

Requires Node.js 24 or later, and macOS: The one backend is the macOS keychain, reached through /usr/bin/security. Constructing a store on another platform throws, naming the platform, rather than failing later at the first call.

How a secret is stored

An item is named by a service and an account. The service is the caller's own name for the secret, never derived from a host or a URL, so one item can serve every product that accepts the same token. The account is optional and defaults to the empty account, which is matched exactly: A lookup on a service holding several accounts returns the one that is asked for, rather than an arbitrary one.

A secret reaches security through its interactive mode, which takes a whole command on stdin, so the secret never sits in an argument vector that any local process could read. It travels as hexadecimal, which encodes every byte, a line break included, and every write is read back and compared before it is reported as stored.

One command line contains the secret together with the service, the account, and the keychain, and security reads at most 4,095 bytes of it. That leaves room for a secret of roughly 2,000 bytes, and the exact ceiling falls as the other three grow. A secret that would not fit is refused, naming the room left; none is ever stored in part.

An item created here is local to the Mac that created it. security offers no iCloud Keychain synchronization, so a secret stored on one machine has to be stored again on the next.

CLI

The package ships a tb-secret command exposing the store to a shell caller.

pnpm add --global @williamthorsen/toolbelt.secrets   # puts tb-secret on PATH
npx @williamthorsen/toolbelt.secrets get my-token    # or run it without installing

tb-secret --help, each subcommand's --help, and tb-secret --version report the surface and the installed version.

| Subcommand | Effect | | ---------------------------- | ---------------------------------------------------- | | tb-secret delete <service> | Removes a secret | | tb-secret get <service> | Prints a secret | | tb-secret has <service> | Reports whether a secret is stored, printing nothing | | tb-secret set <service> | Stores a secret |

| Option | Effect | | ----------------------- | ------------------------------------------------------- | | -a, --account <name> | Account holding the secret (default: the empty account) | | -k, --keychain <path> | Keychain to act on, rather than the default search list |

At a terminal, set prompts for the secret twice and echoes nothing; piped, it reads stdin and drops one trailing newline, since echo adds one. The secret passes through this process either way.

tb-secret set atlassian-api-token --account [email protected]   # prompts, echoing nothing

pbpaste | tb-secret set atlassian-api-token                  # or pipe it

export ATLASSIAN_API_TOKEN=$(tb-secret get atlassian-api-token --account [email protected])

Exit codes

| Code | Meaning | | ---- | ------------------------------------------------------------- | | 0 | The command succeeded | | 1 | No secret is stored under that service and account | | 2 | Usage or validation error, with the message on stderr | | 3 | The keychain could not be reached, with the message on stderr |

An absent secret is 1 and a keychain that could not be reached is 3, so a script can tell one from the other.

if token=$(tb-secret get atlassian-api-token); then
  curl --user "[email protected]:$token" https://example.atlassian.net/rest/api/3/myself
elif [ $? -eq 1 ]; then
  echo 'No token stored. Run `tb-secret set atlassian-api-token`.' >&2
fi

createKeychainStore

createKeychainStore(options?: { keychain: string }): WritableSecretStore;

interface SecretQuery {
  readonly account?: string | undefined;
  readonly service: string;
}

interface SecretStore {
  deleteSecret(query: SecretQuery): boolean;
  findSecret(query: SecretQuery): string | undefined;
  hasSecret(query: SecretQuery): boolean;
}

interface WritableSecretStore extends SecretStore {
  setSecret(query: SecretQuery, secret: string): void;
}

Opens the macOS keychain as a secret store. Every call is synchronous.

import { createKeychainStore } from '@williamthorsen/toolbelt.secrets/candidate';

const store = createKeychainStore();

store.setSecret({ account: '[email protected]', service: 'atlassian-api-token' }, token);
store.findSecret({ account: '[email protected]', service: 'atlassian-api-token' });
// the token

findSecret returns undefined where no item is stored, and throws where the keychain could not be reached, so absence is never confused with a failure. deleteSecret reports whether an item was there to remove.

hasSecret reads the item's attributes rather than its data. That is the difference worth knowing: Retrieving a secret can raise a keychain access prompt where the item was created by another program, and an attribute lookup cannot.

setSecret rejects an empty secret, which the keychain would hold as an item indistinguishable from a stray one, and one too long for the command line that contains it. Both are UnstorableSecretError, which is exported: Nothing was attempted, so a caller can tell a value that the keychain cannot store from a keychain that it could not reach. Every other secret is stored and returned byte for byte, whatever it holds. Each write is read back and compared, so a secret that did not survive the round trip fails at the write rather than at a later caller. That readback retrieves the secret, so replacing an item created by another program can raise the keychain access prompt described above, and a write whose readback is refused is reported as unverified rather than as stored.

const projectStore = createKeychainStore({ keychain: '/Users/me/Library/Keychains/project.keychain-db' });

projectStore.findSecret({ service: 'deploy-key' });

A named keychain accepts a write like the default search list does. SecretStore remains the read-only half of the surface, for a backend that accepts no new secret.

promptSecret

promptSecret(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<string>;

Reads a secret from a terminal without echoing it, asking twice and comparing, since nothing on screen shows what was typed. It rejects where the two entries differ, and where the input ends before a secret is entered, which is the one event that Ctrl-C, Ctrl-D, and a closed stream all share.

import { promptSecret } from '@williamthorsen/toolbelt.secrets/candidate';

const secret = await promptSecret(process.stdin, process.stderr);

The prompts go to output and the line being edited does not, so a caller passing process.stderr leaves stdout free for the command's own result. security has a prompt of its own, but it fills a 128-byte buffer and hands back nothing to verify, which is why this reads the secret instead.