fix(credentials-local): one operation chain, read-modify-write under the shared writer lock, and a quote-aware line editor

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.
This commit is contained in:
Yichen Jiang
2026-07-30 15:40:09 +08:00
parent 8ff6b23214
commit 90c3118302
7 changed files with 579 additions and 155 deletions
+49 -1
View File
@@ -55,7 +55,12 @@ declare module 'cordis' {
/**
* 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.
* 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
*/
@@ -109,6 +114,49 @@ export abstract class Credentials extends Service {
* @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