2026-08-13 15:33:29 +08:00
|
|
|
import { CredentialProvider } from '@deepseek-ai/dsh-credentials'
|
|
|
|
|
import type {
|
|
|
|
|
CredentialInfo,
|
|
|
|
|
CredentialKey,
|
|
|
|
|
CredentialRecord,
|
|
|
|
|
CredentialRecordEntry,
|
|
|
|
|
CredentialRecordInfo,
|
|
|
|
|
CredentialRef,
|
|
|
|
|
ResolvedCredential,
|
|
|
|
|
} from '@deepseek-ai/dsh-credentials'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* In-memory credentials provider for the authorization suite. Only the record
|
|
|
|
|
* half is exercised — the seam's whole interest in this service is whether a
|
|
|
|
|
* flow left a record behind — so the reference half answers "nothing stored".
|
|
|
|
|
*/
|
fix(credentials,authorization,llm-pi-ai): harden the auth seams per review
Review findings on #2509, all confirmed:
- Every writer of .credentials.yaml now waits out the record-mutation
lock (DOCUMENT_LOCK_WAIT_MS): refs and records share one file and one
lock, so a reference write or record delete contending with an OAuth
refresh must not fail at the 2s file-work default.
- api-key records are admitted before they are rendered: an empty key,
a non-POSIX env name, or an empty env value is refused at the write
instead of persisting a document the next boot rejects wholesale.
- llm-pi-ai no longer lets the credential-key grammar reject legal
route ids: reads answer "nothing stored" via isCredentialKeySegment
(new dsh-credentials export), deletes have nothing to remove, and only
a write refuses, as LlmError UNSTORABLE_PROVIDER_ID; flow registration
skips a future catalog id outside the grammar instead of failing the
mount.
- authorization/settled fans out with contained listener failures on
the credentials seam's terms (INVARIANT still rethrows), so a broken
watcher can never turn a finished attempt into a failure.
- notify() is fire-and-forget at the seam: a surface that cannot render
a notice loses the notice, never the attempt.
- A declined prompt is an outcome: interactions reject with the new
AuthorizationDeclinedError and the attempt settles cancelled instead
of failed.
- NOT_COMMITTED now confirms a commit observed during the attempt
(credentials/record-updated for the flow's key), so a re-auth cannot
pass a stale record off as fresh; a flow that deletes its record is
refused on the same code.
READMEs, the subsystem/event/config catalogs, and the Agent Note follow
the shipped behavior; memory.ts carries the dedup TODO.
2026-08-17 13:46:13 +08:00
|
|
|
// TODO: near-duplicate of the record half of
|
|
|
|
|
// packages/credentials/credentials/tests/memory.ts; fold both into a shared
|
|
|
|
|
// test-support double when a third suite needs one.
|
2026-08-13 15:33:29 +08:00
|
|
|
export class MemoryCredentials extends CredentialProvider {
|
|
|
|
|
private readonly records = new Map<CredentialKey, CredentialRecord>()
|
|
|
|
|
|
|
|
|
|
override resolve(_ref: CredentialRef): Promise<ResolvedCredential | undefined> {
|
|
|
|
|
return Promise.resolve(undefined)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override describe(_ref: CredentialRef): Promise<CredentialInfo> {
|
|
|
|
|
return Promise.resolve({ configured: false, writable: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override set(_ref: CredentialRef, _value: string): Promise<void> {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override unset(_ref: CredentialRef): Promise<void> {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override readRecord(key: CredentialKey): Promise<CredentialRecord | undefined> {
|
|
|
|
|
return Promise.resolve(this.records.get(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override describeRecord(key: CredentialKey): Promise<CredentialRecordInfo> {
|
|
|
|
|
const stored = this.records.get(key)
|
|
|
|
|
return Promise.resolve(stored === undefined
|
|
|
|
|
? { configured: false, writable: true }
|
|
|
|
|
: { configured: true, kind: stored.kind, writable: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override listRecords(): Promise<readonly CredentialRecordEntry[]> {
|
|
|
|
|
return Promise.resolve([...this.records].map(([key, record]) => ({ key, kind: record.kind })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override async modifyRecord(
|
|
|
|
|
key: CredentialKey,
|
|
|
|
|
mutate: (current: CredentialRecord | undefined) => Promise<CredentialRecord | undefined>,
|
|
|
|
|
): Promise<CredentialRecord | undefined> {
|
|
|
|
|
const current = this.records.get(key)
|
|
|
|
|
const next = await mutate(current)
|
|
|
|
|
if (next === undefined) return current
|
|
|
|
|
this.records.set(key, next)
|
|
|
|
|
this.ctx.emit('credentials/record-updated', key)
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override deleteRecord(key: CredentialKey): Promise<void> {
|
|
|
|
|
if (this.records.delete(key)) this.ctx.emit('credentials/record-updated', key)
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|