Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BannedSymbols.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# with a why-comment naming the design section that sanctions it; never delete a line to land a change.

# Cancellation and execution-context discipline (design §8.1, §8.3). Flow suppression is allowed only inside the
# one background-launch helper of design §5.4, which does not exist yet.
# one background-launch helper of design §5.4, Internal/BackgroundWork.cs.
M:System.Threading.Thread.Interrupt;Thread.Interrupt is prohibited (design §8.3); cancel through a CancellationToken
M:System.Threading.ExecutionContext.SuppressFlow;Flow suppression belongs only in the background-launch helper of design §5.4
M:System.Threading.ThreadPool.UnsafeQueueUserWorkItem(System.Threading.WaitCallback,System.Object);Flow-suppressing queueing belongs only in the background-launch helper of design §5.4
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,33 @@ Roadmap phase 1, defects S1–S9, each pinned by a `[Trait("Category", "Security
- **Breaking:** `OperationPolicy`'s parameterless constructor becomes `OperationPolicy(TimeProvider? timeProvider = null)`
(source-compatible, binary-incompatible); `AddStandardResilience` passes its `timeProvider` to it, so a fake clock drives
the deadline.
- **Breaking:** `RequestOptions` equality now includes the new `Auth` and `OperationAuth` descriptors (`AUTH-4`); two
options differing only in a descriptor were equal before.
- **Breaking:** `AccessToken.ExpiresOn` is `DateTimeOffset?` (a token may never expire); the token must be non-blank; equality
is by value (token, expiry, refresh) with `==`/`!=`; `ToString` redacts the token (`AUTH-8`..`AUTH-10`).
- **Breaking:** `ApiKeyCredential` rejects a whitespace-only key, a blank or whitespace-containing `Scheme`, and a key or
scheme the outbound header grammar refuses, at construction (was: an empty key only); `ToString` redacts the key
(`AUTH-26`, `XCUT-18`).
- **Breaking:** `BasicCredential` rejects an empty username or password and a `:` in the username (was: `null` only);
`ToString` redacts the password (`AUTH-14`).
- **Breaking:** `TokenRequestContext.Scopes` is a copy of the caller's list (was: the list itself).
- **Breaking:** `AuthorizationPolicy`'s protected surface: a constructor taking the client `AuthDescriptor` and the available
`AuthScheme`s; `WithheldHeaderNames` replaces `WithheldHeaderName`; `GetCredentialAsync` / `GetCredential` take the
resolved `AuthRequirement` and the request, may return `null` ("nothing to attach on this pass") and are both abstract
(the sync-over-async default is gone); new `OnChallengeAsync` / `OnChallenge` hooks (`AUTH-27`..`AUTH-33`).
- **Breaking:** the HTTPS refusal is `HttpsRequiredException` (was a plain `SdkException`, which it still derives from);
it now also covers the outbound pass of a reactive scheme (`AUTH-28`).
- **Breaking:** every auth policy honours `RequestOptions.Auth` / `OperationAuth` (a per-call `NoAuth` sends the call
anonymously, an unservable scheme throws `AuthResolutionException` before anything is sent) and answers a `401`
carrying `WWW-Authenticate` through its hook, replaying once when the replacement is same-origin and replayable.
- **Breaking:** `AccessTokenCache` refreshes 30 seconds before expiry by default (was: at expiry); stamps a still-valid token and
refreshes it in the background, one refresh per key (was: awaited the refresh on the request path); logs a failed
background refresh as event 160 `dexpace.auth.token_refresh_failed` (was: silent); rejects a default or already-expired
provider token with `TokenProviderException`; lets the waiters of a failed fetch share its outcome; and is bounded to
1024 keys (`AUTH-11`, `AUTH-34`..`AUTH-37`, `XCUT-12`, `XCUT-14`).
- **Breaking:** `BearerTokenAuthPolicy` retries a call once with a freshly fetched token when the server answers `401` with
a `Bearer` challenge (was: returned the `401`), and its synchronous path no longer blocks on the asynchronous one
(`AUTH-30`, `AUTH-31`, `AUTH-36`).

- **Breaking (phase 6b redirect, PR 1):** `RedirectOptions` is reshaped (`REDIR-3`, `REDIR-4`, `REDIR-5`, `REDIR-15`, `REDIR-17`,
`REDIR-20`, `REDIR-26`). `StripSensitiveHeadersOnCrossOrigin` is removed (it has had no effect since phase 1; stripping always
Expand Down Expand Up @@ -490,5 +517,28 @@ Roadmap phase 1, defects S1–S9, each pinned by a `[Trait("Category", "Security
cross-origin hop or a retry across one), the NativeAOT smoke check `CheckPhase6bRedirectAsync`, and `docs/sdk-documentation/redirect.md`.
- `docs/sdk-documentation/retry.md`; the AOT smoke covers `RetryPolicy`, `RetryRecovery` through `RecoveryDispatcher`, `OperationTimeoutException`, a custom `IRetryableError`
and the `RetryOptions` validation.
- Phase 6c descriptor and resolver: `AuthScheme`, `AuthRequirement`, `AuthDescriptor`, `AuthResolver` (per-call, operation and
client tiers, no fall-through), `AuthResolutionException`, and the `RequestOptions.Auth` / `OperationAuth` carriers
(`AUTH-1`..`AUTH-7`).
- `DigestCredential` and `AuthCredentials` (with a `TokenRefreshMargin`); `CredentialRedactionTests` (`Security`) pins that no
credential type formats its secret (`XCUT-19`(d)).
- Phase 6c challenge model: `AuthenticationChallenge` and its lenient, linear RFC 7235 parser (`Parse(string?)`,
`Parse(ReadOnlySpan<char>)`; a duplicate parameter keeps the first value), the `IChallengeHandler` SPI,
`BasicChallengeHandler`, `CompositeChallengeHandler`, and `HttpHeaderName.WellKnown.WwwAuthenticate`,
`ProxyAuthenticate` and `ProxyAuthorization` (`AUTH-12`, `AUTH-13`, `AUTH-14`, `AUTH-23`, `AUTH-25`).
- `ChallengeAuthPolicy` (Basic or Digest answered on a `401`, never preemptive), `AuthChallengeContext` and
`HttpsRequiredException` (`AUTH-26`..`AUTH-33`, `AUTH-38`).
- `AccessTokenCache.Get` (a real synchronous path), `AccessTokenCache.DefaultRefreshMargin` and `RefreshMargin`,
`BearerTokenAuthPolicy(AccessTokenCache, params string[])`, `TokenProviderException`, and log event 160
`DexpaceLogEvents.TokenRefreshFailed` (`AUTH-35`, `AUTH-37`).
- RFC 7616 Digest authentication: `DigestChallengeHandler` (MD5, MD5-sess, SHA-256, SHA-256-sess, `qop=auth` or the legacy
no-qop form; a configurable algorithm preference, SHA-256 first by default; `username*` for a non-ASCII user name; a
per-nonce counter bounded to 1024 entries) and `DigestAlgorithm`. A host whose crypto provider refuses MD5 (FIPS) drops the
MD5 algorithms instead of failing, so an MD5-only challenge is declined and a server offering both gets SHA-256
(`AUTH-15`..`AUTH-22`, `AUTH-24`). No **Breaking** change.
- `MultiSchemeAuthPolicy`: the descriptor-driven, multi-credential auth step (`AuthCredentials` for OAuth2, API key, Basic and
Digest) for APIs that declare several security schemes, sharing its stamping logic with the single-scheme policies;
the OpenAPI mapping table is in `docs/sdk-documentation/auth.md` (`AUTH-4`, `AUTH-5`).
- `docs/sdk-documentation/auth.md`; the AOT smoke covers the challenge parser, the resolver, Digest (SHA-256 and the CSPRNG cnonce), the redacting credentials and the bearer policy with its background refresh.

[Unreleased]: https://github.com/dexpace/dotnet-sdk/commits/main
15 changes: 9 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,13 @@ dotnet-sdk/
│ │ ├── Resilience/ # internal: RetryFacts (classifier, re-send gate), RetryBackoff, RetryPacing, RetryBudget, RetryEngine (the one retry loop)
│ │ ├── Pipeline/ # HttpPipeline (also a transport), PipelineBuilder, HttpPipelinePolicy,
│ │ │ └── Policies/ # PipelineContext, PipelineRunner, DexpacePipeline; operation, redirect, retry,
│ │ │ # idempotency, set-date, client-identity, instrumentation, auth, error-mapping policies;
│ │ │ # idempotency, set-date, client-identity, instrumentation, error-mapping policies; the auth base
│ │ │ # AuthorizationPolicy + ApiKey, Basic, BearerToken, Challenge and MultiScheme policies;
│ │ │ # Redirect/ holds the internal decider, chain, location resolver and reissue behind RedirectPolicy
│ │ ├── Recovery/ # Outcome, the step contracts, request/response recovery chains, RecoveryDispatcher, RetryRecovery,
│ │ │ # ErrorMappingStep, IdempotencyKeyStep, ClientIdentityStep
│ │ ├── Auth/ # TokenCredential, AccessTokenCache, ApiKeyCredential, BasicCredential
│ │ ├── Auth/ # AuthScheme, AuthRequirement, AuthDescriptor, AuthResolver, AuthCredentials, TokenCredential, AccessToken, AccessTokenCache,
│ │ │ # ApiKey/Basic/DigestCredential, AuthenticationChallenge, IChallengeHandler + Basic/Digest/Composite handlers
│ │ ├── Pagination/ # AsyncPageable<T>, Page<T>, Pageable, PaginationStrategies
│ │ ├── Configuration/ # DexpaceClientOptions, RetryOptions, RedirectOptions, HttpLoggingOptions (sealed records), HttpLogLevel, ProxyOptions, TimeProviderWaits, BuildInfo
│ │ ├── Diagnostics/ # DexpaceDiagnostics (ActivitySource + Meter), UrlRedactor, DexpaceLogEvents/Keys, HttpLogEmitter,
Expand Down Expand Up @@ -264,10 +266,11 @@ the `http.request`/`http.response` events, header and URL redaction, the emissio
`docs/sdk-documentation/logging-and-redaction.md`; and phase 5c's tracing and metrics — the operation span, the attempt span rework, the
`dexpace.*` span events, the two instruments with their stable attribute sets and `SystemNetHttpClient`'s `traceparent` rule — see
`docs/sdk-documentation/tracing-and-metrics.md`; and phase 6a's retry — the classifier and `IRetryableError`, the re-send gate, `RetryOptions`' validation, the one `RetryEngine` under `RetryPolicy` and `RetryRecovery`, the pacing
headers, the suppressed trail, `OperationTimeoutException` and the enforced `AttemptTimeout` — see `docs/sdk-documentation/retry.md`; and phase 6b's redirect —
headers, the suppressed trail, `OperationTimeoutException` and the enforced `AttemptTimeout` — see `docs/sdk-documentation/retry.md`; phase 6b's redirect —
the pure decider behind `RedirectPolicy`, `RedirectOptions` (`AllowedMethods`, `FollowSeeOther`, `Predicate`), the `RedirectException` family, loop detection, the five
`http.redirect.*` events and the end-to-end credential-leak tests — see `docs/sdk-documentation/redirect.md`):
the auth resolver with RFC 7235
challenges and Digest (6c), tri-state PATCH, SSE and the remaining pagination surface (7), the transport
`http.redirect.*` events and the end-to-end credential-leak tests — see `docs/sdk-documentation/redirect.md`; and phase 6c's authentication — the
descriptor and resolver with their `RequestOptions` tiers, the redacting credentials, the lenient challenge parser, the `401` lifecycle in `AuthorizationPolicy`, the
bounded token cache with its background refresh, Digest with the MD5 probe, and `MultiSchemeAuthPolicy` — see `docs/sdk-documentation/auth.md`):
tri-state PATCH, SSE and the remaining pagination surface (7), the transport
conformance kit (8), the DI package `Dexpace.Sdk.Extensions.DependencyInjection` (9), and the release
path (12).
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ only describes what already exists is a table that gets edited after the fact.
| [`superpowers/`](./superpowers/README.md) | Nothing, for long. The **inbox** the Superpowers skills write into | `brainstorming`, `writing-plans` | Yes — it drains it |
| [`first-release.md`](./first-release.md) | Release-readiness register: package versions, blockers before first publish, the release path, what v1 ships without, post-release triggers | A human, updated as blockers close | No |
| `deviations.md` **(planned)** | As-built audit of `sdk-design-dotnet/10`'s deviation ledger, plus deviations found outside a phase. Created by roadmap phase 11 | A human, following a phase or review | No — judgment, not a mechanical append |
| [`sdk-documentation/`](./sdk-documentation/http.md) | **As-built** user documentation — how the packages compose, worked examples — one page per phase that ships a layer, mirroring `ruby-sdk/docs/sdk-documentation/`. Today: [`http.md`](./sdk-documentation/http.md) (phase 2a), [`seams.md`](./sdk-documentation/seams.md) (phase 2b), [`io.md`](./sdk-documentation/io.md) (phase 3a), [`bodies.md`](./sdk-documentation/bodies.md) (phase 3b), [`execution-context.md`](./sdk-documentation/execution-context.md) (phase 4a), [`recovery.md`](./sdk-documentation/recovery.md) (phase 4b), [`pipelines.md`](./sdk-documentation/pipelines.md) (phase 4c), [`configuration.md`](./sdk-documentation/configuration.md) (phase 5a), [`logging-and-redaction.md`](./sdk-documentation/logging-and-redaction.md) (phase 5b), [`tracing-and-metrics.md`](./sdk-documentation/tracing-and-metrics.md) (phase 5c), [`retry.md`](./sdk-documentation/retry.md) (phase 6a), [`redirect.md`](./sdk-documentation/redirect.md) (phase 6b) | A human, or a skill on request | Yes |
| [`sdk-documentation/`](./sdk-documentation/http.md) | **As-built** user documentation — how the packages compose, worked examples — one page per phase that ships a layer, mirroring `ruby-sdk/docs/sdk-documentation/`. Today: [`http.md`](./sdk-documentation/http.md) (phase 2a), [`seams.md`](./sdk-documentation/seams.md) (phase 2b), [`io.md`](./sdk-documentation/io.md) (phase 3a), [`bodies.md`](./sdk-documentation/bodies.md) (phase 3b), [`execution-context.md`](./sdk-documentation/execution-context.md) (phase 4a), [`recovery.md`](./sdk-documentation/recovery.md) (phase 4b), [`pipelines.md`](./sdk-documentation/pipelines.md) (phase 4c), [`configuration.md`](./sdk-documentation/configuration.md) (phase 5a), [`logging-and-redaction.md`](./sdk-documentation/logging-and-redaction.md) (phase 5b), [`tracing-and-metrics.md`](./sdk-documentation/tracing-and-metrics.md) (phase 5c), [`retry.md`](./sdk-documentation/retry.md) (phase 6a), [`redirect.md`](./sdk-documentation/redirect.md) (phase 6b), [`auth.md`](./sdk-documentation/auth.md) (phase 6c) | A human, or a skill on request | Yes |
| [`architecture.md`](./architecture.md) | The original foundation-slice architecture note, marked superseded by `sdk-design-dotnet/01-overview.md`; kept until roadmap phase 12 writes `sdk-documentation/architecture.md` | A human | Yes |
| [`README.md`](./README.md) | This index | A human | Yes |

