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:
71
packages/credentials/credentials/tests/credentials.spec.ts
Normal file
71
packages/credentials/credentials/tests/credentials.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
37
packages/credentials/credentials/tests/invariant.spec.ts
Normal file
37
packages/credentials/credentials/tests/invariant.spec.ts
Normal 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/)
|
||||
})
|
||||
})
|
||||
51
packages/credentials/credentials/tests/memory.ts
Normal file
51
packages/credentials/credentials/tests/memory.ts
Normal 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
|
||||
Reference in New Issue
Block a user