Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
# Tasks: enterprise-macos-keychain
**Change ID**: enterprise-macos-keychain
**Package**: `@suaveplan/macos-keychain`
**Target path**: `universal/auth/macos-keychain`
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/crypto`
---
## 1. Scaffold
- [x] Create directory `universal/auth/macos-keychain/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
- [x] Write `package.json` at version `0.1.0` with:
- `"type": "module"`, `"sideEffects": false`
- `exports` map with top-level `"."` plus subpaths: `"./core/keychain-bridge/keychain-bridge"`, `"./core/error-classifier/error-classifier"`, `"./core/output-parser/output-parser"`, `"./errors/keychain-errors"`, `"./schemas/macos-keychain.schemas"`, `"./types/macos-keychain.types"`
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/crypto workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
---
## 2. Types and errors
### 2a. Types (`src/types/macos-keychain.types.ts` + `.md`)
- [x] Define all public interfaces: `KeychainConfig`, `KeychainItem`, `KeychainResult`, `CommandExecutor`.
- `CommandExecutor` signature: `(command: string, args: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`.
- The default `CommandExecutor` implementation uses `Bun.spawn` (Bun runtime) or `node:child_process` `execFile` (not `exec` — shell injection prevention); all arguments are passed as an array, never interpolated into a shell string.
- [x] Add genesis-only optional fields: `clock?: Clock` and `logger?: LoggerLike` on `KeychainConfig`.
- [x] Write `macos-keychain.types.md` (≥200 words, API reference for every type, security note on `CommandExecutor` using `execFile`/`Bun.spawn` not `exec`).
### 2b. Error codes (`src/error-codes.ts`)
- [x] Define `KeychainErrorCode` union — eight codes: `KEYCHAIN_NOT_FOUND`, `KEYCHAIN_ITEM_EXISTS`, `KEYCHAIN_ACCESS_FAILED`, `KEYCHAIN_LOCKED`, `KEYCHAIN_UNAVAILABLE`, `KEYCHAIN_PERMISSION_DENIED`, `KEYCHAIN_ITEM_INVALID`, `KEYCHAIN_TIMEOUT` (genesis addition).
- [x] Define `KEYCHAIN_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code.
- [x] Export `registerKeychainErrorCodes()` as idempotent eager side-effect; call it at file bottom.
- [x] Export `KeychainError extends SuaveplanError<KeychainErrorCode>`.
### 2c. Zod schemas (`src/schemas/macos-keychain.schemas.ts` + `.md`)
- [x] Write Zod schemas: `KeychainConfigSchema`, `KeychainItemSchema`, `KeychainResultSchema`.
- [x] Write `macos-keychain.schemas.md`.
- [x] Write `macos-keychain.schemas.test.ts` — parse valid/invalid for each schema.
---
## 3. Constants (`src/constants/index.ts`)
- [x] Export:
- `SECURITY_BINARY = "/usr/bin/security"` — path to the macOS security CLI.
- `DEFAULT_TIMEOUT_MS = 10_000`.
- `EXIT_ITEM_NOT_FOUND = 44` — `errSecItemNotFound`.
- `EXIT_DUPLICATE_ITEM = 45` — `errSecDuplicateItem`.
- `EXIT_AUTH_FAILED = 36` — `errAuthFailed`.
- `EXIT_LOCKED_PATTERNS: string[]` — stderr substrings that indicate a locked keychain.
---
## 4. Error classifier (`src/core/error-classifier/`)
- [x] Implement `classifyError(exitCode, stderr, service, account): KeychainError | null`:
- Map `EXIT_ITEM_NOT_FOUND` + "could not be found" / "no such keychain" stderr patterns → `keychainNotFound`.
- Map `EXIT_DUPLICATE_ITEM` + "duplicate" / "already exists" → `keychainItemExists`.
- Map `EXIT_AUTH_FAILED` + "auth" patterns → `keychainAccessFailed`.
- Map locked-keychain stderr patterns → `keychainLocked`.
- Map "permission" / "denied" / "not authorized" → `keychainPermissionDenied`.
- Map "security: SecKeychainItemRef" invalid type → `keychainItemInvalid`.
- Return `null` for unrecognized combinations (caller wraps in a generic `KEYCHAIN_ACCESS_FAILED`).
- [x] Write `error-classifier.test.ts` — each known exit code + stderr combo, unknown combo returns null, stderr precedence over exit code when ambiguous. Each `it` has `{ timeout: 5000 }`.
- [x] Write `error-classifier.md`.
---
## 5. Error factories (`src/errors/keychain-errors.ts`)
- [x] Implement named factory helpers: `keychainNotFound`, `keychainItemExists`, `keychainAccessFailed`, `keychainLocked`, `keychainUnavailable`, `keychainPermissionDenied`, `keychainItemInvalid`, `keychainTimeout`.
- [x] Each factory returns a `KeychainError` with the corresponding code, a human-readable message, and a structured context object `{ service, account }` (where applicable).
- [x] Write `keychain-errors.test.ts` — construction of each factory, correct code, message non-empty, context fields present. Each `it` has `{ timeout: 5000 }`.
- [x] Write `keychain-errors.md`.
---
## 6. Output parser (`src/core/output-parser/`)
- [x] Implement `parseKeychainItem(stdout: string, service: string, account: string): KeychainItem`:
- Extract `password` from `security find-generic-password -g` output (appears after `password:` line, possibly base-64 encoded for binary passwords).
- Extract optional `label`, `comment`, `createdAt`, `modifiedAt` from structured output lines.
- Return `KeychainItem` with all available fields.
- Throw `KeychainError(KEYCHAIN_ITEM_INVALID)` if the output cannot be parsed.
- [x] Implement `parseKeychainList(stdout: string): Array<{ service: string; account: string }>`:
- Parse `security dump-keychain` output for service + account pairs.
- [x] Write `output-parser.test.ts` — valid stdout for each supported format, missing password line, list parsing, malformed output triggers `KEYCHAIN_ITEM_INVALID`. Each `it` has `{ timeout: 5000 }`.
- [x] Write `output-parser.md`.
---
## 7. MacOSKeychainBridge (`src/core/keychain-bridge/`)
- [x] Implement `MacOSKeychainBridge`:
- Constructor: `constructor(config?: KeychainConfig, executor?: CommandExecutor)`.
- If `executor` is not provided, use a default `CommandExecutor` that invokes `SECURITY_BINARY` via `Bun.spawn` (on Bun runtime) or `node:child_process` `execFile` (not `exec`) — arguments are always passed as an array to prevent shell injection.
- Validate `config` with `KeychainConfigSchema`; register error codes eagerly.
- `addItem(item)`:
- Build args array: `["add-generic-password", "-s", service, "-a", account, "-w", password, ...]` — never interpolate into a shell string.
- Execute via `executor`; wrap with `AbortSignal.timeout(timeoutMs)` and translate `TimeoutError` to `keychainTimeout`.
- On `exitCode !== 0`, classify via `classifyError`; throw the result (or generic `keychainAccessFailed` for unrecognized codes).
- On duplicate, throw `keychainItemExists`.
- Instrument with `keychain.add` span and `keychain.ops_total` counter.
- `findItem(service, account)`:
- Build args array: `["find-generic-password", "-s", service, "-a", account, "-g"]`.
- Parse stdout via `parseKeychainItem`.
- `zeroize` the internal temporary stdout buffer after extraction.
- Instrument with `keychain.find` span and counter.
- `updateItem(service, account, newPassword)`:
- Delete existing item, then re-add with new password (macOS has no native in-place update for generic passwords).
- Instrument with `keychain.update` span and counter.
- `deleteItem(service, account)`:
- Build args array: `["delete-generic-password", "-s", service, "-a", account]`.
- Instrument with `keychain.delete` span and counter.
- `listItems(service?)`:
- Build args for `find-generic-password` or `dump-keychain`.
- Parse via `parseKeychainList`.
- Instrument with `keychain.list` span and histogram `keychain.items_returned`.
- `itemExists(service, account)`:
- Calls `findItem`; returns `true` on success, `false` on `KEYCHAIN_NOT_FOUND`, re-throws other errors.
- All methods accept optional `signal?: AbortSignal`; pass to `executor` and reject with `keychainTimeout` on abort.
- Log all operations at `debug` level via `config.logger` (if provided).
- [x] Write `keychain-bridge.test.ts` (≤500 lines):
- Use a stub `CommandExecutor` (plain object literal — no `vi.mock`) that returns preset `{ stdout, stderr, exitCode }` values.
- Test each CRUD operation: success path, each error code classification, AbortSignal cancellation, logger calls, OTel counter increments (in-memory exporter).
- `{ timeout: 5000 }` on each `it`.
- [x] Write `keychain-bridge.md` (≥200 words — architecture overview, security model noting `execFile`/`Bun.spawn` + args-array usage, platform limitations, stub executor guide for tests).
---
## 8. OTel instrumentation (cross-cutting review)
- [x] Confirm all spans, counters, and histograms from `proposal.md §4` are wired.
- [x] Add `tests/otel.test.ts` — in-memory exporter; assert `keychain.ops_total` increments for add/find/update/delete, `keychain.cli_duration_ms` records, `keychain.items_returned` histogram records on list.
---
## 9. Property-based tests (`tests/property.test.ts`)
Use `fast-check`. All test timeouts `{ timeout: 30000 }`.
- [x] **Error classification coverage**: for each documented macOS exit code constant, `classifyError` returns a non-null `KeychainError` with the expected `code`.
- [x] **Error factory idempotency**: calling `keychainNotFound(s, a)` twice returns errors with identical `.code` and `.message`.
- [x] **Round-trip via stub executor**: `addItem` then `findItem` using a deterministic stub that echoes stored data — result password equals input password.
- [x] **Timeout detection**: stub executor that delays beyond `timeoutMs` (AbortSignal + FakeClock) triggers `KEYCHAIN_TIMEOUT` error.
---
## 10. Benchmarks (`src/__benchmarks__/macos-keychain.bench.ts`)
Use `mitata`. All benchmarks use stub `CommandExecutor` (no real keychain access).
- [x] `findItem_stub`: 100 000 `findItem` calls via a stub executor returning a static valid stdout string — measures parse + error-classification overhead.
- [x] `classifyError_10_patterns`: 1 000 000 calls to `classifyError` cycling through 10 exit-code/stderr pairs — pure function overhead.
- [x] Document placeholder numbers in `README.md` under `## Benchmarks`.
---
## 11. Integration tests (`tests/integration.test.ts`)
- [x] Tag all tests with `// @integration` header comment.
- [x] Guard every test with a runtime skip if not on macOS: `if (process.platform !== 'darwin') { it.skip(...) }`.
- [x] Tests: real `addItem`, `findItem`, `updateItem`, `deleteItem`, `listItems`, `itemExists`. Clean up added items in `afterEach`.
- [x] Exclude from coverage threshold in `vitest.config.ts`.
---
## 12. Docs
Per-file `.md` (all ≥200 words):
- [x] `src/types/macos-keychain.types.md`
- [x] `src/core/keychain-bridge/keychain-bridge.md`
- [x] `src/core/error-classifier/error-classifier.md`
- [x] `src/core/output-parser/output-parser.md`
- [x] `src/errors/keychain-errors.md`
- [x] `src/schemas/macos-keychain.schemas.md`
Root README:
- [x] Write `universal/auth/macos-keychain/README.md`: purpose, install, quick-start (add + find + delete), security model (`execFile` array args, password zeroize, access group), stub executor guide for tests, platform requirements (macOS only for real operations), modules table, benchmarks placeholder.
---
## 13. Verify and publish
- [x] Run `bunx turbo lint --filter=@suaveplan/macos-keychain` — zero errors.
- [x] Run `bunx turbo typecheck --filter=@suaveplan/macos-keychain` — zero errors.
- [x] Run `bunx turbo test --filter=@suaveplan/macos-keychain` — all unit + property tests pass (integration skipped on non-macOS).
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/macos-keychain` — 100/100/100/100 on non-integration source files.
- [x] Run `bun run docs:check --filter=@suaveplan/macos-keychain` — zero errors.
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore` in `src/`. Platform-guarded `.skip` in `tests/integration.test.ts` is permissible.
- [x] Confirm `"sideEffects": false` in `package.json`.
- [x] Run `bun scripts/publish.ts --package @suaveplan/macos-keychain`.
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/macos-keychain`.
- [x] Archive: move `openspec/changes/enterprise-macos-keychain/` to `openspec/archive/2026-04-24-enterprise-macos-keychain/` and fold spec delta into `openspec/specs/macos-keychain/spec.md`.