description: "The credential seam for users and maintainers resolving, describing, or storing credentials — reference values and durable records — without putting secret values in configuration."
English | 中文
dsh-credentials keeps secret values out of configuration: you store an API key once and reference it by name (DEEPSEEK_API_KEY) from settings or cordis.yml, and the product supplies the value when a provider request needs it. Beside those references it also keeps durable credential records — per-plugin entries such as an authorization grant or provider environment values — so a plugin holds what it manages for its own ids across restarts. A rotated key takes effect on the very next request — no restart, no configuration edit. Configuration UIs can tell you whether a key or record is set, where it comes from, and whether you can change it, without ever showing a value. Storing an empty value counts as "no key", so a blank can never masquerade as a configured secret; a record's presence is the whole fact, so an entry carrying no value is a deliberate statement, not a blank.
This package is the part of the product that stores and looks up secret values: store a key once, reference it by name everywhere, and read, check, or remove it at any time. It also keeps durable credential records, so a plugin can store, update, and remove the credentials it holds for its own ids. The product's default composition already includes a credential store; a custom composition loads the local store package with a file path.
Use a credential store whenever configuration must stay free of secret values: settings files that are synced, shared, or rendered in a configuration UI, or teams that rotate keys without editing configuration. Use records when a plugin must keep credentials with no single environment variable — an authorization grant from a sign-in flow, or provider environment values — and when a configuration UI should list what a user is authorized for. A configuration UI can show whether a key or record is set, where it comes from, and whether you can change it — never the value itself. If you only need one fixed environment variable, read that variable directly and skip the store.
Load the local store package with a document path:
- name: '@deepseek-ai/dsh-credentials-local'
config:
path: /absolute/path/to/.credentials.yaml
The local store README owns the full configuration surface; the generated configuration catalog is the exhaustive field list.
import type { Context } from '@deepseek-ai/cordis'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
declare const ctx: Context
const ref = credentialRef('DEEPSEEK_API_KEY') // POSIX shell identifier, branded
const hit = await ctx.credentials.resolve(ref) // { value, source } | undefined
const info = await ctx.credentials.describe(ref) // { configured, source?, writable } — never the value
await ctx.credentials.set(ref, 'sk-…') // rejects while a read-only source shadows the ref
await ctx.credentials.unset(ref) // no-op when absent; same shadowing rule
Store a key with set, remove it with unset, check its status with describe, and read the current value with resolve when an operation needs it. describe reports whether the key is set, where it comes from, and whether you can write to it — it never returns the value.
A plugin addresses each record by <scope>/<id> — its own registered name plus an id it chooses, such as a provider route key — and reads, modifies, or removes what it holds:
import type { Context } from '@deepseek-ai/cordis'
import { credentialKey } from '@deepseek-ai/dsh-credentials'
declare const ctx: Context
const key = credentialKey('llm-pi-ai', 'openai-codex') // <owner>/<id>, branded
const hit = await ctx.credentials.readRecord(key) // CredentialRecord | undefined
await ctx.credentials.describeRecord(key) // { configured, kind?, writable } — never the value
await ctx.credentials.listRecords() // [{ key, kind }] — never values
await ctx.credentials.modifyRecord(key, async () => ({ kind: 'grant', payload: { token: '…' } }))
await ctx.credentials.deleteRecord(key) // no-op when absent
modifyRecord is the only write path: it hands your mutation the record as it stands at the moment the write is exclusive, and returning undefined leaves the entry untouched. Records have no empty-value rule — a record carrying neither a key nor environment values states that its owner confirmed ambient authentication — and a configuration UI can enumerate every record to show what you are authorized for and find records a removed plugin left behind.
A settings section or cordis.yml entry names a key instead of containing it — an LLM adapter, for example, takes apiKeyEnv:
apiKeyEnv: DEEPSEEK_API_KEY
Requests that need the key use its current stored value, so rotating the key takes effect on the very next request — no restart and no configuration edit.
DEEPSEEK_API_KEY=… dsh (or a CI secret, a container -e) wins for this run and is reported read-only; clear the variable in the launching shell before storing a different value.Read these pages when the package-level contract is not enough. They move from the shared subsystem vocabulary to the shipped store and the capability architecture.
CredentialRef/CredentialKey, per-operation resolution, UI-safe info, provider layers, and the generated cordis surface.Indirectly, through the consuming adapter, which resolves each credential reference and owns every model-facing use a value authorizes.
No direct invalidation; resolved values never enter a request prefix.
These limits define when this package is a poor fit or needs special care. They are current package constraints, not a task backlog.
list() over that half has no current consumer. Records do enumerate, because they have no schema to be discovered from.<owner>/<id> addressing.describe() on its own navigation.listRecords() and whatever registry owns that scope, and the seam has no registry of its own to check against.