Move docs/ package mirror into document-navigator plugin

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>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# error-classifier
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Translates the noisy `(exitCode, stderr)` tuples emitted by the macOS
`security` binary into a single `KeychainError` with a stable code. Apple's
CLI returns the same numeric exit code for several distinct failure modes,
so the classifier looks at stderr substrings first; the exit code only
matters when stderr is silent or generic.
This separation keeps the bridge implementation small — every CRUD method
calls the classifier on a non-zero exit code and either rethrows the
returned error or, when the classifier returns `null`, wraps the failure in
`keychainAccessFailed` for the trace to retain the original stderr.
## Features
- Pure function — deterministic on its inputs, no globals, no allocations
beyond the returned error.
- Stderr substring matching (case-insensitive) takes precedence over numeric
exit codes.
- Five exit codes are mapped: `errSecItemNotFound`, `errSecDuplicateItem`,
`errSecAuthFailed`, `errSecInteractionNotAllowed`, `errSecInvalidKeychain`.
- Returns `null` for unrecognised pairs so the caller can decide between a
generic fallback and a hard fail.
## Usage: Basic Example
```ts
import { classifyError } from "@suaveplan/macos-keychain";
const err = classifyError(44, "could not be found", "my-app", "alice");
console.error(err?.code); // "KEYCHAIN.NOT_FOUND"
```
## Usage: Advanced Example
```ts
import {
classifyError,
keychainAccessFailed,
} from "@suaveplan/macos-keychain";
async function callSecurity(): Promise<void> {
const result = await runner("/usr/bin/security", ["find-generic-password"]);
if (result.exitCode === 0) return;
const classified = classifyError(
result.exitCode,
result.stderr,
"my-app",
"alice"
);
throw classified ?? keychainAccessFailed("my-app", "alice", result.stderr);
}
declare function runner(
cmd: string,
args: readonly string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }>;
```
## API Reference
- **`classifyError(exitCode: number, stderr: string, service: string,
account: string): KeychainError | null`** — returns the matching
`KeychainError` or `null` for unknown combinations. Pure function.
Precedence:
1. Stderr matches `EXIT_LOCKED_PATTERNS` → `KEYCHAIN.LOCKED`.
2. Stderr matches `UNAVAILABLE_STDERR_PATTERNS` → `KEYCHAIN.UNAVAILABLE`.
3. Stderr matches `PERMISSION_STDERR_PATTERNS` →
`KEYCHAIN.PERMISSION_DENIED`.
4. Stderr matches `NOT_FOUND_STDERR_PATTERNS` → `KEYCHAIN.NOT_FOUND`.
5. Stderr matches `DUPLICATE_STDERR_PATTERNS` → `KEYCHAIN.ITEM_EXISTS`.
6. Stderr matches `INVALID_STDERR_PATTERNS` → `KEYCHAIN.ITEM_INVALID`.
7. Exit code `44` → `KEYCHAIN.NOT_FOUND`.
8. Exit code `45` → `KEYCHAIN.ITEM_EXISTS`.
9. Exit code `36` → `KEYCHAIN.ACCESS_FAILED`.
10. Exit code `25308` → `KEYCHAIN.LOCKED`.
11. Exit code `25295` → `KEYCHAIN.ITEM_INVALID`.
12. Otherwise → `null`.
## Implementation Notes
- Substring matching is case-insensitive; both the haystack and the needles
are lower-cased before `String.prototype.includes`.
- The locked-pattern check runs first because Apple frequently surfaces a
locked-keychain failure with exit code `1`, which the numeric switch
cannot disambiguate.
- The classifier never throws; on a malformed input (e.g. NaN exit code) it
returns `null` and lets the caller's fallback layer take over.
- Adding a new `errSec*` constant requires three edits: a new entry in
`constants/index.ts`, a new branch in this file's `switch`, and a new
scenario in `error-classifier.test.ts`.

View File

