refactor(server)!: package structure, DI and OAuth hosting; working Blazor widgets #136

Closed
opened 2026-09-24 12:39:10 +00:00 by Grandiras · 2 comments
Owner

Part of the v0.7.0 update cycle (wave 7). Owner decisions:

  • Re-split EF Core into its own package.
  • Drop the Redis replay store and document a snippet instead.
  • Drop Microsoft.Extensions.Http.Resilience.
  • Move the OAuth cookie login from Blazor into Server.
  • Keep and fix the Blazor widgets.

Packages

  • Split ATProtoNet.Server.EntityFrameworkCore back out: the token store, the Spaces EF stores, both DbContexts and their Add* methods, keeping namespace ATProtoNet.Server.EntityFrameworkCore. Add it to the slnx, pack it, and add tests. Mind the Forgejo package linkage: the package ID predates 0.5.0.
  • Remove RedisSpaceReplayStore, StackExchange.Redis, the Redis tests and ATPROTO_REDIS_URL. docs/spaces.md shows the ~15-line SET NX EX store instead.
  • Remove Microsoft.Extensions.Http.Resilience. The builder exposes the IHttpClientBuilder so apps can add their own resilience (Aspire ServiceDefaults already do). Document that the SDK must not be combined with a handler that retries non-idempotent POSTs or replays DPoP proofs.
  • Result: ATProtoNet.Server has no NuGet dependencies beyond the shared framework.

DI

  • Fix services.AddAtProto(), which currently cannot resolve AtProtoClient: AddHttpClient<T> wins and the singleton is never registered. It returns an IAtProtoBuilder (Services, HttpClient) that the other registrations hang off:
    • WithLifetime, replacing AddAtProtoScoped;
    • token/session store registration;
    • OAuth;
    • the user client factory;
    • the Aspire client and health check, replacing AtProtoClientSettings.
  • Every options class goes through IOptions<T> with validation and ValidateOnStart, and binds from configuration.
  • Add a DI resolution test for every Add* method.
  • The default token store is in-memory, with a startup warning. File or EF storage is an explicit opt-in; today AddAtProtoServer() silently writes to %LocalAppData%.
  • Merge the AddAtProtoServer overloads. Use one HttpClient-name constant, and have OAuth use the factory.

Auth

  • Delete the bearer-JWT AtProtoAuthenticationHandler. It asks services to accept users' PDS access tokens, builds and leaks a client per request, and sets an empty did claim. The service-auth scheme replaces it.
  • Move the OAuth cookie login (AtProtoOAuthService, options, AddAtProtoAuthentication, MapAtProtoOAuth) into ATProtoNet.Server. Delete IOAuthClientProvider.
  • The OAuthClient is an options-built singleton, so refresh works after a restart.
  • Add a per-DID refresh coordinator that re-reads the store after taking the lock. Today two concurrent requests for one user both spend the single-use refresh token and log the user out.
  • The client factory caches DPoP key material and the logger per DID; it currently spends 25 µs importing the key on every request.
  • FileAtProtoTokenStore: lock-free reads and per-DID write locks (4.5× throughput at 64 concurrent callers).
  • Add AtProtoClaimTypes constants.
  • Error redirects carry a fixed code unless the exception is an OAuthException. Today any exception message lands in the login redirect URL.
  • The callback returns a typed result instead of accepting any absolute return URL.
  • MapAtProtoOAuth can serve /oauth-client-metadata.json and the JWKS.

Blazor

  • Rewire FeedView, PostCard, ProfileCard and ComposePost to a scoped current-user client resolved through the factory from the auth state. They currently inject an unregistered, non-user AtProtoClient.
  • Type FeedSource (not a magic string); fix docs/blazor.md.
  • Ship minimal default styles or document the CSS classes.
  • Remove <SupportedPlatform Include="browser" />.
  • Use InternalsVisibleTo for tests instead of 13 reflection calls.
  • Update samples/ServerIntegrationSample and cover each widget with a bUnit or render test.

CHANGELOG Breaking changes, with a migration section (package references, namespaces, registration calls).

