mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Review round three, credentials half. dsh-atomic-write grows the cross-process writer-lock primitive (withFileLock: wx sentinel, bounded backoff, stale takeover via onStaleBreak, deadline failure) plus a dirMode option, and settings-local migrates its private copy to it; both providers now create harness-home directories 0700. credentials-local reuses the reviewed settings-local shape: watcher reloads and line edits share one settled operation chain; every write re-reads the document under the lock and publishes unobserved external entries before editing, so an edit inside the debounce window (or another process's write) can never be overwritten; the watcher's ready signal queues one reconcile closing the startup gap. The line editor is now physical-line aware: continuation lines of a quoted multi-line value are never mistaken for assignments, untouched lines keep their exact bytes (CRLF included), an edited line keeps its own terminator, and appends use the document's dominant ending. A multi-line entry reports writable: false, matching what set() would do. The Credentials base class owns a contained notifyUpdated fan-out: providers publish only after the commit, every listener runs, sync throws and async rejections are logged without failing the committed write, and INVARIANT-coded failures rethrow after the fan-out.
163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
/**
|
|
* Credential seam (`ctx.credentials`). Settings and composition files carry
|
|
* *references* to secrets — environment-variable names — while providers own
|
|
* the actual values and their storage. Consumers resolve a reference once per
|
|
* operation, so a changed credential reaches the next operation without any
|
|
* plugin restart, and configuration surfaces describe a reference without
|
|
* ever seeing its value.
|
|
* @module @deepseek-ai/dsh-credentials
|
|
*/
|
|
|
|
import { Context, Service } from 'cordis'
|
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
|
|
|
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
|
|
export type CredentialRef = Branded<'CredentialRef'>
|
|
|
|
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
|
|
/**
|
|
* Brand a raw string as a {@link CredentialRef}.
|
|
* @param value - candidate reference; a POSIX shell identifier such as `DEEPSEEK_API_KEY`.
|
|
* @returns the branded reference.
|
|
*/
|
|
export function credentialRef(value: string): CredentialRef {
|
|
if (!REF_PATTERN.test(value)) {
|
|
throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`)
|
|
}
|
|
return value as CredentialRef
|
|
}
|
|
|
|
/** One resolved credential value and the source layer that supplied it. */
|
|
export interface ResolvedCredential {
|
|
/** The non-empty secret value. */
|
|
value: string
|
|
/** Provider-defined source layer id (the local provider uses `env` and `file`). */
|
|
source: string
|
|
}
|
|
|
|
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
|
|
export interface CredentialInfo {
|
|
/** Whether {@link Credentials.resolve} would currently return a value. */
|
|
configured: boolean
|
|
/** Source layer currently supplying the value; absent while unconfigured. */
|
|
source?: string
|
|
/** Whether {@link Credentials.set} would currently succeed for this reference. */
|
|
writable: boolean
|
|
}
|
|
|
|
declare module 'cordis' {
|
|
interface Context {
|
|
credentials: Credentials
|
|
}
|
|
|
|
interface Events {
|
|
/**
|
|
* Committed change to a provider-managed credential source: a `set`, an
|
|
* `unset`, or an external edit observed in storage. Ambient
|
|
* process-environment changes are not observable and never emit. Listener
|
|
* failures are contained and logged — a sync throw and an async rejection
|
|
* alike — without changing the committed operation's outcome, except
|
|
* `INVARIANT`-coded failures, which rethrow after every listener ran;
|
|
* that rethrow reaches the emitter only from synchronous listeners, so
|
|
* invariant checks on this event must not be async functions.
|
|
* @param ref - the reference whose stored value changed.
|
|
* @mode emit
|
|
*/
|
|
'credentials/updated'(ref: CredentialRef): void
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abstract credential service. Providers implement the four operations over
|
|
* their source layers; one seam-wide rule binds them all: an empty stored
|
|
* value is absent everywhere — `resolve` skips it, `describe` reports it
|
|
* unconfigured — so a blank never masquerades as a configured secret.
|
|
*/
|
|
export abstract class Credentials extends Service {
|
|
constructor(ctx: Context) {
|
|
super(ctx, 'credentials')
|
|
}
|
|
|
|
/**
|
|
* Resolve one reference to its current value. Resolution is per call:
|
|
* consumers re-resolve at each operation and must not cache across
|
|
* operations — that per-operation read is what makes a changed credential
|
|
* reach the next operation without a restart.
|
|
* @param ref - the reference to resolve.
|
|
* @returns the value and its source, or `undefined` while unconfigured.
|
|
*/
|
|
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
|
|
|
|
/**
|
|
* Describe one reference for configuration surfaces without exposing the
|
|
* value.
|
|
* @param ref - the reference to describe.
|
|
* @returns configured state, supplying source, and writability.
|
|
*/
|
|
abstract describe(ref: CredentialRef): Promise<CredentialInfo>
|
|
|
|
/**
|
|
* Durably store one value in the provider-managed writable source. Rejects
|
|
* while a read-only source shadows the reference — the write would appear
|
|
* to succeed while resolution keeps returning the shadowing value — and
|
|
* rejects an empty value (use {@link unset}).
|
|
* @param ref - the reference to store.
|
|
* @param value - the non-empty secret value.
|
|
*/
|
|
abstract set(ref: CredentialRef, value: string): Promise<void>
|
|
|
|
/**
|
|
* Remove one reference from the provider-managed writable source; removing
|
|
* an absent reference is a no-op. Rejects while a read-only source shadows
|
|
* the reference, like {@link set}.
|
|
* @param ref - the reference to remove.
|
|
*/
|
|
abstract unset(ref: CredentialRef): Promise<void>
|
|
|
|
/* jscpd:ignore-start -- deliberate symmetry with the settings seam's commit
|
|
fan-out: the contained-dispatch shape is the reviewed listener-lifecycle
|
|
contract, and extracting it would couple the two seams' event semantics. */
|
|
/**
|
|
* Fan `credentials/updated` out with contained listener failures: every
|
|
* listener runs, and a sync throw or async rejection is logged without
|
|
* changing the committed operation's outcome — except `INVARIANT`-coded
|
|
* failures, which rethrow after every listener ran (the rethrow reaches the
|
|
* caller only from synchronous listeners, so invariant checks on this event
|
|
* must not be async functions). Providers call this only after the write or
|
|
* reload actually committed, so a broken observer can never make a durable
|
|
* change look failed.
|
|
* @param ref - the reference whose stored value changed.
|
|
*/
|
|
protected notifyUpdated(ref: CredentialRef): void {
|
|
let invariantFailure: unknown
|
|
const args = ['credentials/updated', ref]
|
|
for (const listener of this.ctx.events.dispatch('emit', args) as Array<(...listenerArgs: unknown[]) => unknown>) {
|
|
try {
|
|
const returned = listener(ref)
|
|
if (returned != null && typeof (returned as PromiseLike<unknown>).then === 'function') {
|
|
void Promise.resolve(returned as PromiseLike<unknown>).then(undefined, (error: unknown) => {
|
|
this.warnListenerFailure(ref, error)
|
|
})
|
|
}
|
|
} catch (error) {
|
|
if ((error as { code?: unknown } | null)?.code === 'INVARIANT') {
|
|
invariantFailure ??= error
|
|
continue
|
|
}
|
|
this.warnListenerFailure(ref, error)
|
|
}
|
|
}
|
|
if (invariantFailure !== undefined) throw invariantFailure as Error
|
|
}
|
|
/* jscpd:ignore-end */
|
|
|
|
/** Contained-listener diagnostic shared by the sync and async failure paths. */
|
|
private warnListenerFailure(ref: CredentialRef, error: unknown): void {
|
|
this.ctx.logger.warn('credentials: a credentials/updated listener for "%s" failed', ref)
|
|
this.ctx.logger.warn(error)
|
|
}
|
|
}
|
|
|
|
export default Credentials
|