Skip to content

feat(NET-16): Track public API surface; rename two SDK client methods - #40

Open
cozminu wants to merge 58 commits into
cozmin/refactorfrom
cozmin/net-16
Open

feat(NET-16): Track public API surface; rename two SDK client methods#40
cozminu wants to merge 58 commits into
cozmin/refactorfrom
cozmin/net-16

Conversation

@cozminu

@cozminu cozminu commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds Microsoft.CodeAnalysis.PublicApiAnalyzers to both shipping packages (OpenPayments.Sdk, OpenPayments.Sdk.HttpSignatureUtils) with committed PublicAPI.Shipped.txt/PublicAPI.Unshipped.txt baselines, so an undeclared public API change now fails the build. Closes Track the public API surface and correct method names #22.
  • BREAKING: renames IAuthenticatedClient.CompleteIncomingPaymentsAsyncCompleteIncomingPaymentAsync (completes one payment) and IResourceClientBase/ResourceClientBase.ListOutgoingPaymentAsyncListOutgoingPaymentsAsync (lists many), so the auth and resource client layers agree on singular/plural naming.
  • Adds a tested scripts/promote-public-api.sh release step (make promote-api), CHANGELOG.md, and a README "Releasing" section documenting the process.
  • This branch also carries the preceding incremental work already committed: splitting generated DTOs from hand-owned client wrappers, a shared HTTP error-response mapper, and an async HTTP-signing message-handler pipeline.

See docs/adr/0001-track-the-public-api-surface.md (local, git-ignored by repo convention) for the full design rationale behind the baseline-seeding and rule-scoping decisions.

Test plan

  • dotnet build OpenPayments.sln -c Release --no-incremental — 0 warnings, 0 errors
  • dotnet test OpenPayments.sln -c Release — 227/227 passing
  • scripts/promote-public-api.test.sh — 5/5 fixture checks passing
  • Old method names fully removed (grep -rn across the repo returns nothing outside the *REMOVED* baseline entry)
  • CI green on this PR

cozminu and others added 30 commits July 29, 2026 12:20
Adds the missing <param> tags for the Uri parameters introduced by the
thread-safe base URL refactor (resourceServerUrl/incomingPaymentUrl/
outgoingPaymentUrl) in the Incoming/Outgoing Payment partials, clearing
the CS1573 warnings from the documentation build.

Also hardens the CI BaseUrl guard: scans all of OpenPayments.Sdk/ (not
just Clients/) while excluding generated *.g.cs files, tolerates
whitespace around the '=' in "BaseUrl =", and fails loudly instead of
silently passing if its scan target ever disappears or is renamed.
Authenticated clients now use a signed named HttpClient carrying
SigningHttpMessageHandler, and a separate unsigned client for
wallet-address and public incoming-payment reads. Removes the two
sync-over-async .Result signing hooks.

Closes #17
…ed pipeline test coverage)

- Replace the unresolvable <see cref="IHttpClientFactory"/> doc-comment reference
  in SigningHttpMessageHandler with <c>IHttpClientFactory</c>, since
  OpenPayments.Sdk.HttpSignatureUtils has no reference to Microsoft.Extensions.Http.
  This was the only warning in a clean Release build.
- Add DI-level tests proving UseOpenPayments wires the "signed" named HttpClient
  (openpayments-signed) with the signing handler and the "unsigned" named
  HttpClient (openpayments) without it, so a swapped registration would now
  fail a test instead of silently shipping.
TryReadError was documented as "Never throws" but could throw ArgumentException
when error.code or error.description were non-scalar JSON types (objects, arrays,
numbers, etc.). Newtonsoft's explicit (string?) cast only catches JsonException,
leaving ArgumentException uncaught.

Refactored to extract string conversion logic into TryGetStringValue, which:
- Checks token.Type == JTokenType.String before attempting conversion
- Returns null for non-string tokens
- Catches and swallows ArgumentException as a defensive measure

