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
- Expiry detection -- checks
ServiceCredentialConfig.expiresAtagainst the current time - Upstream validation -- sends a HEAD request to the service's first domain with injected headers to verify the credential is still accepted
- Status tracking -- maintains an in-memory map of per-service credential status (valid, lastValidated, lastUsed, expired)
Key API
isExpired(service)-- Returnstrueif the service'scredential.expiresAtis in the past. Returnsfalseif noexpiresAtis set (non-expiring credential).validateCredential(service, credentials)-- Makes a HEAD request tohttps://<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 thelastUsedtimestamp (called when credentials are injected into a proxied request).getStatus(serviceId)-- Returns the currentCredentialStatusfor a service, or a default status if not yet tracked.
Types
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:
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
ServiceDefinitionfrom../../types-- providescredential.expiresAt,injection.proxy.domains, andinjection.proxy.headersglobalThis.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 servicelatch:credential-refresh-- triggers validation and returns the result
- Preload in
src/preload/index.ts:window.latch.getCredentialStatus({ serviceId })-- renderer access to credential statuswindow.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-expiredfeedback 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