# Enclave Credential Manager

> Credential lifecycle management for the Latch Enclave. Tracks expiry, validates credentials against upstream services, records usage metadata. Use when working on credential health checks, token rotation, or service authentication status.

- Skill: `latchagent/enclave-credential-manager` (Agent Skill)
- Install (CLI): `npx skillmds@latest add latchagent/enclave-credential-manager`
- Raw SKILL.md: https://api.skillmd.com/api/skills/latchagent/enclave-credential-manager/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: latchagent (https://skillmd.com/u/latchagent)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/latchagent/enclave-credential-manager

---


# Credential Manager

The CredentialManager class lives at `src/main/services/credential-manager.ts` and handles credential lifecycle tracking for enclave services.

## What It Does

The CredentialManager does NOT store credentials (that is SecretStore's job). Instead it tracks metadata about credential health: whether a credential has expired, when it was last validated, and when it was last used for injection.

### Core Responsibilities

1. **Expiry detection** -- checks `ServiceCredentialConfig.expiresAt` against the current time
2. **Upstream validation** -- sends a HEAD request to the service's first domain with injected headers to verify the credential is still accepted
3. **Status tracking** -- maintains an in-memory map of per-service credential status (valid, lastValidated, lastUsed, expired)

## Key API

- `isExpired(service)` -- Returns `true` if the service's `credential.expiresAt` is in the past. Returns `false` if no `expiresAt` is set (non-expiring credential).
- `validateCredential(service, credentials)` -- Makes a HEAD request to `https://<first-domain>/` with headers populated from the service's injection config. Returns `{ valid, status, error? }`. Status codes 200, 404, and 405 are considered valid (the credential works, the endpoint just may not support HEAD).
- `recordValidation(serviceId, valid)` -- Updates the in-memory status map with the validation result and timestamp.
- `recordUsage(serviceId)` -- Updates the `lastUsed` timestamp (called when credentials are injected into a proxied request).
- `getStatus(serviceId)` -- Returns the current `CredentialStatus` for a service, or a default status if not yet tracked.

## Types

```typescript
interface CredentialStatus {
  serviceId: string
  valid: boolean
  lastValidated: string | null  // ISO 8601
  lastUsed: string | null       // ISO 8601
  expired: boolean
}

interface ValidationResult {
  valid: boolean
  status: number | null   // HTTP status code from HEAD request
  error?: string          // network or timeout error message
}
```

## Credential Lifecycle Fields (Phase 5)

The `ServiceCredentialConfig` type in `src/types/index.ts` includes lifecycle fields:

```typescript
interface ServiceCredentialConfig {
  type: 'token' | 'keypair' | 'oauth' | 'env-bundle'
  fields: string[]
  expiresAt?: string             // ISO 8601, null/undefined if non-expiring
  refreshEndpoint?: string       // URL for token refresh (future use)
  rotationPolicy?: 'manual' | 'auto'  // how rotation is handled (future use)
}
```

The `expiresAt` field drives the `isExpired()` check. The `refreshEndpoint` and `rotationPolicy` fields are defined for future token refresh and rotation automation.

## Credential Injection for Validation

The `validateCredential` method builds headers by replacing `${credential.<field>}` placeholders in the service's `injection.proxy.headers` with actual credential values. For example:

```
Header template: "Bearer ${credential.token}"
Credentials: { token: "ghp_abc123" }
Result header: "Bearer ghp_abc123"
```

This mirrors the same injection logic used by the EgressFilter during proxy operation.

## Dependencies

- `ServiceDefinition` from `../../types` -- provides `credential.expiresAt`, `injection.proxy.domains`, and `injection.proxy.headers`
- `globalThis.fetch` -- used for the HEAD validation request (5 second timeout via AbortSignal)

## Integration Points

- **IPC handlers** in `src/main/index.ts`:
  - `latch:credential-status` -- returns expiry/validation status for a service
  - `latch:credential-refresh` -- triggers validation and returns the result
- **Preload** in `src/preload/index.ts`:
  - `window.latch.getCredentialStatus({ serviceId })` -- renderer access to credential status
  - `window.latch.refreshCredential({ serviceId })` -- renderer-initiated credential validation
- **LatchProxy** -- can call `recordUsage()` when injecting credentials into proxied requests
- **Renderer** -- the ServicesPanel can poll credential status to show health indicators
- **Proxy feedback** -- the `credential-expired` feedback type (label: `CRED-EXPIRED`) is used when a credential is detected as expired during proxy operation

## Testing

Tests: `src/main/services/credential-manager.test.ts`
Run: `npx vitest run src/main/services/credential-manager.test.ts`

The test suite covers:
- Expired credential detection (past expiresAt)
- Non-expired credential detection (future expiresAt)
- Missing expiresAt treated as non-expiring
- Successful upstream validation (mocked 200)
- Failed upstream validation (mocked 401)
- Status tracking after recordValidation
- Usage tracking after recordUsage

