fix(config): trust the invoking project, and stop leaking what it must not decide

Review found five real defects in the configuration-source work, all confirmed
against the code rather than argued:

1. The note claimed --config outranks settings.yaml. It does not: the settings
   seam registers a plugin's cordis entry config as the `base` layer and the
   user section layers over it, and the seam cannot tell a shipped value from a
   --config one. The note now states shipped reality and names --config-replace
   as the lever for a deployment that must win. Separately, a literal `apiKey`
   in settings outranked both the environment and .credentials.yaml — the field
   is removed, so configuration carries a reference and nothing else.
2. DEEPSEEK_SEARCH_BASE_URL was functionally deleted: the shipped inline went
   away without the provider learning to read it. It now resolves from the
   environment snapshot, as the README always claimed.
3. The bootstrap deny list missed the interpreter start-up hooks. BASH_ENV is
   the sharpest: `bash -c` sources it on every bash tool call, so a project
   .env could run a file of its choosing before every command. The list now
   covers BASH_ENV and its per-language siblings, the Git hook commands, and
   the remaining preload and CA variables, organised by what a variable does
   rather than which runtime owns it.
4. YAML parse errors quoted the offending source line — which in a credentials
   document is the secret — into boot stderr and the watcher's logger. Only the
   error code and position are reported now, in credentials-local and
   settings-local alike, pinned by a test that asserts the secret is absent.
5. 0600 governed only files the harness wrote. A hand-created 0644 document was
   read normally. POSIX now checks the mode before reading contents, at boot
   and on every reload; Windows has no mode to inspect and is skipped rather
   than faked.

The project a session is launched in is trusted by default, with no prompt and
no stored trust record: it may supply its own endpoint, ordinary variables, and
a key ranked below the managed store. Trust stops at the harness itself — a
discovered file still cannot set DSH_PERMISSION_MODE, PATH, BASH_ENV, or the
rest, because those take effect with no user action, before any turn, outside
the permission policy and the sandbox.
This commit is contained in:
Yichen Jiang
2026-08-04 17:16:11 +08:00
parent 0512b12714
commit 8c2970e70e
31 changed files with 366 additions and 207 deletions

View File