Expand Down
7 changes: 7 additions & 0 deletions docs/first-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ Each phase adds an entry for each requirement it declines, with the requirement
- **`REDIR-27` (MAY): the redirect target header is not configurable.** The header is `Location`; a `LocationHeader` option has no consumer
and no trigger is named. Phase 6b (2026-10-08); design §12's deferred list and P6b-21.

- **Authentication features outside the requirements (phase 6c, P6c-13, P6c-19, P6c-35, P6c-40).** Not built, and not planned for v1: **claims challenges**
(`WWW-Authenticate: Bearer … claims="…"`, continuous access evaluation; the bearer hook re-fetches with the same `TokenRequestContext`), an **`apiKey` in a query
string or a cookie** (`AUTH-26` is header-only; no policy stamps either), a **preemptive Digest** (the handler answers a challenge and remembers only counts),
**`407` proxy challenges in the pipeline** (a forward proxy's `407` is answered by the transport from `ProxyOptions.ChallengeCredentials`, phase 8b), and a
**`NamedKeyCredential`** (a prefixed key is `new ApiKeyCredential(key, scheme: "SharedAccessKey")`). Digest's MD5 algorithms are unavailable on a host whose crypto
provider refuses MD5 (design §10 entry 16).

### Behavioural asymmetries a consumer must know

- **`async-redirect-pillar` (design §10 entry 14; `PIPE-32`, `REDIR-25`).** The reference contract has the asynchronous standard pipeline
Expand Down
13 changes: 13 additions & 0 deletions docs/knowledge/notes/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# authentication — notes

Hand-written. `../harvested/authentication.md` is what the documents say; this is what the port decided, and it wins.

## Superseded
- **Hex is lower-cased with `Convert.ToHexStringLower`, because the port targets `net10.0` only.** Supersedes `authentication/af09917c`: design §6.3's "lower-cases hex output manually … the port targets net8.0" predates roadmap decision D1, which set every project to `net10.0`; the Digest code calls `Convert.ToHexStringLower` (verified to return `ab0f` for `[0xAB, 0x0F]`), and `.editorconfig`'s CA1308 relaxation is unrelated to it.
<sub>review · `docs/work/mvp/phase6/phase6c/2026-10-08-phase6c-auth-checklist.md` · high · sha:manual-p6c-hex-lower</sub>

## Conclusions
- **`net10.0` has no `MD5.IsSupported`, so the Digest handler finds out whether MD5 is available by trying one hash, once per process.** Verified on 10.0.401 (reflection finds no such member, `MD5.HashData([])` succeeds); a `CryptographicException` or `PlatformNotSupportedException` means a FIPS-refusing provider, which drops the MD5 algorithms from the handler's effective preference.
<sub>review · `docs/work/mvp/phase6/phase6c/2026-10-08-phase6c-auth-checklist.md` · high · sha:manual-p6c-md5-probe</sub>
- **`Uri.GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped)` equals the request-target `SocketsHttpHandler` writes, so it is the Digest `uri`.** Verified on the wire for `/a%2Fb?x=%26y`, an empty path (`/`), an empty path with a query (`/?q=1`) and `/p%20q/r?a=b%20c&d=%7E` (both write `d=~`); `DigestWireTests` pins it.
<sub>review · `docs/work/mvp/phase6/phase6c/2026-10-08-phase6c-auth-checklist.md` · high · sha:manual-p6c-digest-uri</sub>
4 changes: 4 additions & 0 deletions docs/sdk-design-dotnet/05-pipeline-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@ because it is the runtime's own and every OpenTelemetry collector reads it (P14)
runs under `ExecutionContext.SuppressFlow()` — verified: a task started under suppression observes no `AsyncLocal`
value.

*Correction 2026-10-08 (phase 6c, P6c-25):* the one background-launch helper is `Internal/BackgroundWork.cs` (`BackgroundWork.Run(Func<Task>)`), the only
source file that mentions `SuppressFlow`; a source scan (`BannedSymbolsSiteTests`) pins it. The token cache's refresh is its first caller. It skips the
suppression when flow is already suppressed.

**The promotion chain** (**CTX-1**–**CTX-3**) is three sealed records — `DispatchContext`, `RequestContext`,
`ExchangeContext` — not one type with a stage field, so "the exchange type exposes no method promoting back"
(**CTX-1**) is the absence of a method. Each `Promote` returns a new instance carrying the same key and bundle and
Expand Down
Loading
Loading