@byokit/secrets

中文文档
0.5.3发布于 2026/10/25Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 7524

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

原始使用文档
文档目录9 节
  1. Backends
  2. Errors
  3. Seal accounts files on desktop
  4. Opt-in dual wrapping
  5. Envelope and keyring rotation
  6. Binding choice and security review
  7. Servers and headless Node
  8. Rotate an automatic host key
  9. Threat model

@byokit/secrets

One secret per name for apps that hold API keys: get/set/delete(name). Platform stores plus ready Node sealing adapters:

import { fileStore, keyringStore, overrideStore } from '@byokit/secrets';

// The OS keyring: macOS Keychain or Secret Service (libsecret).
const keys = keyringStore(); // { service?, bin?, tool?, env?, timeoutMs? }
await keys.set('openai', 'sk-…');
await keys.get('openai'); // 'sk-…' | null
await keys.delete('openai'); // true when an entry existed

// A passphrase-sealed file, sealed with @byokit/seal.
async function saveToFile(passphrase: Uint8Array) { // supplied by the person through the host app
  const file = fileStore({ path: '/home/app/data/keys.json', passphrase });
  await file.set('openai', 'sk-…');
}

// CI: the host builds the map from process.env itself; the kit never reads it.
const ci = overrideStore({ openai: process.env.OPENAI_API_KEY ?? '' });

The default Node entry requires Node 22.18+. Browser and React Native conditions select portable entries with no runtime Node imports. Explicit entries are @byokit/secrets/node, @byokit/secrets/web and @byokit/secrets/native. All entries export Keystore, KeystoreError and overrideStore; platform backends are exported only on their platform.

// Web / PWA (also available from the main entry under the browser condition).
import { webStore } from '@byokit/secrets/web';
const web = webStore({ database: 'my-app-keys' });
await web.set('openai', 'sk-…');

// React Native (also available from the main entry under the react-native condition).
// Install the optional peer in the host app: npx expo install expo-secure-store.
import { nativeStore } from '@byokit/secrets/native';
const phone = nativeStore({ prefix: 'my-app' });
await phone.set('openai', 'sk-…');

Backends

Native OS keyring (osKeyringStore({ service })). macOS Keychain, Windows Credential Manager and Linux Secret Service through the optional, exactly pinned @napi-rs/keyring 2.1.0 N-API binding. The synchronous form osKeyring({ service }) implements KeyringBackend for sealing; the store form implements the common async API. The binding loads only on an operation. Linux explicitly requires { linux: { store: 'secret-service' } }: no fallback to a kernel keyring that disappears on reboot. An absent/locked service or missing native binary throws KeystoreError with code: 'unavailable'. Only an actual absent entry returns null/false. Unsupported OSes throw unsupported. Names, service and values are validated before native calls; native errors are sanitized. Unlike the CLI backend, values ending in LF are preserved.

OS keyring (keyringStore). macOS Keychain through /usr/bin/security, Secret Service through /usr/bin/secret-tool, each spawned by absolute path. The secret reaches the CLI only on stdin, never in argv or env; every spawn gets an env built from nothing (PATH, LANG) plus only what the host passes in env (on Linux that is where DBUS_SESSION_BUS_ADDRESS goes). get strips the single trailing LF the CLI adds when it prints a secret, so a secret ending in a newline round-trips without it. Windows Credential Manager is unsupported in v1: on win32 every call rejects unsupported.

Passphrase file (fileStore). The whole map lives in one JSON file { v: 1, kdf: 'scrypt-16384-8-1', salt, box } where box is sealSecretBox over the entries under a scrypt key, with a fresh 16-byte salt per save. Writes go through the exported writeFileAtomic (0700 folders, 0600 file, rename over the target). A wrong passphrase rejects auth-failed and fails closed: nothing is returned and nothing is written. Prefer a Uint8Array passphrase: a string cannot be zeroed because the runtime keeps copies; derived keys are zeroed after use.

CI override (overrideStore). A validated copy of the map the host passes; get/set/delete work on the copy. In CI the host builds it from process.env itself — the kit never reads the environment, so a poisoned environment cannot change what the kit does.

