fix: record CIMD provenance as an issuer-keyed marker, not on the credential

Copilot review (#2287), three findings:

1. **invalid-client recovery lost the provenance.** SDK v2 `auth()` answers
   `invalid_client` / `unauthorized_client` with `invalidateCredentials("client")`
   and an immediate retry; our clear drops the registration *and* its kind, so
   the retry's URL-based client-ID save landed with nothing recorded and was
   stored as `dcr`. Provenance now lives in `cimdClientMetadataUrl`, an
   issuer-keyed marker on the issuer slot that records a property of the AS
   rather than a credential — `clearClientInformation` deliberately leaves it
   alone. `ensureCimdClientRegistration` writes it, and withdraws it when the AS
   stops advertising CIMD, on every connect.

2. **Discovery became a hard network dependency.** Moving the existing-client
   check after discovery meant a well-known outage could fail a reconnect the SDK
   would have served from its persisted discovery state. Reuse
   `provider.discoveryState()` first, and treat a discovery failure as "skip
   pre-registration" rather than an error — this helper is an optimization over
   what `auth()` does for itself.

3. **Dropped an unjustified double cast** in the test fetch helper; the async
   signature is directly assignable to `typeof fetch`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lmj6X2epWpPE7ScdSKQmtA
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-07 01:34:03 -04:00
commit 4297ef8449

View file

@ -186,6 +186,34 @@ export class OAuthStorageBase implements OAuthStorage {
);
}
async getCimdClientMetadataUrl(
serverUrl: string,
issuer?: string,
): Promise<string | undefined> {
await this.ensureLoaded();
const state = this.memory.getState().getServerState(serverUrl);
return this.issuerSlot(state, issuer)?.cimdClientMetadataUrl;
}
async saveCimdClientMetadataUrl(
serverUrl: string,
issuer: string,
clientMetadataUrl: string | undefined,
): Promise<void> {
await this.ensureLoaded();
// Not a save of credentials, so it must not promote this issuer to
// `activeIssuer` — the marker is written during discovery, before anything
// has been authorized against this AS.
this.updateIssuerSlot(
serverUrl,
issuer,
{ cimdClientMetadataUrl: clientMetadataUrl },
{},
false,
);
await this.persist();
}
async saveClientInformation(
serverUrl: string,
clientInformation: OAuthClientInformation,