feat(credentials): abstract credential seam (ctx.credentials)

References-not-values doctrine: settings carry env-shaped CredentialRefs,
providers own storage. Per-operation resolve, UI-safe describe, fail-loud
set/unset under read-only shadowing, credentials/updated commit event with
a live-service invariant.
This commit is contained in:
Yichen Jiang
2026-07-29 13:03:12 +08:00
parent ba37180946
commit 3a794495ad
12 changed files with 479 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { credentialRef } from '../src/index.ts'
import type { CredentialRef } from '../src/index.ts'
import { MemoryCredentials } from './memory.ts'
const REF = credentialRef('DEEPSEEK_API_KEY')
async function boot(seed: Record<string, string> = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(MemoryCredentials, seed)
return ctx
}
describe('credentialRef', () => {
it('brands POSIX shell identifiers', () => {
expect(credentialRef('DEEPSEEK_API_KEY')).toBe('DEEPSEEK_API_KEY')
expect(credentialRef('_private')).toBe('_private')
expect(credentialRef('lower_case9')).toBe('lower_case9')
})
it('rejects every other shape', () => {
for (const invalid of ['', '9LEADING', 'WITH-DASH', 'WITH SPACE', 'ns:key']) {
expect(() => credentialRef(invalid)).toThrow(TypeError)
}
})
})
describe('the credentials seam through the memory provider', () => {
it('mounts as ctx.credentials and resolves a seeded reference with its source', async () => {
const ctx = await boot({ DEEPSEEK_API_KEY: 'sk-seeded' })
expect(await ctx.credentials.resolve(REF)).toEqual({ value: 'sk-seeded', source: 'memory' })
expect(await ctx.credentials.describe(REF)).toEqual({ configured: true, source: 'memory', writable: true })
})
it('treats an empty stored value as absent everywhere', async () => {
const ctx = await boot({ DEEPSEEK_API_KEY: '' })
expect(await ctx.credentials.resolve(REF)).toBeUndefined()
expect(await ctx.credentials.describe(REF)).toEqual({ configured: false, writable: true })
})
it('stores through set, removes through unset, and emits the committed change', async () => {
const ctx = await boot()
const events: CredentialRef[] = []
ctx.on('credentials/updated', ref => void events.push(ref))
await ctx.credentials.set(REF, 'sk-live')
expect(await ctx.credentials.resolve(REF)).toEqual({ value: 'sk-live', source: 'memory' })
await ctx.credentials.unset(REF)
expect(await ctx.credentials.resolve(REF)).toBeUndefined()
expect(events).toEqual([REF, REF])
})
it('rejects an empty set and keeps an absent unset silent', async () => {
const ctx = await boot()
const events: CredentialRef[] = []
ctx.on('credentials/updated', ref => void events.push(ref))
await expect(ctx.credentials.set(REF, '')).rejects.toThrow(/empty value/)
await ctx.credentials.unset(REF)
expect(events).toEqual([])
})
it('removes the service with its fiber', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(MemoryCredentials)
expect(ctx.get('credentials')).toBeDefined()
await fiber.dispose()
expect(ctx.get('credentials')).toBeUndefined()
})
})

View File

@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import InvariantService from '@deepseek-ai/dsh-invariants'
import { credentialRef } from '../src/index.ts'
import * as CredentialsInvariant from '../src/invariant.ts'
import { MemoryCredentials } from './memory.ts'
const REF = credentialRef('DEEPSEEK_API_KEY')
describe('credentials invariant companion', () => {
it('accepts a committed change emitted by a live service', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(CredentialsInvariant)
await ctx.plugin(MemoryCredentials)
await expect(ctx.credentials.set(REF, 'sk-live')).resolves.toBeUndefined()
})
it('fails an update event emitted without a live service', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(CredentialsInvariant)
expect(() => { ctx.emit('credentials/updated', REF) }).toThrow(/invariant violated by "@deepseek-ai\/dsh-credentials"/)
})
it('reserves the package name against duplicate registration', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(CredentialsInvariant)
expect(() => {
ctx.invariants.register('@deepseek-ai/dsh-credentials', () => {})
}).toThrow(/already registered/)
})
})

View File

@@ -0,0 +1,51 @@
import type { Context } from 'cordis'
import { Credentials } from '../src/index.ts'
import type { CredentialInfo, CredentialRef, ResolvedCredential } from '../src/index.ts'
/**
* In-memory credentials provider for interface and consumer tests: one
* always-writable `memory` source seeded from plugin config.
*/
export class MemoryCredentials extends Credentials {
private readonly store = new Map<string, string>()
constructor(ctx: Context, seed: Record<string, string> = {}) {
super(ctx)
for (const [key, value] of Object.entries(seed)) this.store.set(key, value)
}
override resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined> {
const value = this.store.get(ref)
return Promise.resolve(value === undefined || value.length === 0
? undefined
: { value, source: 'memory' })
}
override describe(ref: CredentialRef): Promise<CredentialInfo> {
const value = this.store.get(ref)
const configured = value !== undefined && value.length > 0
return Promise.resolve({
configured,
...configured ? { source: 'memory' } : {},
writable: true,
})
}
override set(ref: CredentialRef, value: string): Promise<void> {
if (value.length === 0) {
return Promise.reject(new Error('memory credentials: an empty value cannot be stored; use unset'))
}
this.store.set(ref, value)
this.ctx.emit('credentials/updated', ref)
return Promise.resolve()
}
override unset(ref: CredentialRef): Promise<void> {
if (this.store.delete(ref)) {
this.ctx.emit('credentials/updated', ref)
}
return Promise.resolve()
}
}
export default MemoryCredentials