Part of the v0.7.0 update cycle (wave 7). **Owner decisions:** - Re-split EF Core into its own package. - **Drop the Redis replay store** and document a snippet instead. - Drop `Microsoft.Extensions.Http.Resilience`. - Move the OAuth cookie login from Blazor into Server. - **Keep and fix** the Blazor widgets. ## Packages - [ ] Split `ATProtoNet.Server.EntityFrameworkCore` back out: the token store, the Spaces EF stores, both DbContexts and their `Add*` methods, keeping namespace `ATProtoNet.Server.EntityFrameworkCore`. Add it to the slnx, pack it, and add tests. Mind the Forgejo package linkage: the package ID predates 0.5.0. - [ ] Remove `RedisSpaceReplayStore`, `StackExchange.Redis`, the Redis tests and `ATPROTO_REDIS_URL`. `docs/spaces.md` shows the ~15-line `SET NX EX` store instead. - [ ] Remove `Microsoft.Extensions.Http.Resilience`. The builder exposes the `IHttpClientBuilder` so apps can add their own resilience (Aspire ServiceDefaults already do). Document that the SDK must not be combined with a handler that retries non-idempotent POSTs or replays DPoP proofs. - [ ] Result: `ATProtoNet.Server` has **no NuGet dependencies** beyond the shared framework. ## DI - [ ] Fix `services.AddAtProto()`, which currently cannot resolve `AtProtoClient`: `AddHttpClient<T>` wins and the singleton is never registered. It returns an `IAtProtoBuilder` (`Services`, `HttpClient`) that the other registrations hang off: - `WithLifetime`, replacing `AddAtProtoScoped`; - token/session store registration; - OAuth; - the user client factory; - the Aspire client and health check, replacing `AtProtoClientSettings`. - [ ] Every options class goes through `IOptions<T>` with validation and `ValidateOnStart`, and binds from configuration. - [ ] Add a DI resolution test for every `Add*` method. - [ ] The default token store is in-memory, with a startup warning. File or EF storage is an explicit opt-in; today `AddAtProtoServer()` silently writes to `%LocalAppData%`. - [ ] Merge the `AddAtProtoServer` overloads. Use one HttpClient-name constant, and have OAuth use the factory. ## Auth - [ ] Delete the bearer-JWT `AtProtoAuthenticationHandler`. It asks services to accept users' PDS access tokens, builds and leaks a client per request, and sets an empty `did` claim. The service-auth scheme replaces it. - [ ] Move the OAuth cookie login (`AtProtoOAuthService`, options, `AddAtProtoAuthentication`, `MapAtProtoOAuth`) into `ATProtoNet.Server`. Delete `IOAuthClientProvider`. - [ ] The `OAuthClient` is an options-built singleton, so refresh works after a restart. - [ ] Add a **per-DID refresh coordinator** that re-reads the store after taking the lock. Today two concurrent requests for one user both spend the single-use refresh token and log the user out. - [ ] The client factory caches DPoP key material and the logger per DID; it currently spends 25 µs importing the key on every request. - [ ] `FileAtProtoTokenStore`: lock-free reads and per-DID write locks (4.5× throughput at 64 concurrent callers). - [ ] Add `AtProtoClaimTypes` constants. - [ ] Error redirects carry a fixed code unless the exception is an `OAuthException`. Today any exception message lands in the login redirect URL. - [ ] The callback returns a typed result instead of accepting any absolute return URL. - [ ] `MapAtProtoOAuth` can serve `/oauth-client-metadata.json` and the JWKS. ## Blazor - [ ] Rewire `FeedView`, `PostCard`, `ProfileCard` and `ComposePost` to a scoped current-user client resolved through the factory from the auth state. They currently inject an unregistered, non-user `AtProtoClient`. - [ ] Type `FeedSource` (not a magic string); fix `docs/blazor.md`. - [ ] Ship minimal default styles or document the CSS classes. - [ ] Remove `<SupportedPlatform Include="browser" />`. - [ ] Use `InternalsVisibleTo` for tests instead of 13 reflection calls. - [ ] Update `samples/ServerIntegrationSample` and cover each widget with a bUnit or render test. CHANGELOG `Breaking changes`, with a migration section (package references, namespaces, registration calls).
Author
Owner

Follow-ups from the claude review of #167 (non-blocking, picked up here since #136 moves the OAuth hosting):

  • OAuthClient.ResolveFromServerUrlAsync: the success path calls ResolveAuthorizationServerAsync again, redoing the protected-resource metadata fetch from the try block. The metadata cache absorbs it, but the second fetch and parse are wasted; pass the already-resolved result through.
  • AtProtoOAuthService's cross-origin loopback relay: a relay entry that is never redeemed is not revoked when it expires. The session it holds stays live at the AS. This predates #167. Revoke on expiry or eviction when the hosting moves into ATProtoNet.Server.
Follow-ups from the claude review of #167 (non-blocking, picked up here since #136 moves the OAuth hosting): - `OAuthClient.ResolveFromServerUrlAsync`: the success path calls `ResolveAuthorizationServerAsync` again, redoing the protected-resource metadata fetch from the `try` block. The metadata cache absorbs it, but the second fetch and parse are wasted; pass the already-resolved result through. - `AtProtoOAuthService`'s cross-origin loopback relay: a relay entry that is never redeemed is not revoked when it expires. The session it holds stays live at the AS. This predates #167. Revoke on expiry or eviction when the hosting moves into `ATProtoNet.Server`.
Author
Owner

Splitting this into two PRs for reviewability, and so the first half doesn't wait on #133/#134:

  1. Auth, OAuth hosting and Blazor (starting now, branch issue-136-auth-hosting): the whole Auth and Blazor sections, the two #167 follow-ups above, the confidential-client hosting deferred from #132 (client keys in the hosted integration; serving the client metadata and JWKS), and the claim-type constants deferred from #125 (AtProtoServiceAuthDefaults → AtProtoClaimTypes).
  2. Packages and DI (after #133 and #134 merge, since both add EF stores and DI registrations): the Packages and DI sections.

The issue closes with the second PR.

Splitting this into two PRs for reviewability, and so the first half doesn't wait on #133/#134: 1. **Auth, OAuth hosting and Blazor** (starting now, branch `issue-136-auth-hosting`): the whole **Auth** and **Blazor** sections, the two #167 follow-ups above, the confidential-client hosting deferred from #132 (client keys in the hosted integration; serving the client metadata and JWKS), and the claim-type constants deferred from #125 (`AtProtoServiceAuthDefaults` → `AtProtoClaimTypes`). 2. **Packages and DI** (after #133 and #134 merge, since both add EF stores and DI registrations): the **Packages** and **DI** sections. The issue closes with the second PR.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#136
No description provided.