@@ -0,0 +1,78 @@
# default-executor
## Purpose
Provides the runtime-aware default `CommandExecutor` used by the bridge when
the consumer does not inject one. The executor encapsulates the only place
in the package that reaches into `Bun.spawn` or `node:child_process.spawn`;
every other module operates on the `CommandExecutor` abstraction so tests
can substitute a deterministic stub.
The factory chooses Bun when `globalThis.Bun` is present and falls back to
Node's `spawn` otherwise. Neither path goes through a shell — the binary
path and arguments are passed as a structured array.
## Features
- Runtime detection per call (so a host that lazily polyfills `globalThis.Bun`
still routes correctly).
- AbortSignal propagation — `options.signal` is forwarded to the underlying
spawn API so caller-side cancellation reaches the child.
- UTF-8 stdout/stderr capture — both streams are decoded as text before being
returned.
- Graceful failure — `error` events on the Node path reject the promise; Bun
errors propagate via `child.exited`.
## Usage: Basic Example
```ts
import { createDefaultExecutor } from "@suaveplan/macos-keychain";
const runner = createDefaultExecutor();
const result = await runner("/usr/bin/security", ["list-keychains"]);
console.log(result.exitCode, result.stdout);
```
## Usage: Advanced Example
```ts
import {
MacOSKeychainBridge,
createDefaultExecutor,
} from "@suaveplan/macos-keychain";
const runner = createDefaultExecutor();
const bridge = new MacOSKeychainBridge({ timeoutMs: 5_000 }, runner);
const controller = new AbortController();
const timeoutHandle = setTimeout(() => controller.abort(), 1_000);
try {
await bridge.findItem("github", "alice", { signal: controller.signal });
} finally {
clearTimeout(timeoutHandle);
}
```
## API Reference
- **`createDefaultExecutor(): CommandExecutor`** — returns an async
`(command, args, options) => Promise<CommandExecutorResult>`. The
resolved value contains `stdout`, `stderr`, and `exitCode`. Rejects only
when the runtime itself errors (binary missing, abort signal already
aborted).
## Implementation Notes
- The Bun branch reads the `stdout` / `stderr` `ReadableStream<Uint8Array>`
fully into memory before returning. For the `security` CLI this is fine
because the worst-case payload (a single keychain item plus surrounding
metadata) is a few kilobytes.
- The Node branch uses `child.stdout.setEncoding("utf8")` so the `data`
events deliver decoded strings; the executor concatenates them rather
than buffering raw bytes.
- Aborting the underlying child mid-call surfaces as either an `error`
event (Node) or a non-zero exit code (Bun). The bridge layer translates
both into `KEYCHAIN.TIMEOUT` when the combined signal indicates the
timeout fired.
- No globals are mutated; the factory returns a fresh closure per call so
tests can construct multiple bridges with independent runtime detection.

View File

