@wy-ai-labs/secure-store
v0.1.0
Published
Named credential sets encrypted at rest — OS keychain (injected Electron safeStorage adapter) when available, AES-256-GCM with a scrypt machine key otherwise; atomic writes, versioned envelope with migration hook, values never logged or listed.
Maintainers
Readme
@wy-ai-labs/secure-store
Named credential sets encrypted at rest — OS keychain (injected
safeStorageadapter) when available, AES-256-GCM with a scrypt machine key otherwise — that writes atomically and never puts a value on disk, in a log line, in an error or in a listing.
Part of the wy-ai-labs parts monorepo (packages/secure-store) · contract v1 · no Python twin
Install
npm install @wy-ai-labs/secure-store # Node ≥22.11, ESM only (import — no require)30-second usage
import { createSecureStore, createKeyProvider, CONTRACT_VERSION } from '@wy-ai-labs/secure-store';
import { homedir } from 'node:os';
import { join } from 'node:path';
const store = createSecureStore({
dir: join(homedir(), '.my-app'), // the app decides where the file lives
keyProvider: createKeyProvider({ salt: 'my-app-credentials-v1' }), // app-specific salt (not a secret); add `safeStorage` in Electron
});
store.set('hosted', 'api_key', process.env.LLM_API_KEY ?? 'paste-at-runtime'); // encrypted + written atomically
store.set('gateway', 'client_id', 'my-app');
console.log(CONTRACT_VERSION, store.listSets(), store.listKeys('hosted')); // 1 [ 'gateway', 'hosted' ] [ 'api_key' ] — names only
console.log(store.get('hosted', 'api_key') !== undefined); // true — values come back only through get()In an Electron main process pass the real keychain adapter: createKeyProvider({ salt, safeStorage }) — the part never imports electron; when safeStorage.isEncryptionAvailable() is false it falls back to the machine key, and a store written with one can be opened with the other.
Recovering from a file that cannot be opened (moved from another machine, modified, keychain gone):
try {
store.listSets();
} catch (err) {
if (err.code === 'locked' || err.code === 'integrity' || err.code === 'invalid_envelope') {
store.clear(); // preserves the old file once as <path>.unreadable-backup, then starts fresh
store.set('hosted', 'api_key', askUserAgain());
} else throw err;
}Contract
Public API, invariants and error model are fixed in CONTRACT.md; the tests in test/ are the contract suite (one test per numbered invariant). Three invariants to know before depending on this part:
- No plaintext on disk, no values in errors or listings — the file holds an envelope (
v,kdf,alg,iv,tag,data, …); set names, key names and values all live inside the ciphertext;listSets()/listKeys()return names only and everySecureStoreErrormessage is value-free. - Tamper →
integrity, wrong key →locked— a modified payload and a file sealed on another machine / user / salt / keychain fail with stable codes and one-line messages, never with a raw crypto error; an unreadable file is never silently overwritten — only an explicitclear()replaces it, after preserving it once as<path>.unreadable-backup. - Atomic writes, memory-only hot path — temp file + rename (mode
0o600where supported); a failed write leaves the previous file byte-identical; after the first load, reads never touch the disk and scrypt runs once per key provider.
An incompatible change bumps CONTRACT_VERSION and the major version together (CONTRACT.md → Compatibility).
Mined from
Extracted from two private source repositories (LLM_GATEWAY, module server/credstore.mjs; CodeReviewWar, modules server/llm/vendor/credstore.mjs, server/github-creds.ts; developed 2026-07-13 – 2026-08-08), generalized from a flat per-app credential blob to named credential sets with a pluggable key provider and a versioned JSON envelope, and re-tested for publication. What was kept, generalized and stripped: PROVENANCE.md.
Used by
| Client | Role of this part there |
|---|---|
| llm-gateway | provider API keys and per-provider connection settings entered in the tray UI, one set per provider |
| code-review-war | LLM provider credentials and per-host GitHub tokens (one set per host) without a plaintext .env |
| ark | embedding / chat endpoint keys for the local RAG workbench |
Dependencies & budget
- Runtime dependencies: 0 (budget 0, default 0) — enforced by
scripts/check-budget.mjs; imports arenode:crypto,node:fs,node:os,node:pathonly. - Allowed: other
@wy-ai-labs/*parts, pinned as caret ranges from the registry. Never a client, neverfile:/link:/ git URLs /../. - No private host, model id or key as a default: the part reads no environment variables; the caller chooses
dir,name,saltand the key provider.
Gates
| Command | What | When |
|---|---|---|
| npm test | contract suite (node:test), offline, seconds | every save |
| npm run gates:core | dependency budget + independence + engines.node + tests | before every commit / PR |
| npm run gates:full | everything above | nightly / before release |
CI only calls these scripts (wy-ai-labs/.github → node-gates.yml). Releases: Keep a Changelog + npm publish --provenance.
License
MIT © 2026 waneekim
한국어 요약
- @wy-ai-labs/secure-store — 이름 있는 자격증명 세트를 저장 시 암호화하는 부품. OS 키체인(호출자가 넘긴 Electron
safeStorage어댑터)이 있으면 그걸, 없으면 scrypt 머신 키 + AES-256-GCM을 쓰고, 원자적으로 쓰며, 값은 디스크·로그·오류 메시지·목록 어디에도 평문으로 남기지 않습니다. 공개 API·불변식·오류 모델은 CONTRACT.md에 고정되어 있고, 테스트가 곧 계약 스위트입니다. - 설치
npm install @wy-ai-labs/secure-store(Node ≥22, ESM). 런타임 의존성 0, 다른 part에만 핀 버전으로 의존, 사설 호스트 기본값 없음(환경변수도 읽지 않음). - 비공개 저장소
LLM_GATEWAY·CodeReviewWar의 credstore 모듈에서 추출·일반화했습니다(PROVENANCE.md). 커밋 전npm run gates:core.