@@ -3,9 +3,10 @@
* against the environment by how much each layer is trusted:
*
* ```text
* inherited process environment (read-only, wins)
* > $DSH_HOME/.credentials.yaml (provider-managed, writable)
* > $DSH_HOME/.env (read-only fallback)
* inherited process environment (read-only, wins)
* > $DSH_HOME/.credentials.yaml (provider-managed, writable)
* > <invocation cwd>/.env (read-only fallback)
* > $DSH_HOME/.env (read-only fallback)
* ```
*
* The inherited environment wins because `DEEPSEEK_API_KEY=… dsh`, a CI
@@ -15,10 +16,10 @@
* web page or TUI writes takes effect immediately even when an older key sits
* in the user's `.env`.
*
* The invoking directory's `.env` supplies no credential at all. A project
* directory can be written by the model, and a substituted key would send
* every request — prompts included — through an account someone else reads;
* that decision belongs to the launching shell, not to a discovered file.
* The invoking project may supply a key, because the product trusts the
* project it is launched in. It ranks below the managed store, so a key stored
* through the web page or TUI is never displaced by one a checkout happens to
* carry.
*
* The file is the provider-managed writable source: every write re-reads the
* document under a cross-process writer lock before patching only its own key
@@ -37,7 +38,7 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { watch as chokidarWatch } from 'chokidar'
import { mkdir, readFile } from 'node:fs/promises'
import { mkdir, readFile, stat } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { Document, parseDocument } from 'yaml'
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
@@ -83,11 +84,56 @@ export function resolveSpec(config: Config): ResolvedSpec {
}
}
/** Permission bits outside the owner; a credentials document must have none of them. */
const GROUP_OTHER_BITS = 0o077
/**
* Reject a credentials document other OS users can read, before its contents
* are read at all. The provider creates and replaces the file at `0600`, but a
* hand-written or externally generated one carries whatever umask produced it,
* and silently serving secrets out of a world-readable file would make the
* mode the provider promises meaningless.
*
* POSIX only: Windows has no mode to inspect — its ACLs are not expressible
* here — so the check is skipped rather than faked, and the file's protection
* there is whatever the create and replace APIs express.
* @param filename - absolute path of the document.
* @throws when the file exists with group or other permission bits set.
*/
async function assertOwnerOnly(filename: string): Promise<void> {
if (process.platform === 'win32') return
let mode: number
try {
mode = (await stat(filename)).mode
} catch (error) {
if (!isENOENT(error)) throw error
return
}
const offending = mode & GROUP_OTHER_BITS
if (offending === 0) return
throw new Error(
`credentials-local: ${filename} is readable beyond its owner (mode ${(mode & 0o777).toString(8)});`
+ ` run "chmod 600 ${filename}" before starting again`,
)
}
/** Whether a filesystem error means absence; every non-ENOENT failure must surface. */
function isENOENT(error: unknown): boolean {
return (error as NodeJS.ErrnoException | null)?.code === 'ENOENT'
}
/**
* Describe one YAML parse failure without quoting the source. The parser's own
* message embeds the offending line, which here holds a secret.
* @param error - the parser's error.
* @returns the error code with its line and column.
*/
function describeYamlError(error: { code?: string; linePos?: [{ line: number; col: number }, ...unknown[]] }): string {
const at = error.linePos?.[0]
const where = at === undefined ? '' : ` at line ${String(at.line)}, column ${String(at.col)}`
return `${error.code ?? 'YAML_ERROR'}${where}`
}
/**
* Parse one credentials document into its entries. The document is a strict
* mapping of {@link CredentialRef} to non-empty string: a non-mapping root, a
@@ -101,10 +147,15 @@ function isENOENT(error: unknown): boolean {
* @returns the parsed entries, keyed by reference.
*/
export function parseCredentialsDocument(text: string, filename: string): Map<string, string> {
// `prettyErrors` is on only for `linePos`; `error.message` is never used,
// because the parser quotes the offending source line and in this document
// that line is a secret. Only the code and position leave this function, and
// the same rule governs every other diagnostic here — a key name is safe to
// print, a value is not.
const document = parseDocument(text, { prettyErrors: true, uniqueKeys: true })
if (document.errors.length > 0) {
throw new Error(`credentials-local: invalid document at ${filename}: ${
document.errors.map(error => error.message).join('; ')}`)
document.errors.map(describeYamlError).join('; ')}`)
}
const root: unknown = document.toJS() ?? {}
if (typeof root !== 'object' || root === null || Array.isArray(root)) {
@@ -116,6 +167,8 @@ export function parseCredentialsDocument(text: string, filename: string): Map<st
// is exactly the constraint a stored reference must satisfy to be
// addressable through the seam.
credentialRef(key)
// The key name is quoted, never the value: a wrong-typed entry is still a
// secret the user meant to store.
if (typeof value !== 'string') {
throw new TypeError(`credentials-local: the value for "${key}" in ${filename} must be a string`)
}
@@ -194,9 +247,13 @@ export class CredentialsLocal extends Credentials {
return entry !== undefined && entry.value.length > 0 ? entry.value : undefined
}
/** The user `.env` fallback for a reference — below the managed store, never above it. */
private userEnvFallback(ref: CredentialRef): EnvironmentEntry | undefined {
const entry = environmentOf(this.ctx).getFrom(ref, ['user-env'])
/**
* The `.env` fallback for a reference — below the managed store, never above
* it. The invoking project ranks over the user's home file, matching the
* environment layering: the more specific location wins.
*/
private dotenvFallback(ref: CredentialRef): EnvironmentEntry | undefined {
const entry = environmentOf(this.ctx).getFrom(ref, ['project-env', 'user-env'])
return entry !== undefined && entry.value.length > 0 ? entry : undefined
}
@@ -249,8 +306,8 @@ export class CredentialsLocal extends Credentials {
if (inherited !== undefined) return Promise.resolve({ value: inherited, source: 'env' })
const stored = this.values.get(ref)
if (stored !== undefined) return Promise.resolve({ value: stored, source: 'file' })
const fallback = this.userEnvFallback(ref)
if (fallback !== undefined) return Promise.resolve({ value: fallback.value, source: 'user-env' })
const fallback = this.dotenvFallback(ref)
if (fallback !== undefined) return Promise.resolve({ value: fallback.value, source: fallback.source })
return Promise.resolve(undefined)
}
@@ -263,9 +320,8 @@ export class CredentialsLocal extends Credentials {
}
const stored = this.values.get(ref)
if (stored !== undefined) return Promise.resolve({ configured: true, source: 'file', writable: true })
if (this.userEnvFallback(ref) !== undefined) {
return Promise.resolve({ configured: true, source: 'user-env', writable: true })
}
const fallback = this.dotenvFallback(ref)
if (fallback !== undefined) return Promise.resolve({ configured: true, source: fallback.source, writable: true })
return Promise.resolve({ configured: false, writable: true })
}
@@ -361,6 +417,7 @@ export class CredentialsLocal extends Credentials {
* cannot be trusted must never be treated as "no credentials stored".
*/
private async loadInitial(): Promise<void> {
await assertOwnerOnly(this.spec.filename)
let text: string
try {
text = await readFile(this.spec.filename, 'utf8')
@@ -401,6 +458,9 @@ export class CredentialsLocal extends Credentials {
* overwriting a document it could not understand.
*/
private async reconcileFromDisk(): Promise<void> {
// Re-checked on every reload and before every write: an external editor or
// a restored backup can loosen the mode after boot.
await assertOwnerOnly(this.spec.filename)
let text: string | undefined
try {
text = await readFile(this.spec.filename, 'utf8')

View File

@@ -8,6 +8,11 @@ import { createEnvironmentSnapshot, DSH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal, resolveSpec } from '../src/index.ts'
/** Credential documents are seeded owner-only, exactly as the provider creates them. */
function writeCredentials(file: string, text: string): Promise<void> {
return writeFile(file, text, { mode: 0o600 })
}
const KEY = credentialRef('DSH_CRED_TEST')
const OTHER = credentialRef('DSH_CRED_OTHER')
@@ -65,7 +70,7 @@ describe('layering and reads', () => {
it('serves file entries alongside comments and quoted values', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, '# notes\nDSH_CRED_TEST: plain\nDSH_CRED_OTHER: "with space"\n')
await writeCredentials(path, '# notes\nDSH_CRED_TEST: plain\nDSH_CRED_OTHER: "with space"\n')
const ctx = await boot({ path, watch: false })
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'plain', source: 'file' })
expect(await ctx.credentials.resolve(OTHER)).toEqual({ value: 'with space', source: 'file' })
@@ -75,7 +80,7 @@ describe('layering and reads', () => {
it('lets a non-empty process environment win read-only over the file', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: from-file\n')
await writeCredentials(path, 'DSH_CRED_TEST: from-file\n')
const ctx = await boot({ path, watch: false })
vi.stubEnv('DSH_CRED_TEST', 'from-env')
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'from-env', source: 'env' })
@@ -85,7 +90,7 @@ describe('layering and reads', () => {
it('treats an empty environment value as absent, falling through to the file', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: stored\n')
await writeCredentials(path, 'DSH_CRED_TEST: stored\n')
const ctx = await boot({ path, watch: false })
vi.stubEnv('DSH_CRED_TEST', '')
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'stored', source: 'file' })
@@ -119,7 +124,7 @@ describe('layer ladder', () => {
it('lets the stored value beat the user .env, so a UI write takes effect immediately', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: stored\n')
await writeCredentials(path, 'DSH_CRED_TEST: stored\n')
const ctx = await bootLayered(path, [
{ source: 'process', values: {} },
{ source: 'user-env', path: '/home/.dsh/.env', values: { DSH_CRED_TEST: 'older-user-env' } },
@@ -143,22 +148,41 @@ describe('layer ladder', () => {
expect(await ctx.credentials.describe(KEY)).toEqual({ configured: true, source: 'user-env', writable: true })
})
it('ignores the invoking directory .env entirely', async () => {
it('serves the invoking project .env over the user one, but never over the store', async () => {
const dir = await tempDir()
const ctx = await bootLayered(join(dir, '.credentials.yaml'), [
{ source: 'process', values: {} },
{ source: 'project-env', path: '/work/.env', values: { DSH_CRED_TEST: 'from-project' } },
])
// A project directory can be written by the model, and a substituted key
// would route every request through an account someone else reads.
expect(await ctx.credentials.resolve(KEY)).toBeUndefined()
expect(await ctx.credentials.describe(KEY)).toEqual({ configured: false, writable: true })
const path = join(dir, '.credentials.yaml')
// The product trusts the project it is launched in, so a checkout may
// carry its own key — ranked above the user's home file (more specific
// wins) and below the managed store, which a stored key must never lose to.
const layers = [
{ source: 'process' as const, values: {} },
{ source: 'project-env' as const, path: '/work/.env', values: { DSH_CRED_TEST: 'from-project' } },
{ source: 'user-env' as const, path: '/home/.dsh/.env', values: { DSH_CRED_TEST: 'from-user' } },
]
const bare = await bootLayered(path, layers)
expect(await bare.credentials.resolve(KEY)).toEqual({ value: 'from-project', source: 'project-env' })
expect(await bare.credentials.describe(KEY)).toEqual({ configured: true, source: 'project-env', writable: true })
await writeCredentials(path, 'DSH_CRED_TEST: stored\n')
const stored = await bootLayered(path, layers)
expect(await stored.credentials.resolve(KEY)).toEqual({ value: 'stored', source: 'file' })
})
it('refuses a document other OS users can read', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: leaked\n', { mode: 0o644 })
const ctx = new Context()
// Before the contents are read at all: serving secrets out of a
// world-readable file would make the 0600 the provider writes meaningless.
await expect(ctx.plugin(CredentialsLocal, { path, watch: false }))
.rejects.toThrow(/readable beyond its owner \(mode 644\)/)
})
it('lets only the inherited environment shadow the store, read-only', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: stored\n')
await writeCredentials(path, 'DSH_CRED_TEST: stored\n')
const ctx = await bootLayered(path, [
{ source: 'process', values: { DSH_CRED_TEST: 'from-shell' } },
{ source: 'user-env', path: '/home/.dsh/.env', values: { DSH_CRED_TEST: 'from-user-env' } },
@@ -184,15 +208,36 @@ describe('document validation', () => {
])('fails boot on %s', async (_case, text, message) => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, text)
await writeCredentials(path, text)
const ctx = new Context()
await expect(ctx.plugin(CredentialsLocal, { path, watch: false })).rejects.toThrow(message)
})
it('never puts a credential value in a diagnostic', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
const secret = 'sk-live-DO-NOT-LOG-abcdef123456'
// The yaml parser's own message quotes the offending source line, which in
// this document is the secret itself. Boot stderr and the watcher's logger
// both receive whatever this throws.
await writeCredentials(path, `DSH_CRED_TEST: "${secret}\n`)
let failure: unknown
try {
await new Context().plugin(CredentialsLocal, { path, watch: false })
} catch (error) {
failure = error
}
expect(String(failure)).toMatch(/invalid document/)
// The position survives; the line's contents do not.
expect(String(failure)).toMatch(/line 2, column 1/)
expect(String(failure)).not.toContain(secret)
expect((failure as Error).stack ?? '').not.toContain(secret)
})
it('reads an empty document as an empty store', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, '# nothing stored yet\n')
await writeCredentials(path, '# nothing stored yet\n')
const ctx = await boot({ path, watch: false })
expect(await ctx.credentials.resolve(KEY)).toBeUndefined()
})
@@ -214,7 +259,7 @@ describe('document writes', () => {
it('patches one entry, preserving comments and every untouched entry', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, '# deployment notes\nDSH_CRED_OTHER: keep\n\n# the one under edit\nDSH_CRED_TEST: old\n')
await writeCredentials(path, '# deployment notes\nDSH_CRED_OTHER: keep\n\n# the one under edit\nDSH_CRED_TEST: old\n')
const ctx = await boot({ path, watch: false })
await ctx.credentials.set(KEY, 'new value!')
expect(await readFile(path, 'utf8')).toBe(
@@ -242,7 +287,7 @@ describe('document writes', () => {
// Comments above an entry are that entry's annotation and go with it when
// it is removed — including anything above the document's first entry.
// Every other entry keeps its own comments.
await writeFile(path, '# about the doomed one\nDSH_CRED_TEST: gone\n# about the survivor\nDSH_CRED_OTHER: stays\n')
await writeCredentials(path, '# about the doomed one\nDSH_CRED_TEST: gone\n# about the survivor\nDSH_CRED_OTHER: stays\n')
const ctx = await boot({ path, watch: false })
const seen = updates(ctx)
await ctx.credentials.unset(KEY)
@@ -254,7 +299,7 @@ describe('document writes', () => {
it('rejects empty values and writes the environment would shadow', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: stored\n')
await writeCredentials(path, 'DSH_CRED_TEST: stored\n')
const ctx = await boot({ path, watch: false })
await expect(ctx.credentials.set(KEY, '')).rejects.toThrow(/empty value/)
@@ -267,7 +312,7 @@ describe('document writes', () => {
it('leaves an empty mapping after unsetting the only entry', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: only\n')
await writeCredentials(path, 'DSH_CRED_TEST: only\n')
const ctx = await boot({ path, watch: false })
await ctx.credentials.unset(KEY)
expect(await readFile(path, 'utf8')).toBe('{}\n')
@@ -282,7 +327,7 @@ describe('document writes', () => {
const ctx = await boot({ path, watch: false })
// An external editor left the document unparsable: the read-modify-write
// must refuse rather than overwrite content it cannot understand.
await writeFile(path, 'DSH_CRED_TEST: "unterminated\n')
await writeCredentials(path, 'DSH_CRED_TEST: "unterminated\n')
await expect(ctx.credentials.set(OTHER, 'lands')).rejects.toThrow(/invalid document/)
})
@@ -326,17 +371,17 @@ describe('real hot reload', () => {
const path = join(dir, '.credentials.yaml')
// Watching starts on an existing document: creation racing watcher setup
// is a chokidar readiness gap, not the reload contract under test.
await writeFile(path, 'DSH_CRED_TEST: boot\n')
await writeCredentials(path, 'DSH_CRED_TEST: boot\n')
const ctx = await boot({ path, debounceMs: 10 })
const seen = updates(ctx)
await writeFile(path, 'DSH_CRED_TEST: live\nDSH_CRED_OTHER: extra\n')
await writeCredentials(path, 'DSH_CRED_TEST: live\nDSH_CRED_OTHER: extra\n')
await vi.waitFor(async () => {
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'live', source: 'file' })
})
// Wholesale replacement: an entry deleted on disk never lingers in memory.
await writeFile(path, 'DSH_CRED_TEST: live\n')
await writeCredentials(path, 'DSH_CRED_TEST: live\n')
await vi.waitFor(async () => {
expect(await ctx.credentials.resolve(OTHER)).toBeUndefined()
})

View File

@@ -10,6 +10,11 @@ import { join } from 'node:path'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal } from '../src/index.ts'
/** Credential documents are seeded owner-only, exactly as the provider creates them. */
function writeCredentials(file: string, text: string): Promise<void> {
return writeFile(file, text, { mode: 0o600 })
}
const ALPHA = credentialRef('DSH_REVIEW_ALPHA')
const BETA = credentialRef('DSH_REVIEW_BETA')
const INNER = credentialRef('DSH_REVIEW_INNER')
@@ -44,7 +49,7 @@ describe('read-modify-write', () => {
await ctx.credentials.set(ALPHA, 'one')
// The external edit has landed on disk but no watcher reported it (watch
// is off — the same blind spot as a debounce window or a missed event).
await writeFile(path, `${ALPHA}: one\n${BETA}: external\n`)
await writeCredentials(path, `${ALPHA}: one\n${BETA}: external\n`)
await ctx.credentials.set(ALPHA, 'two')
const text = await readFile(path, 'utf8')
expect(text).toContain(`${BETA}: external`)
@@ -124,7 +129,7 @@ describe('document editor', () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
const wrapped = `DSH_REVIEW_WRAPPED: |-\n line1\n line2\n${ALPHA}: a\n`
await writeFile(path, wrapped)
await writeCredentials(path, wrapped)
const ctx = await boot({ path, watch: false })
await ctx.credentials.set(ALPHA, 'b')
expect(await readFile(path, 'utf8')).toBe(`DSH_REVIEW_WRAPPED: |-\n line1\n line2\n${ALPHA}: b\n`)

View File

@@ -6,6 +6,11 @@ import { join } from 'node:path'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal } from '../src/index.ts'
/** Credential documents are seeded owner-only, exactly as the provider creates them. */
function writeCredentials(file: string, text: string): Promise<void> {
return writeFile(file, text, { mode: 0o600 })
}
// chokidar is the nondeterministic OS boundary: faking it lets these tests
// drive the event pipeline (error events, races with unreadable files)
// deterministically. Real end-to-end watching stays covered by local.spec.ts.
@@ -80,7 +85,7 @@ describe('watcher pipeline', () => {
instance!.watcher.emit('error', new Error('watch backend failure'))
expect(await ctx.credentials.resolve(KEY)).toBeUndefined()
await writeFile(path, 'DSH_CRED_PIPE: arrived\n')
await writeCredentials(path, 'DSH_CRED_PIPE: arrived\n')
instance!.watcher.emit('all', 'change', path)
await vi.waitFor(async () => {
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'arrived', source: 'file' })
@@ -90,7 +95,7 @@ describe('watcher pipeline', () => {
it('keeps the last good snapshot when the file turns unreadable at runtime', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_PIPE: good\n')
await writeCredentials(path, 'DSH_CRED_PIPE: good\n')
const ctx = await boot({ path, debounceMs: 5 })
await chmod(path, 0o000)
@@ -113,7 +118,7 @@ describe('watcher pipeline', () => {
})
const [instance] = await fakeInstances()
await writeFile(path, 'DSH_CRED_PIPE: first\n')
await writeCredentials(path, 'DSH_CRED_PIPE: first\n')
instance!.watcher.emit('all', 'change', path)
// The snapshot commits before the fan-out, so the value lands even though
// the listener threw out of the refresh.
@@ -122,7 +127,7 @@ describe('watcher pipeline', () => {
})
arm = false
await writeFile(path, 'DSH_CRED_PIPE: second\n')
await writeCredentials(path, 'DSH_CRED_PIPE: second\n')
instance!.watcher.emit('all', 'change', path)
await vi.waitFor(async () => {
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'second', source: 'file' })
@@ -132,7 +137,7 @@ describe('watcher pipeline', () => {
it('quiesces the refresh pipeline before dispose completes', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_PIPE: initial\n')
await writeCredentials(path, 'DSH_CRED_PIPE: initial\n')
const ctx = new Context()
const fiber = ctx.plugin(CredentialsLocal, { path, debounceMs: 5 })
await fiber
@@ -142,7 +147,7 @@ describe('watcher pipeline', () => {
if (disposed) postDisposeCommits += 1
})
await writeFile(path, 'DSH_CRED_PIPE: changed\n')
await writeCredentials(path, 'DSH_CRED_PIPE: changed\n')
const [instance] = await fakeInstances()
// Two queued refreshes: dispose interrupts one mid-flight and the other
// before it starts, so both closed guards must hold.
@@ -159,7 +164,7 @@ describe('watcher pipeline', () => {
it('empties the snapshot when the document is deleted and emits the removals', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_PIPE: doomed\n')
await writeCredentials(path, 'DSH_CRED_PIPE: doomed\n')
const ctx = await boot({ path, debounceMs: 5 })
const seen: string[] = []
ctx.on('credentials/updated', (ref) => {
@@ -178,7 +183,7 @@ describe('watcher pipeline', () => {
it('keeps the last good snapshot when an external edit makes the document invalid', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_PIPE: a\n')
await writeCredentials(path, 'DSH_CRED_PIPE: a\n')
const ctx = await boot({ path, debounceMs: 5 })
const seen: string[] = []
ctx.on('credentials/updated', (ref) => {
@@ -189,7 +194,7 @@ describe('watcher pipeline', () => {
// this document holds nothing but credentials. A live reload must warn
// and keep serving the last good snapshot rather than take the process
// down or silently drop the entry it could not validate.
await writeFile(path, 'BAD-KEY: 2\nDSH_CRED_PIPE: b\n')
await writeCredentials(path, 'BAD-KEY: 2\nDSH_CRED_PIPE: b\n')
const [instance] = await fakeInstances()
instance!.watcher.emit('all', 'change', path)
await new Promise(resolve => setTimeout(resolve, 50))
@@ -197,7 +202,7 @@ describe('watcher pipeline', () => {
expect(seen).toEqual([])
// Repairing the document resumes publishing.
await writeFile(path, 'DSH_CRED_PIPE: b\n')
await writeCredentials(path, 'DSH_CRED_PIPE: b\n')
instance!.watcher.emit('all', 'change', path)
await vi.waitFor(async () => {
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'b', source: 'file' })
@@ -218,11 +223,11 @@ describe('watcher pipeline', () => {
it('reconciles at watcher ready so a change during setup is not missed', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, `${KEY}: a\n`)
await writeCredentials(path, `${KEY}: a\n`)
const ctx = await boot({ path, debounceMs: 5 })
// Written after the initial load but before the watcher became active:
// no 'all' event will ever fire for it.
await writeFile(path, `${KEY}: written-before-ready\n`)
await writeCredentials(path, `${KEY}: written-before-ready\n`)
const [instance] = await fakeInstances()
instance!.watcher.emit('ready')
await vi.waitFor(async () => {