@@ -0,0 +1,109 @@
# keychain-bridge
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hosts `MacOSKeychainBridge`, the single class consumers of the package
construct. It composes the executor, parser, error classifier, and OTel
meter into a strongly-typed CRUD surface for macOS generic-password items.
Every method either resolves with a typed result or rejects with a
`KeychainError` whose `code` is one of the eight registered literals.
The class is deliberately thin — most of the policy work (output parsing,
error classification, runtime detection) lives in sibling modules so it can
be tested in isolation. The bridge's job is to thread the
configuration / abort signal / timeout / OTel concerns through every CLI
invocation.
## Features
- CRUD methods: `addItem`, `findItem`, `updateItem`, `deleteItem`,
`listItems`, `itemExists`.
- Per-call `KeychainCallOptions.signal` cancellation, combined with the
configured `timeoutMs` deadline.
- Pluggable `CommandExecutor` constructor argument; defaults to
`createDefaultExecutor()` for the running runtime.
- Zod-validated config; an invalid `timeoutMs` throws a `ZodError` from the
constructor before any CLI is invoked.
- OTel emission on every call: `keychain.<op>` span (via the global
provider), `keychain.ops_total` counter, `keychain.cli_duration_ms`
histogram, and `keychain.items_returned` histogram (on `listItems`).
- Logger pass-through — when `config.logger` is provided the bridge emits a
`debug` record per invocation.
## Usage: Basic Example
```ts
import { MacOSKeychainBridge } from "@suaveplan/macos-keychain";
const bridge = new MacOSKeychainBridge();
await bridge.addItem({ service: "my-app", account: "alice", password: "hunter2" });
const item = await bridge.findItem("my-app", "alice");
console.log(item.password);
```
## Usage: Advanced Example
```ts
import {
MacOSKeychainBridge,
type CommandExecutor,
} from "@suaveplan/macos-keychain";
// Deterministic stub for tests — no real keychain, no `vi.mock`.
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 controller = new AbortController();
const item = await bridge.findItem("svc", "user", { signal: controller.signal });
console.log(item.password); // "stored"
```
## API Reference
- **`new MacOSKeychainBridge(config?: KeychainConfig, runner?:
CommandExecutor)`** — validates `config` via `KeychainConfigSchema` and
installs the runner. Calls `registerKeychainErrorCodes()` before
returning.
- **`addItem(item, options?): Promise<KeychainResult>`** — throws
`KEYCHAIN.ITEM_EXISTS` on duplicate; otherwise routes through
`classifyError` for failure mapping.
- **`findItem(service, account, options?): Promise<KeychainItem>`** —
parses `find-generic-password -g` output via `parseKeychainItem`. Throws
`KEYCHAIN.NOT_FOUND` when the item is absent.
- **`updateItem(service, account, newPassword, options?): Promise<
KeychainResult>`** — performs delete-then-add. Tolerates a missing
pre-existing item.
- **`deleteItem(service, account, options?): Promise<KeychainResult>`** —
throws `KEYCHAIN.NOT_FOUND` when the item does not exist.
- **`listItems(service?, options?): Promise<readonly KeychainListEntry[]>`** —
filters the dump output by service when provided. Records
`keychain.items_returned` with the post-filter count.
- **`itemExists(service, account, options?): Promise<boolean>`** — `true`
on success, `false` on `KEYCHAIN.NOT_FOUND`, rethrows other errors.
## Implementation Notes
- AbortSignal handling is centralised in `combineSignals`: a fresh
`AbortController` is constructed per call and aborts on whichever fires
first — the caller's signal or the configured `timeoutMs`. The `dispose`
callback removes the listener and clears the timeout to avoid leaking
handles.
- Timeout detection inspects the combined signal's `aborted` flag; this
ensures both caller-aborts and timer-aborts surface as
`KEYCHAIN.TIMEOUT`.
- Stderr placement of the password is a quirk of `find-generic-password
-g`; the bridge concatenates stdout and stderr before handing the buffer
to `parseKeychainItem` so the regex matches regardless of CLI version.
- `listItems` post-filters in TypeScript rather than relying on the CLI's
attribute filtering, which is unreliable across macOS versions.
- The default runner deliberately lives in a separate file so the bridge
itself stays under the file-size budget and so the test suite can
construct bridges without touching child-process imports.

View File

