Files
skills/docs/identity/auth/macos-keychain
2026-05-29 08:20:56 +01:00
..
2026-05-29 08:20:56 +01:00
2026-05-29 08:20:56 +01:00

@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](./src/core/error-classifier/error-classifier.md) — _(no submodules)_- [keychain-bridge](./src/core/keychain-bridge/keychain-bridge.md) — _(no submodules)_- [default-executor](./src/core/keychain-bridge/default-executor.md) — Provides the runtime-aware default `CommandExecutor` used by the bridge when the consumer does not inject one. The exec…

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.spawn on Bun and Node's child_process.spawn otherwise; 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's zeroize before the parser returns. The decoded string is the caller's responsibility to dispose of.
  • The accessGroup config 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

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.