Phone (nativeStore). Calls Expo SecureStore's async get/set/delete methods. The optional expo-secure-store peer loads on the first operation; importing the entry does not load native code. A host can supply secureStore with the same three methods for tests or another compatible secure backend. options passes SecureStore's keychainService, accessibility, authentication and access options consistently to every call. Native failures reject with a sanitized failed error; a missing peer rejects unavailable. There is no simulator plaintext fallback. Names are encoded as fixed-width UTF-16 hex under prefix (default byokit) to satisfy Expo's key alphabet without collisions. SecureStore can reject large values even below the kit's 1 MiB limit; authentication-protected values can become inaccessible after biometric changes. See Expo's installation and persistence guide. Native delete reads before deleting; concurrent callers may both report true.

Web (webStore). Requires a secure browser context (HTTPS or trustworthy localhost), IndexedDB and WebCrypto. database defaults to byokit-secrets; use a distinct database per app on a shared origin. The wrapped object store holds a structured-cloned non-extractable AES-256-GCM key under device-wrap-key, and versioned ciphertext under item:<name>. Each set generates a fresh 12-byte IV; the JSON-encoded entry name is authenticated as additional data. Secret values are also JSON-encoded to preserve every JavaScript string, including unmatched UTF-16 surrogates. Initialization chooses/inserts the key in one readwrite transaction, so concurrent tabs keep one key. Writes resolve only after transaction commit; delete checks existence and removes the entry in one transaction. Tampered ciphertext, swapped entry names and invalid keys reject auth-failed. Browser storage/crypto failures reject with sanitized errors; there is no plaintext fallback. Injectable indexedDB, crypto and isSecureContext options are for fake-only tests. No key is cached after an operation: clearing origin storage requires sign-in again. This protects ciphertext at rest, not against same-origin scripts or XSS, which can use the stored key to decrypt. Browser profile copying and storage eviction remain browser/OS concerns.

Existing product stores are not imported automatically. To migrate, the host reads each entry through its old store, sets it through this API, verifies it, then removes the old entry. Native names and web ciphertext have kit-owned formats, so pointing at an old app's database is not a migration.

Errors

KeystoreError with code: invalid (bad name, secret, path, passphrase or options — messages name the entry, never the secret), keyring-locked (sealed keyring credentials temporarily inaccessible; retry after unlock), auth-failed (wrong passphrase/key, tampered file or web ciphertext/key), unsupported (Windows CLI keyring or unsupported native OS), unavailable (missing/inaccessible native keyring, CLI, host key, Expo peer or browser APIs), failed (anything else: platform errors, timeouts, oversized output, unreadable files). Missing entries are not errors: get resolves null and delete resolves false.

Seal accounts files on desktop

import { fileStore } from '@byokit/accounts';
import { osKeyringSeal } from '@byokit/secrets/node';

const seal = osKeyringSeal({ service: 'Umer-desktop' });
const accounts = fileStore('/app-owned/private/accounts.bin', seal);
// Or directly: fileStore(path, osKeyringSeal({ service: 'Umer-desktop' })).