@@ -0,0 +1,101 @@
# output-parser
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parses the macOS `security` CLI output formats consumed by the bridge into
strongly-typed records. The CLI emits attribute-style output with quoted
strings, hex blobs, and timestamps; this module is the only place that
understands those wire formats so the rest of the package operates on
`KeychainItem` instances.
The parser is also the package's only sensitive-data handler: it materialises
the password bytes into a temporary `Uint8Array` long enough to decode UTF-8,
zeroes the buffer immediately, and returns only a JS string view. Callers
control how long the string lives.
## Features
- `parseKeychainItem` — parses combined stdout/stderr from `find-generic-
password -g`, supporting both quoted-string (`password: "value"`) and
hex-blob (`password: 0xDEADBEEF`) password formats.
- Attribute extraction — recognises `svce`, `acct`, `labl`, `icmt`, `cdat`,
`mdat` attribute lines and decodes Apple's date-time format.
- Buffer zeroization — the working `Uint8Array` produced by `TextEncoder`
and the hex-decode buffer are zeroed via `@suaveplan/crypto-utils`.
- `parseKeychainList` — walks `dump-keychain` records and returns
`{ service, account }` pairs, skipping entries that lack a `svce` or
`acct` attribute.
- Throws `KEYCHAIN.ITEM_INVALID` on malformed input rather than returning
undefined or null.
## Usage: Basic Example
```ts
import { parseKeychainItem } from "@suaveplan/macos-keychain";
const item = parseKeychainItem(
'password: "hunter2"\n"svce"<blob>="my-app"\n"acct"<blob>="alice"',
"my-app",
"alice"
);
console.log(item.password); // "hunter2"
```
## Usage: Advanced Example
```ts
import {
parseKeychainItem,
parseKeychainList,
} from "@suaveplan/macos-keychain";
const dump = `keychain: "/Users/alice/Library/Keychains/login.keychain-db"
"svce"<blob>="github"
"acct"<blob>="alice"
keychain: "/Users/alice/Library/Keychains/login.keychain-db"
"svce"<blob>="my-app"
"acct"<blob>="alice"
`;
const entries = parseKeychainList(dump);
for (const e of entries) {
const detail = parseKeychainItem(
`password: "${"redacted"}"\n"svce"<blob>="${e.service}"\n"acct"<blob>="${e.account}"`,
e.service,
e.account
);
void detail;
}
```
## API Reference
- **`parseKeychainItem(output: string, service: string, account: string):
KeychainItem`** — returns a fully-typed `KeychainItem`. Throws a
`KeychainError` with `code === "KEYCHAIN.ITEM_INVALID"` if the output is
missing a `password:` line or contains an undecodable hex blob.
- **`parseKeychainList(stdout: string): readonly KeychainListEntry[]`** —
returns a frozen-shape array. Records lacking `svce` or `acct` are
silently skipped (this matches Apple's own behaviour when listing items
that a process cannot read).
- **`KeychainListEntry`** — `{ readonly service: string; readonly account:
string }`.
## Implementation Notes
- The quoted-password regex tolerates backslash-escaped characters
(`\\`, `\"`, `\n`, `\t`); `unescapeQuoted` reverses the escape sequences
before the parsed value reaches the caller.
- Hex decoding uses `TextDecoder("utf-8", { fatal: false })` so a
truncated multi-byte sequence produces a `<EFBFBD>` replacement character
rather than throwing — this matches the CLI's own lenient behaviour.
- Dates use Apple's `YYYYMMDDHHMMSSZ` format with a trailing `\\000`
null marker; the parser strips the marker and returns `undefined` for
non-conformant strings rather than guessing.
- `parseKeychainList` splits on `^keychain:` so multiple keychains in a
single dump (login + iCloud + system) are walked independently.
- Both parsers are pure on their inputs; instrumentation is the caller's
responsibility.

View File

@@ -0,0 +1,91 @@
# error-codes
## Purpose
Centralises the eight error codes emitted by `@suaveplan/macos-keychain` and
the metadata table that downstream consumers (dashboards, retry policies,
i18n catalogs) use to interpret them. Defining the registry in one file means
factories, tests, and the bridge implementation never disagree on the literal
values, and future codes can be added without coordinating across modules.
The module also exports the `KeychainError` class — a thin subclass of
`SuaveplanError<KeychainErrorCode>` that narrows the code union and pre-sets
`this.name`. It is the only error class the package ever throws.
## Features
- `KEYCHAIN_CODES` — frozen `Readonly<Record<reason, "KEYCHAIN.<reason>">>`
produced by `defineCodes`.
- `KeychainErrorCode` — discriminated union of every literal code value.
- `KEYCHAIN_ERROR_METADATA` — frozen metadata table covering HTTP status,
severity, retryable flag, user-message key, category, and description.
- `registerKeychainErrorCodes()` — idempotent registration hook, called at
module bottom.
- `isKeychainErrorCodeRegistryRegistered()` — predicate exposed for tests.
- `KeychainError` — concrete subclass of `SuaveplanError<KeychainErrorCode>`.
## Usage: Basic Example
```ts
import {
KEYCHAIN_CODES,
KeychainError,
} from "@suaveplan/macos-keychain";
throw new KeychainError(KEYCHAIN_CODES.NOT_FOUND, "keychain.error.not_found", {
context: { service: "my-app", account: "alice" },
});
```
## Usage: Advanced Example
```ts
import {
KEYCHAIN_CODES,
KEYCHAIN_ERROR_METADATA,
KeychainError,
} from "@suaveplan/macos-keychain";
function isRetryable(err: unknown): boolean {
if (!(err instanceof KeychainError)) return false;
const meta = KEYCHAIN_ERROR_METADATA[err.code];
return meta.retryable;
}
if (isRetryable(thrown)) {
await retryWithBackoff();
}
declare const thrown: unknown;
declare function retryWithBackoff(): Promise<void>;
```
## API Reference
- **`KEYCHAIN_CODES`** — `Readonly<{ NOT_FOUND, ITEM_EXISTS, ACCESS_FAILED,
LOCKED, UNAVAILABLE, PERMISSION_DENIED, ITEM_INVALID, TIMEOUT }>` mapping
reason names to `"KEYCHAIN.<reason>"` literals.
- **`KeychainErrorCode`** — derived union type.
- **`KEYCHAIN_ERROR_METADATA`** — frozen `Record<KeychainErrorCode,
KeychainCodeMetadata>` keyed by code value.
- **`KeychainCodeMetadata`** — `{ httpStatus, severity, retryable,
userMessageKey, category, description }`.
- **`registerKeychainErrorCodes(): void`** — idempotent. The first call
flips an internal flag; subsequent calls are no-ops.
- **`isKeychainErrorCodeRegistryRegistered(): boolean`** — returns the flag
state. Test-only; production code should not branch on this.
- **`class KeychainError`** — `new KeychainError(code, messageKey, options?)`.
`options` is `SuaveplanErrorOptions` from `@suaveplan/error` (cause,
context, traceId, clock).
## Implementation Notes
- The metadata table is `Object.freeze`-d at module load to prevent runtime
tampering.
- `defineCodes` constructs the literal-typed code map without `as` assertions,
so renaming a code triggers a typecheck error in every consumer.
- `registerKeychainErrorCodes` does not currently push to a global registry —
it exists as a stable hook so a future change can introduce a
central code-discovery map without breaking the public surface.
- `KeychainError` always sets `this.name = "KeychainError"` so `err.name`-based
log filters work even after minification.

