Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
@suaveplan/macos-keychain - macOS Keychain bridge for generic-password credentials via the /usr/bin/security CLI: typed errors, AbortSignal cancellation, OTel instrumentation, and an injectable CommandExecutor for testing
Summary
macOS Keychain bridge for generic-password credentials. Wraps the system
/usr/bin/security CLI behind a strongly-typed CRUD surface with typed errors,
AbortSignal cancellation, OpenTelemetry instrumentation, and an injectable
CommandExecutor that lets tests run anywhere without touching the real
keychain.
Documentation tree
- error-classifier — (no submodules)
- error-classifier — (no submodules)
- keychain-bridge — (no submodules)
- default-executor — Provides the runtime-aware default
CommandExecutorused by the bridge when the consumer does not inject one. The exec… - keychain-bridge — (no submodules)
- default-executor — Provides the runtime-aware default
- output-parser — (no submodules)
- output-parser — (no submodules)
- errors — (no submodules)
- keychain-errors — (no submodules)
- schemas — (no submodules)
- macos-keychain.schemas — (no submodules)
- error-codes — Centralises the eight error codes emitted by
@suaveplan/macos-keychainand the metadata table that downstream consume…
- keychain-bridge — (no submodules)- output-parser — (no submodules)- output-parser — (no submodules)- errors — (no submodules)- keychain-errors — (no submodules)- schemas — (no submodules)- macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
@suaveplan/macos-keychainand the metadata table that downstream consume… - error-classifier — (no submodules)- keychain-bridge — (no submodules)- default-executor — Provides the runtime-aware default
CommandExecutorused by the bridge when the consumer does not inject one. The exec…- keychain-bridge — (no submodules)- output-parser — (no submodules)- output-parser — (no submodules)- errors — (no submodules)- keychain-errors — (no submodules)- schemas — (no submodules)- macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
@suaveplan/macos-keychainand the metadata table that downstream consume…
- keychain-bridge — (no submodules)- output-parser — (no submodules)- output-parser — (no submodules)- errors — (no submodules)- keychain-errors — (no submodules)- schemas — (no submodules)- macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
- error-classifier — (no submodules)- keychain-bridge — (no submodules)- default-executor — Provides the runtime-aware default
CommandExecutorused by the bridge when the consumer does not inject one. The exec…- keychain-bridge — (no submodules)- output-parser — (no submodules)- output-parser — (no submodules)- errors — (no submodules)- keychain-errors — (no submodules)- schemas — (no submodules)- macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
@suaveplan/macos-keychainand the metadata table that downstream consume…
- keychain-bridge — (no submodules)- output-parser — (no submodules)- output-parser — (no submodules)- errors — (no submodules)- keychain-errors — (no submodules)- schemas — (no submodules)- macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
- error-classifier — (no submodules)- keychain-bridge — (no submodules) - default-executor — Provides the runtime-aware default
CommandExecutorused by the bridge when the consumer does not inject one. The exec…- keychain-bridge — (no submodules)- output-parser — (no submodules) - output-parser — (no submodules)- errors — (no submodules) - keychain-errors — (no submodules)- schemas — (no submodules) - macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
@suaveplan/macos-keychainand the metadata table that downstream consume…
- keychain-bridge — (no submodules)- output-parser — (no submodules) - output-parser — (no submodules)- errors — (no submodules) - keychain-errors — (no submodules)- schemas — (no submodules) - macos-keychain.schemas — (no submodules)- error-codes — Centralises the eight error codes emitted by
Install
bun add @suaveplan/macos-keychain
Quick start
import { MacOSKeychainBridge } from "@suaveplan/macos-keychain";
const bridge = new MacOSKeychainBridge({ timeoutMs: 5_000 });
await bridge.addItem({
service: "my-app",
account: "alice",
password: "hunter2",
});
const item = await bridge.findItem("my-app", "alice");
console.log(item.password); // "hunter2"
await bridge.deleteItem("my-app", "alice");
Security model
- Arguments are always passed to the binary as a
string[]— no shell wrapper, no string interpolation. Even payloads containing shell metacharacters (a; rm -rf /,$(whoami), …) are forwarded verbatim. - The default child-process launcher uses
Bun.spawnon Bun and Node'schild_process.spawnotherwise; both spawn the binary directly by file path. - The output parser materialises the password bytes into a temporary
Uint8Array, decodes UTF-8, and zeroes the buffer via@suaveplan/crypto-utils'szeroizebefore the parser returns. The decodedstringis the caller's responsibility to dispose of. - The
accessGroupconfig option is forwarded to the CLI so credentials can be scoped to a specific keychain access group.
Stub-executor guide for tests
import {
MacOSKeychainBridge,
type CommandExecutor,
} from "@suaveplan/macos-keychain";
const stubRunner: CommandExecutor = async (_cmd, args) => {
if (args[0] === "find-generic-password") {
return { stdout: "", stderr: 'password: "stored"', exitCode: 0 };
}
return { stdout: "", stderr: "", exitCode: 0 };
};
const bridge = new MacOSKeychainBridge({ timeoutMs: 1_000 }, stubRunner);
const item = await bridge.findItem("svc", "user");
The bridge's only escape hatch into the OS is the second constructor
argument; injecting any plain object literal that satisfies the
CommandExecutor type covers every branch without vi.mock.
Platform requirements
The package builds on every platform but the real CRUD operations only work
on macOS where /usr/bin/security exists. Off-Darwin, the bridge throws
KEYCHAIN.UNAVAILABLE. The integration test file
(tests/integration.test.ts) is platform-guarded and skipped automatically
on non-Darwin hosts.
Modules
- src/types/macos-keychain.types.md — public type contracts.
- src/error-codes.md — eight stable codes + metadata table.
- src/errors/keychain-errors.md — named factory helpers.
- src/schemas/macos-keychain.schemas.md — Zod schemas.
- src/core/error-classifier/error-classifier.md — exit-code/stderr mapping.
- src/core/output-parser/output-parser.md —
find-generic-passwordanddump-keychainparsers. - src/core/keychain-bridge/keychain-bridge.md — the entry-point class.
- src/core/keychain-bridge/default-executor.md — runtime-aware default
CommandExecutor.
Benchmarks
Run via bun run bench. Numbers are reported on the local macOS / Linux
host and serve as drift trackers, not absolute SLOs.
| Benchmark | Iterations | Notes |
|---|---|---|
findItem_stub |
100 000 | Parses static stdout/stderr through the bridge. |
classifyError_10_patterns |
1 000 000 | Pure-function classification overhead. |
Baseline numbers will be inserted once the first hosted CI runs the bench
job; until then the suite exists to detect regressions locally via
bunx mitata.
Errors
Eight codes — see src/error-codes.md for the full metadata table:
KEYCHAIN.NOT_FOUND · KEYCHAIN.ITEM_EXISTS · KEYCHAIN.ACCESS_FAILED ·
KEYCHAIN.LOCKED · KEYCHAIN.UNAVAILABLE · KEYCHAIN.PERMISSION_DENIED ·
KEYCHAIN.ITEM_INVALID · KEYCHAIN.TIMEOUT.
License
MIT © Suaveplan Team.