KeyManager.canExport capability and optional exportKeyStatus: Accepted
Date: 2026-05-21
Branch / PR: refactor/kms-signing-flow
References: ADR 007, ADR 012, ADR 033
@did-btcr2/api’s key-management facade (KeyManagerApi) exposes an export(id) method that returns a full SchnorrKeyPair from the backing KeyManager. The bundled LocalKeyManager supports this because it holds raw secret bytes in the JS heap. External KeyManager adapters that we want to encourage (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault Transit, hardware wallets, HSMs) typically forbid key export by design: that is the whole point of using such a service.
Before this ADR, the facade reached for instanceof LocalKeyManager to gate export support:
export(id: KeyIdentifier): SchnorrKeyPair {
if (!(this.kms instanceof LocalKeyManager)) {
throw new Error('Key export is not supported by the current KeyManager implementation.');
}
return this.kms.exportKey(id);
}
That gate has three problems:
FileBackedKeyManager that does support export with the same security model) would be rejected for the wrong reason.KeyManager.exportKey is currently a method on LocalKeyManager only, not on the interface, so calling code that holds a KeyManager reference cannot ask “can I export?” without an instanceof jump.Add two members to the KeyManager interface:
readonly canExport?: boolean: capability probe. Default-undefined is treated as false (fail-closed if the adapter does not opt in).exportKey?(id: KeyIdentifier): SchnorrKeyPair: optional export method. Adapters that advertise canExport: true must implement it.KeyManagerApi.export checks both before delegating:
export(id: KeyIdentifier): SchnorrKeyPair {
if (!this.kms.canExport || !this.kms.exportKey) {
throw new Error(
'Key export is not supported by the current KeyManager implementation. '
+ 'The adapter must advertise `canExport: true` and provide an `exportKey` method.'
);
}
return this.kms.exportKey(id);
}
The bundled LocalKeyManager declares readonly canExport = true and continues to expose exportKey as before (no behavior change for in-process callers).
instanceof LocalKeyManager check, decoupling the api package from the key-manager package’s concrete class.KeyManager and sees what optional surface they may implement.LocalKeyManager declares both members, so callers continue to work; adapters that omit them inherit the fail-closed behavior they already had under the instanceof check.Optional interface members + capability flags are a lightweight alternative to a parallel Exportable interface or a KeyManager & Exportable mixin. We pick this because (a) the capability is a single boolean and a single method, (b) we already have a stable KeyManager interface and do not want to fragment it across multiple “shapes,” and (c) the pattern composes cleanly if future capabilities are added (e.g., canRotate, rotateKey?(id)).