View File

@@ -0,0 +1,93 @@
# keychain-errors
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hosts the named factory helpers that produce `KeychainError` instances for
every documented failure code. Centralising construction means the message
key, code literal, and context shape are uniform across the package and across
consumers — call sites cannot accidentally drift on phrasing or omit
`{ service, account }`. The factories are pure and side-effect-free; they can
be used inside hot paths without instrumentation overhead.
## Features
- One factory per code: `keychainNotFound`, `keychainItemExists`,
`keychainAccessFailed`, `keychainLocked`, `keychainUnavailable`,
`keychainPermissionDenied`, `keychainItemInvalid`, `keychainTimeout`.
- Every factory returns a fully-typed `KeychainError`.
- Structured context objects encode the primary key (`service` / `account`)
plus any extra fields specific to the failure (`detail`, `reason`,
`timeoutMs`).
- All factories preserve `exactOptionalPropertyTypes` semantics — fields are
omitted rather than set to `undefined`.
## Usage: Basic Example
```ts
import { keychainNotFound } from "@suaveplan/macos-keychain";
const err = keychainNotFound("my-app", "alice");
console.error(err.code); // "KEYCHAIN.NOT_FOUND"
```
## Usage: Advanced Example
```ts
import {
KeychainError,
keychainAccessFailed,
keychainTimeout,
} from "@suaveplan/macos-keychain";
async function callKeychain(): Promise<void> {
try {
await someBridgeCall();
} catch (cause) {
if (cause instanceof KeychainError && cause.code === "KEYCHAIN.TIMEOUT") {
throw keychainTimeout("my-app", "alice", 5_000);
}
throw keychainAccessFailed("my-app", "alice", String(cause));
}
}
declare function someBridgeCall(): Promise<void>;
```
## API Reference
- **`keychainNotFound(service: string, account: string): KeychainError`** —
code `KEYCHAIN.NOT_FOUND`, message key `keychain.error.not_found`,
context `{ service, account }`.
- **`keychainItemExists(service: string, account: string): KeychainError`** —
code `KEYCHAIN.ITEM_EXISTS`.
- **`keychainAccessFailed(service: string, account: string, detail?: string):
KeychainError`** — code `KEYCHAIN.ACCESS_FAILED`. `detail` is appended to
the context only when supplied.
- **`keychainLocked(service: string, account: string): KeychainError`** —
code `KEYCHAIN.LOCKED`.
- **`keychainUnavailable(detail?: string): KeychainError`** — code
`KEYCHAIN.UNAVAILABLE`. No `service`/`account` context because the
service-level failure is not specific to a single item.
- **`keychainPermissionDenied(service: string, account: string):
KeychainError`** — code `KEYCHAIN.PERMISSION_DENIED`.
- **`keychainItemInvalid(service: string, account: string, reason: string):
KeychainError`** — code `KEYCHAIN.ITEM_INVALID`. `reason` is required so
the parser hint is preserved in the structured context.
- **`keychainTimeout(service: string, account: string, timeoutMs: number):
KeychainError`** — code `KEYCHAIN.TIMEOUT`. `timeoutMs` records the
configured deadline, not the elapsed time.
## Implementation Notes
- Factories never read or write process state; they are deterministic on
their inputs.
- `keychainAccessFailed` and `keychainUnavailable` accept optional `detail`
so the error carries a hint without relying on stack inspection. Callers
building a generic fallback (e.g. when `classifyError` returns `null`)
pass the captured stderr.
- The factory layer is intentionally thin — adding logging or tracing here
would mean every test that constructs an error pays for it. Observability
belongs in the bridge, not in the error constructor.