Added test cases covering non-scalar error fields: nested objects, arrays, numbers,
and booleans. All 90 tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Transport-level failures (DNS, connection, TLS, timeout) still surface as
HttpRequestException/TaskCanceledException from HttpClient before any
response exists — the README and XML doc previously implied
OpenPaymentsApiException covered all failures.
The generated-methods check already caught legacy ApiException usage in
OpenPayments.Sdk/Generated, but Clients/ was only scanned for
EnsureSuccessStatusCode, not ApiException — a regression that catches and
rethrows a generated ApiException inside ResourceClientBase or
AuthClientBase would pass CI undetected.
TryGetStringValue's catch (ArgumentException) could never fire — the only
cast is already guarded by a JTokenType.String check — and its comment
mischaracterized non-scalar tokens as landing elsewhere. ReadBodyAsync's
Content is null guard is dead on modern .NET (Content is never null) and
was inconsistent with Helpers.ExtractHeaders, which dereferences
response.Content with no such guard.
System.Net must sort first per dotnet_sort_system_directives_first,
matching the sibling AuthenticatedClient_Tests.cs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cozminu and others added 27 commits July 30, 2026 12:47
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds internal ContentDigestVerifier.MatchesBodyAsync, wired into
HttpSignatureValidator.ValidateSignatureAsync right after the existing
SignatureInputValidator guard. Closes a signature-bypass gap: the
validator previously read the Content-Digest header but never compared
it to the actual body, so a request whose body was swapped while
replaying the original content headers produced a byte-identical
signature base string and validated successfully.
…ader-source mismatch and unsigned-body bypass

The whole-branch review of the 7-task signature-roundtrip fix (issue #19) found two
independently-shipped, individually-reviewed changes that combined into exploitable gaps:

1. SignatureBaseBuilder.GetHeaderValue (what the Ed25519 signature actually commits to) resolves
   a covered header from request headers before content headers, but ContentDigestVerifier read
   Content-Digest from content headers only. Since Content-Digest isn't a recognized .NET content
   header type, an attacker could set the original digest on a request header (satisfying the
   signature) and a digest for a swapped body on the content header (satisfying the verifier),
   forging an arbitrary body under a valid signature. Fixed by making ContentDigestVerifier read
   the header the same way the signed base string does (GetHeaderValue is now internal instead of
   private).

2. HttpRequestSigner only covers content-digest/-length/-type when the body is non-empty at
   signing time, but validation never enforced the converse: a signature over a bodyless request
   (covering only @method/@target-uri) validated successfully against any body an attacker
   attached afterward, since nothing in the signature committed to it. Fixed by rejecting
   validation when the request carries a non-empty body that content-digest doesn't cover.

Also fixed: a request missing the Signature header (with Signature-Input present) threw
NullReferenceException instead of returning false, via a null-forgiving operator masking a null
value all the way to TryParseSignature's first dereference.

Four regression tests added; RED/GREEN verified for each finding against the unmodified code
before applying the fixes. Full suite: 87/87 passing, 0 warnings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g sections, and cover signed/unsigned pipeline on the resolved client

Tightens the AddOpenPaymentsCore ClientUrl guard to reject non-absolute URIs (configuration
binding parses "wallet.example" as a valid relative Uri rather than failing), adds a
section.Exists() guard so a missing/misspelled IConfiguration section no longer silently
registers nothing, switches Assert.Single(services.Where(...)) to the predicate overload to
clear xUnit2031 warnings, and cleans up temp key-file directories via IDisposable so test runs
stop leaving Ed25519 PEM files scattered in the system temp dir. Also adds a test that resolves
IAuthenticatedClient from a real ServiceProvider and proves its signed pipeline attaches
Signature/Signature-Input headers while its unsigned pipeline does not.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… missing-section message

Reverts the parameter type from IConfigurationSection back to IConfiguration as per the
plan review. Changes the error message to no longer reference section.Path (which only
exists on IConfigurationSection). Updated the test assertion to match the path-free message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the inline BaseUrl and error-handling guard checks out of build.yaml
into executable scripts under .github/workflows/scripts/ for readability.
…ted/ invariants

Fixes gaps found in final whole-branch review: release.yaml regenerated
DTOs with an unpinned NSwag before packing (defeating the drift check's
purpose), build.yaml's path filters excluded the Makefile/spec submodule/
workflow files most likely to cause real drift, and the drift check itself
only compared regeneration against committed output rather than asserting
the DTO-only invariants directly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wire Microsoft.CodeAnalysis.PublicApiAnalyzers 5.6.0 into OpenPayments.Sdk and
OpenPayments.Sdk.HttpSignatureUtils, harvest their PublicAPI.Shipped.txt baselines
straight from analyzer output (60 + 822 entries), and add the promote-api Make
target that folds Unshipped.txt into Shipped.txt via Task 1's script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completes a single payment. Aligns the auth layer with the resource
layer, which already used the singular name. BREAKING.
Lists many. Aligns the resource layer with the auth layer, which
already used the plural name. BREAKING.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…seline sort order, promote-script path matching

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cozminu cozminu changed the title Track public API surface; rename two SDK client methods feat(NET-16): Track public API surface; rename two SDK client methods Jul 31, 2026
@cozminu
cozminu changed the base branch from main to cozmin/refactor July 31, 2026 12:50
@cozminu cozminu self-assigned this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track the public API surface and correct method names

1 participant