just-secrets
v0.0.2
Published
Native OS secret storage for Node.js. Zero dependencies.
Maintainers
Readme
just-secrets
Native OS secret storage for Node.js 22+. Zero dependencies, native addons, or install scripts.
npm install just-secretsimport { secrets } from 'just-secrets';
await secrets.set({
service: 'com.example.cli',
name: 'api-token',
value: 'your-api-token',
});
const token = await secrets.get({ service: 'com.example.cli', name: 'api-token' });
const deleted = await secrets.delete({ service: 'com.example.cli', name: 'api-token' });Node 22.12+ also supports const { secrets } = require('just-secrets') without flags. On earlier Node 22 releases, use ESM import or asynchronous import() from CommonJS. TypeScript declarations ship with the package. There is no build step.
API
Every operation is asynchronous and returns a Promise. Object and positional forms are interchangeable:
await secrets.set({ service, name, value }); // Promise<void>
await secrets.set(service, name, value);
await secrets.get({ service, name }); // Promise<string | null>
await secrets.get(service, name);
await secrets.delete({ service, name }); // Promise<boolean>
await secrets.delete(service, name);setcreates a credential or replaces its value.getreturnsnullwhen the credential is absent.deletereturnstrueon deletion, orfalsewhen absent.- Unavailable stores, denied access, unexpected responses, and timeouts reject. They are not treated as missing credentials.
- Empty values, whitespace, newlines, NUL, and Unicode are preserved exactly. Service and name are case-sensitive and are not normalized.
- Service and name must be nonempty strings, each at most 256 UTF-8 bytes. Values must be strings of at most 2,560 UTF-8 bytes. Unpaired Unicode surrogates are rejected. Invalid arguments reject with
TypeErrororRangeError.
macOS also has a 4,096-byte interactive-command buffer. A combination of long identifiers and a maximum-size value can exceed it; just-secrets rejects before starting the operation. Smaller identifiers or values are then required.
just-secrets uses its own namespaced identifiers and storage representation; it does not discover or migrate credentials written by other libraries.
Platform requirements
| OS | Native store | Bridge | Requirements |
| --- | --- | --- | --- |
| macOS | Login Keychain | /usr/bin/security | An accessible login keychain; macOS may prompt for access or unlock |
| Linux | Secret Service, such as GNOME Keyring or compatible KWallet | /usr/bin/secret-tool | libsecret tools, a session D-Bus, and a running Secret Service provider |
| Windows | Credential Manager | Windows PowerShell and the Windows Credentials API | Windows PowerShell 5.1 with Add-Type permitted and a credential-capable user logon session |
The JavaScript package contains no native binaries. Windows uses a bundled PowerShell script with an embedded C# P/Invoke bridge, compiled by the OS's Add-Type. This requires no downloaded module or developer toolchain, but is not JavaScript-only execution. Restricted PowerShell environments may block it; just-secrets does not bypass those restrictions.
On Debian/Ubuntu, install Linux prerequisites with:
sudo apt-get install libsecret-tools gnome-keyringInstalling the tools is not enough: a session bus and unlocked credential service must also be available. Headless containers, SSH sessions, and WSL often lack these. WSL uses the Linux backend; just-secrets does not automatically forward secrets to Windows. Unsupported operating systems reject without starting a subprocess.
Windows credentials use CRED_PERSIST_ENTERPRISE: credentials persist for the user and may roam when supported by the account configuration.
Security model
Secrets are encrypted at rest by the OS credential store. just-secrets never writes a plaintext vault, generates a file-encryption key, or falls back to weaker storage. It invokes known OS executables without a shell and sends secret input over pipes, not command-line arguments or environment variables. Internal encodings preserve exact strings; they are not encryption.
Each subprocess has a 60-second timeout and a combined 1 MiB output limit. Child-process errors and output are not attached to thrown errors. Values are not cached between calls or logged by the library.
This does not isolate secrets from malicious code running as the same OS user. On macOS, Keychain access is associated with /usr/bin/security, not a unique just-secrets application identity. Other programs invoking that tool can access items permitted to it. See GitHub CLI's discussion of this tradeoff. Windows and Linux access boundaries also depend on the user's session and store configuration.
JavaScript and PowerShell strings cannot be reliably zeroized. Secrets exist in process memory while in use and may be exposed through debuggers, memory dumps, or compromised processes. Node copies some data internally; clearing individual buffers is not a complete memory-erasure guarantee. See SECURITY.md.
Linux explicitly checks and unlocks matching items before reading or deleting, rather than mistaking a locked item for absence. These multi-command operations are not transactional. Concurrent changes can cause rejection, and a timed-out or failed mutation may already have taken effect. Do not automatically retry mutations without checking the result.
Handling unavailable storage
Storage failures never silently fall back to files. Applications can offer session-only credentials, ask the user to unlock their keychain, or explicitly accept an environment variable for CI:
const token = process.env.MY_API_TOKEN ?? await secrets.get('com.example.cli', 'api-token');Do not log secret values or arbitrary upstream error objects. just-secrets's operational errors carry a code:
| Code | Meaning |
| --- | --- |
| ERR_SECRETS_UNSUPPORTED | Unsupported operating system |
| ERR_SECRETS_UNAVAILABLE | Required executable or credential session unavailable |
| ERR_SECRETS_ACCESS_DENIED | Access denied where the platform exposes a distinct status |
| ERR_SECRETS_TIMEOUT | Credential operation exceeded its time limit |
| ERR_SECRETS_OUTPUT_LIMIT | Unexpectedly large credential-tool output |
| ERR_SECRETS_STORE | Other store failure or unexpected response |
Not every backend can distinguish locked, denied, and unavailable states; these can surface as ERR_SECRETS_STORE. Callers should provide an unlock/setup recovery path rather than interpreting this code as absence.
Development
Use the latest patch release of Node.js 22, 24, or 26 and npm for development. There are no runtime or development npm dependencies.
npm ci
npm test # node:test; injected adapters and real subprocess transport tests
npm run test:coverage # Node's built-in coverage
npm run test:integration # real native credential-store lifecycle
npm run test:package # pack, offline install, ESM/CommonJS import checksIntegration tests use a disposable keychain on macOS and UUID-namespaced credentials on Linux/Windows, with cleanup. They cover create, read, overwrite, delete, missing entries, Unicode, empty values, case-sensitive identity, and persistence across Node processes. They do not prove persistence across reboot.
The GitHub Actions matrix runs Node 22, 24, and 26 on Linux x64/ARM64, macOS Intel/Apple Silicon, and Windows x64 (15 combinations). Linux CI starts an isolated GNOME Keyring session. Native Windows tests require the Windows runner; macOS and Linux mocks alone cannot validate the PowerShell bridge.
Publishing
Repository: vercel-labs/just-secrets.
After the full CI matrix passes, review the version and package contents, then publish using an npm account authorized for just-secrets:
npm run test:package
npm pack --dry-run
npm publish --access publicPublishing credentials and npm package ownership must be configured separately. This repository does not automatically publish on push.
License
MIT