View File

@@ -0,0 +1,93 @@
# macos-keychain.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides runtime Zod schemas that mirror the public type contracts. They are
parsed at every consumer boundary so an invalid `timeoutMs`, missing required
field, or wrong-shape logger surfaces as a precise `ZodError` rather than a
downstream undefined-behaviour failure deep inside the bridge.
The schemas are colocated with the rest of the package surface but are also
exposed via the dedicated `./schemas/macos-keychain.schemas` subpath so a
calling library can re-validate config it received from elsewhere (CLI flags,
config file, environment) before constructing the bridge.
## Features
- `KeychainConfigSchema` — structural shape of bridge construction options.
- `KeychainItemSchema` — full credential record (used by the parser).
- `KeychainResultSchema` — boolean success envelope.
- Inferred types — `KeychainConfigInput`, `KeychainItemInput`,
`KeychainResultInput` — parallel the canonical interfaces with the parsing
defaults applied.
## Usage: Basic Example
```ts
import { KeychainConfigSchema } from "@suaveplan/macos-keychain";
const config = KeychainConfigSchema.parse({
serviceName: "my-app",
timeoutMs: 5_000,
});
```
## Usage: Advanced Example
```ts
import {
KeychainConfigSchema,
KeychainItemSchema,
} from "@suaveplan/macos-keychain";
function loadKeychainConfigFromCli(rawArgs: Record<string, unknown>) {
const result = KeychainConfigSchema.safeParse({
serviceName: rawArgs["service"],
timeoutMs: Number(rawArgs["timeout"]) || undefined,
});
if (!result.success) {
throw new Error(
`Invalid keychain CLI flags: ${result.error.message}`
);
}
return result.data;
}
const stored = KeychainItemSchema.parse({
service: "github",
account: "alice",
password: "ghp_…",
});
void stored;
void loadKeychainConfigFromCli;
```
## API Reference
- **`KeychainConfigSchema`** — Zod object schema. Every field optional;
`timeoutMs` validated as a positive integer; `clock` validated as a
function; `logger` validated as an object exposing the four `LoggerLike`
methods.
- **`KeychainItemSchema`** — Zod object schema requiring non-empty `service`
and `account` strings, capping `password` at 1 MiB, and accepting optional
`label`, `comment`, `createdAt`, `modifiedAt`.
- **`KeychainResultSchema`** — Zod object schema with required `success`
boolean and optional `error` string.
- **Inferred types** — re-exported via `z.infer<typeof Schema>` for callers
that want a single source of truth for both runtime and compile-time
validation.
## Implementation Notes
- Schemas use `z.custom` for `clock` and `logger` because they are structural
function/object shapes. Zod's `.parse` would otherwise narrow them to
`unknown` without a type assertion.
- The `password` upper bound exists because the macOS `security` CLI
enforces a similar limit and a multi-megabyte payload almost certainly
indicates a programming error or accidental binary blob.
- Schemas do not validate the platform itself — the bridge throws
`KEYCHAIN.UNAVAILABLE` at runtime when invoked off macOS, but constructing
the schema and parsing inputs remains valid on any platform.

View File