Use the app's own absolute path, with a private 0700 parent directory. Accounts 0.8.0's actual seam is fileStore(path, adapter); the adapter synchronously implements encryptString/decryptString. Run this in Node or Electron's main process. Native calls may block or trigger OS authorization UI; keep them off renderer/request latency paths. Each service must be unique to an app/security context. osKeyringSeal tries non-interactive OS keyring access when constructed. If available, it creates a random 32-byte data key on the first write and stores it in the OS keyring. If the service is absent, locked or unresponsive, it automatically uses the persistent host-key file described below. seal.mode reports the current write mode (successful reads adopt the store's mode): 'keyring', 'host-key-file' or opt-in 'dual-wrap'; hostKeySeal().mode is 'host-key'. Pass fallback: false when the keyring is mandatory. Once a file key exists, it remains the mode for new writes to that service even if a keyring becomes available later. Reads always follow the store header: mode 1 uses the keyring, mode 2 uses the host file key, regardless of the write mode or an existing fallback directory. A mode-1 store opened while the keyring is locked or unresponsive throws sanitized KeystoreError code keyring-locked, not auth-failed; it stays unchanged. Retry the read after unlock (the same adapter works). @byokit/openclaw resolves prepare()/start() in a locked state with plain words, and a later start() retries. It does not launch the helper with an empty store. Provider credentials remain in the sealed accounts file. Automatic keyring selection runs operations in a short-lived helper with a 1000 ms timeout (timeoutMs, 100–5000 ms). Key material travels over private stdin/stdout pipes, never arguments or child environment variables. Electron helpers use ELECTRON_RUN_AS_NODE=1; hosts that disable that fuse should select hostKeyFileSeal directly. Linux uses an exactly pinned D-Bus client and the existing session bus: no service activation, collection creation, Unlock or Prompt calls. Locked items and collections fail closed. It reads only DBUS_SESSION_BUS_ADDRESS for that session (otherwise the standard user bus). Windows uses the native Credential Manager backend. The pinned macOS native binding cannot suppress authorization UI, so automatic selection uses the file key there; explicit osKeyring() remains available to hosts that permit interaction. Injected keyring backends must themselves be bounded and non-interactive. The kit never configures or unlocks the person's keyring.

Opt-in dual wrapping

import { osKeyringSeal } from '@byokit/secrets/node';
const seal = osKeyringSeal({ service: 'my-app', dualWrap: true });

Default off. When the keyring is accessible, each mode-3 envelope wraps a fresh random payload key with both the OS keyring and the persistent owner-only host file key. A locked or unresponsive keyring can open this envelope through the host wrap without prompting. The payload authenticates both wrapping envelopes, their lengths, the outer header and service. A dual-wrapped store is only as strong as the owner-only host-key file, even if the OS keyring is locked. Exclude the host-key directory from backups/exposure boundaries containing ciphertext; same-user code and whole-disk copies containing both key and store can decrypt it. dualWrap: true with fallback: false is invalid.

Reading an existing keyring-only store while unlocked upgrades it to dual-wrap atomically through accounts fileStore or OpenClaw's authSeal. Authentication/validation and a decrypt-equality check precede replacement; a failure or interrupted replacement leaves the original key/store usable. Hold the app's exclusive writer lock when multiple processes share a store. Other consumers of the adapter must call its optional upgrade(bytes) after validating decrypted contents, verify equality, and atomically replace the file themselves; decryptString alone never changes disk.

A newly constructed adapter without keyring access still selects host-key-file for new stores. Successful reads adopt the stored mode for subsequent writes, so an unlocked keyring-only store stays keyring-only and a dual store stays dual. While locked, dual updates reuse only authenticated encrypted wrapping metadata with a fresh payload nonce; plaintext keys are never cached. Creating new dual wraps requires keyring access. There is no automatic rewrite of a locked keyring-only store. Dual rotate(paths) creates a new keyring wrapping key and reseals the supplied stores, retaining prior wrapping keys for backups. It does not retire or rotate the shared host-file wrapping key. Rotate mixed-mode archives in separate groups.

Envelope and keyring rotation

The envelope is BKS1 | mode (1 byte) | key id (16 bytes) | sealSecretBox payload, using @byokit/seal's XSalsa20-Poly1305 with a fresh 24-byte nonce. The encrypted payload repeats the entire header and holds JSON { service, text }; both header and app context are authenticated. Version changes, wrong keys, truncation, tampering or lost/corrupt key entries throw auth-failed. With fallback: false, missing native storage throws unavailable. There is no plaintext fallback or replacement key generation during decryption. The native binding never sees the file's credentials.

seal.rotateKey() creates and verifies a fresh key, then activates it for subsequent writes. Old keys remain in the keyring so previous files and backups still open. Each immutable key has a random entry name, avoiding lost keys when two processes initialize/rotate at once; the last active-id update wins. A failed activation may leave an unused key. Instances re-read the active id on each write, so other processes pick up rotation. Rotation alone does not rewrite files: rewrite through accounts while holding the host's writer lock, verify, then retire old backups. To revoke an old key, the host deletes byokit-seal-key-v1-<id> through osKeyring({ service }).delete(name) only after every file using it has been migrated or retired. Deletion permanently prevents those backups from opening. The pointer is byokit-seal-active-v1; never delete the currently active data key. File writers still require a host lock across processes.

Binding choice and security review

We evaluated platform CLIs and native bindings. The existing CLI store remains available for explicit host-controlled binaries/environments, but its interface is async and lacks Windows. The maintained N-API keyring binding supplies prebuilt binaries for desktop architectures and direct native APIs without shell/argv secrets; N-API avoids a per-Electron-version ABI rebuild. Its platform dependencies use Keychain, Credential Manager and the same Secret Service protocol used by libsecret on Linux. We pin 2.1.0, whose Linux store-selection API lets us require persistent Secret Service. We reuse the kit's existing reviewed crypto primitive instead of introducing another algorithm or implementing crypto. A missing optional binding on a native path fails closed (automatic selection falls back to the file key); hosts must bundle its platform binary with Electron and keep .node files outside ASAR.

  • Other local users: OS account isolation protects keyring data; accounts' private directories/files also limit access. Privileged attackers and malware running as the same unlocked user can ask the keyring to decrypt or inspect process memory; these adapters do not stop them.
  • Leaked files and backups: ciphertext alone reveals length/version/key id, but no API key. Restoring it requires the original app service and retained OS keyring data (or the host key). Copying both keyring material and files can defeat this protection; OS backup policy is host-owned.
  • Tampering fails authentication before accounts reads or writes a credential. Complete replay of an older valid file remains possible; this adapter supplies no rollback/freshness guarantee.
  • Owned byte copies are zeroed after use; JavaScript strings, native copies and returned plaintext cannot reliably be erased. Never log keys, credentials or decrypted content.

Tests drive the built package the way a host app does: test/journey-secrets.test.ts imports the published entries @byokit/secrets, @byokit/secrets/web, @byokit/secrets/native, @byokit/accounts and @byokit/seal, the dev-only esbuild and fake-indexeddb, plus three test helpers (fake keyring CLIs, the real-keyring guard and scratch dirs), and cross-process cases run the built dist/index.js. Fake keyring CLIs keep the owner's keyring untouched. A real Linux test is opt-in through sh scripts/test-keyring.sh, which clears the environment and creates a private HOME, XDG tree, D-Bus and daemon control directory. The test asserts the bus is the private one and refuses inherited desktop settings before any native call. Setting BYOKIT_REAL_KEYRING alone cannot authorize a real test. Without an available Secret Service the opt-in test skips, unless CI requires the provisioned service. CI runs the same isolated harness. The journeys cover keyring lookup and label scoping, keystore read/write/delete, sealing and unsealing with wrong-key, tampered and truncated input refused, process-env isolation, a locked keyring that is never unlocked or written to, and a locked-down permission run that leaves a decoy HOME byte for byte. macOS/Windows native runtime qualification remains host/platform CI work.

Servers and headless Node

osKeyringSeal({ service: 'my-app' }) works without manual provisioning on headless installs. hostKeyFileSeal({ service }) selects the automatic file adapter directly. Both use 32 random bytes from crypto.randomBytes, in a private 0700 directory with 0600 files (Windows installs and checks an owner-only ACL through the platform ACL API and fails closed if that is unavailable). Existing symlinks, foreign owners or group/world-accessible key files/directories are refused with a clear error; the kit never silently fixes their permissions or replaces a damaged key. Unique O_EXCL temporary files are fsynced before rename; first use publishes a complete directory atomically, so racing processes keep one key. POSIX directories are fsynced too; Node does not provide directory fsync on Windows.

The stable path is <platform state dir>/byokit-<SHA-256 of service>/host-key/. The state directory is XDG_STATE_HOME (absolute) or ~/.local/state on Linux, ~/Library/Application Support on macOS, and %LOCALAPPDATA% or ~/AppData/Local on Windows. Use a stable service per app. stateDir can override the platform state root, for example in tests; keep it separate from credential backups. The active pointer names an immutable <id>.key file. Never delete or edit these manually. The envelope uses mode 2 and authenticates the key id and service.