@@ -0,0 +1,114 @@
# macos-keychain.types
## Purpose
Defines every public type contract exposed by `@suaveplan/macos-keychain`. The
package wraps the macOS `security` command-line tool to perform generic-password
CRUD against the system Keychain; these types describe both the values that
flow into the bridge (configuration, item records) and the runner-neutral
abstraction over the underlying child-process surface (`CommandExecutor`).
Keeping the types in one file means the public surface is immediately auditable
and it gives test code, schemas, and the bridge implementation a single source
of truth. The shapes are deliberately minimal — every field that is not
strictly required is optional, allowing callers to opt in incrementally.
## Features
- `CommandExecutor` — pluggable async function: `(command, args, options) =>
Promise<{ stdout, stderr, exitCode }>`.
- `CommandExecutorOptions` — per-call abort signal forwarded to the child
process.
- `CommandExecutorResult` — captured stdout/stderr/exit-code triple.
- `KeychainConfig` — bridge construction options, including injectable `Clock`
and `LoggerLike` from `@suaveplan/types`.
- `KeychainItem` — credential record returned by `findItem`/`listItems`.
- `KeychainResult` — boolean success envelope returned by mutating methods.
- `KeychainCallOptions` — per-call option bag carrying the `signal` parameter.
## Usage: Basic Example
```ts
import type {
CommandExecutor,
KeychainConfig,
KeychainItem,
} from "@suaveplan/macos-keychain/types/macos-keychain.types";
const config: KeychainConfig = { timeoutMs: 5_000 };
const stubRunner: CommandExecutor = async () => ({
stdout: "",
stderr: 'password: "hunter2"',
exitCode: 0,
});
void stubRunner;
void config;
```
## Usage: Advanced Example
```ts
import type {
CommandExecutor,
KeychainConfig,
KeychainCallOptions,
} from "@suaveplan/macos-keychain";
const controller = new AbortController();
const callOptions: KeychainCallOptions = { signal: controller.signal };
const tracingRunner: CommandExecutor = async (cmd, args, options) => {
const child = await spawnHelper(cmd, args, options);
return { stdout: child.stdout, stderr: child.stderr, exitCode: child.code };
};
const config: KeychainConfig = {
timeoutMs: 30_000,
clock: () => Date.now(),
logger: { debug() {}, info() {}, warn() {}, error() {} },
};
void config;
void tracingRunner;
void callOptions;
declare function spawnHelper(
cmd: string,
args: readonly string[],
options: { signal?: AbortSignal } | undefined
): Promise<{ stdout: string; stderr: string; code: number }>;
```
## API Reference
- **`CommandExecutor`** — `(command: string, args: readonly string[],
options?: CommandExecutorOptions) => Promise<CommandExecutorResult>`. The
bridge's only escape hatch into the OS. Implementations MUST pass arguments
as an array.
- **`CommandExecutorOptions.signal`** — optional `AbortSignal` propagated to
the child.
- **`CommandExecutorResult`** — `{ stdout: string; stderr: string; exitCode:
number }`.
- **`KeychainConfig`** — `{ serviceName?, accessGroup?, timeoutMs?, clock?,
logger? }`. All optional. `timeoutMs` defaults to `10_000`; `clock` defaults
to `() => Date.now()`.
- **`KeychainItem`** — `{ service, account, password, label?, comment?,
createdAt?, modifiedAt? }`. `password` is plaintext and the caller's
responsibility once returned.
- **`KeychainResult`** — `{ success: boolean; error?: string }`.
- **`KeychainCallOptions`** — `{ signal?: AbortSignal }`.
## Implementation Notes
- The default `CommandExecutor` (constructed inside the bridge when none is
injected) uses `Bun.spawn` if the global `Bun` is present, otherwise
`node:child_process` `spawnFile`-style file launch. Neither path goes through
a shell, which is why all argument types are arrays of strings — passing one
composite string would defeat the safety property.
- `Clock` is just `() => number`; tests can pass the `FakeClock` from
`@suaveplan/testing` to advance the deadline deterministically.
- `LoggerLike` is the structural contract from `@suaveplan/types`; loggers
produced by `@suaveplan/logger` and any conforming custom logger work
interchangeably.
- The interfaces are intentionally `readonly` to discourage in-place mutation
by callers who might otherwise alter records returned from `findItem`.