Threat model: a host key on the same disk protects sealed stores against copies, backups and archives leaking only when the key directory is excluded. It does not protect against code running as the same OS user, a whole-disk copy containing both key and store, or a privileged attacker. Losing the key makes the stores unrecoverable. Host-managed keys can keep a stronger separation by living in an external secret manager.

Rotate an automatic host key

Stop the engine and hold the app's exclusive writer lock across all processes, adapter creation and rotation. Supply every sealed store and retained archive using this service:

import { hostKeyFileSeal } from '@byokit/secrets/node';
const seal = hostKeyFileSeal({ service: 'my-app' });
seal.rotate(['/app/private/accounts.bin', '/app/private/auth-store.sealed']);

Rotation authenticates every input, durably saves a fresh key and a rotation journal, then atomically rewrites each file. Only after all replacements are durable does it activate the new key, remove the old one and remove the journal. It exposes no key bytes. An interrupted rotation keeps the journal and enough keys to open both generations; subsequent encryption refuses until seal.rotate() (no arguments) resumes the recorded transaction under the writer lock. Decryption still works during recovery. Do not edit/delete the journal or change/restore the listed files during recovery. Files omitted from the list, including old backups, become unreadable after retirement. rotateKey() retains its existing keyring-only behavior; automatic file keys require rotate(paths) so retirement cannot precede the store rewrites.

For an explicitly managed key, use hostKeySeal. The host supplies exactly 32 bytes from its secret manager or an app-owned 0600 key file kept separately from the accounts data:

import { readFileSync } from 'node:fs';
import { fileStore } from '@byokit/accounts';
import { hostKeySeal } from '@byokit/secrets/node';

// Host provisions this raw 32-byte key separately, with 0600 permissions.
const seal = hostKeySeal({ key: readFileSync('/host-managed/keys/Umer.key'), service: 'Umer-server' });
const accounts = fileStore('/app-owned/private/accounts.bin', seal);
// A synchronous resolver is also supported:
// hostKeySeal({ key: () => hostSecretCache.current32ByteKey(), service: 'Umer-server' })

Async secret-manager clients must resolve/cache the key before building the synchronous adapter. The adapter copies the host key per operation and zeroes its own copy; it never erases the host's bytes. A resolver failure throws sanitized unavailable; an invalid key length throws invalid. The host-key envelope uses the same authenticated format with mode 0 and a zero key id. It does not carry a key version: rotation is a host-controlled migration, decrypting with the old adapter and rewriting with the new adapter under the same writer lock. Retain old keys for backups as needed. Keep the key outside any backup/exposure boundary that includes the ciphertext; leaking both removes the protection. OS seals and host-key seals do not open each other's envelopes; switching requires this same deliberate decrypt/rewrite migration.

Threat model

See SECURITY.md.

版本与发布记录

@byokit/secrets 共发布 7 个版本,首次发布于 2026/09/30;过去 12 个月发布了 7 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.3latest2026/10/2514 天前
0.5.22026/10/1822 天前
0.5.12026/10/1227 天前
0.5.02026/10/011 个月前
0.4.02026/10/011 个月前
0.3.02026/09/301 个月前

关于 @byokit/secrets 的常见问题

@byokit/secrets 是什么?

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

如何安装 @byokit/secrets?

在项目目录运行 npm install @byokit/secrets,或使用 pnpm add / yarn add / bun add。

@byokit/secrets 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@byokit/secrets 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@byokit/secrets 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@byokit/secrets 可以商用吗?

@byokit/secrets 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@byokit/secrets 的下载量有多少?

根据 npm 官方下载统计,@byokit/secrets 在 2026/10/09 – 2026/11/07 这 30 天内被下载 7,524 次,日均约 251 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@byokit/secrets 还在维护吗?

最新版本 0.5.3 发布于 2026/10/25(14 天前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/09/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。