CodeLogic.SocialConnect 5.0.121

This package has a SemVer 2.0.0 package version: 5.0.121+4885b82.
dotnet add package CodeLogic.SocialConnect --version 5.0.121
                    
NuGet\Install-Package CodeLogic.SocialConnect -Version 5.0.121
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeLogic.SocialConnect" Version="5.0.121" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeLogic.SocialConnect" Version="5.0.121" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.SocialConnect" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeLogic.SocialConnect --version 5.0.121
                    
#r "nuget: CodeLogic.SocialConnect, 5.0.121"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeLogic.SocialConnect@5.0.121
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeLogic.SocialConnect&version=5.0.121
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.SocialConnect&version=5.0.121
                    
Install as a Cake Tool

CodeLogic.SocialConnect

NuGet License: MIT

Discord webhooks and Steam integration for CodeLogic 5 — send rich Discord notifications, read Steam player profiles, bans, and game libraries, and add "Sign in through Steam" to a website.

Four services behind one library: a Discord webhook sender (text, rich embeds, full payloads), a Steam profile reader (players, bans, owned games — cached), Steam authentication for validating game-session tickets, and Steam sign-in (OpenID 2.0) for websites. No external NuGet dependencies — built on System.Net.Http.Json.

Install

dotnet add package CodeLogic.SocialConnect

Quick start

using CL.SocialConnect;
using CL.SocialConnect.Models;   // SteamPlayer, SocialConnectErrorCodes, ...
using CodeLogic.Core.Results;

await Libraries.LoadAsync<SocialConnectLibrary>();
await CodeLogic.ConfigureAsync();
await CodeLogic.StartAsync();

var social = Libraries.Get<SocialConnectLibrary>();

// Discord — send a webhook text message (Result, no value)
Result sent = await social.Discord.SendMessageAsync("Server restarted");
if (sent.IsFailure)
    Console.WriteLine(sent.Error?.Message);

// Steam — fetch a player profile (Result<SteamPlayer>)
if (social.HasSteam)
{
    Result<SteamPlayer> player = await social.Steam.GetPlayerAsync("76561198012345678");
    Console.WriteLine(player switch
    {
        SteamPlayer p   => $"{p.PersonaName} — {p.ProfileUrl}",
        NotFoundError   => "No such player",                       // social.steam.player_not_found
        TimeoutError    => "Steam did not answer in time",         // social.steam.timeout
        Error e         => $"Steam lookup failed: {e.Code}",       // a SocialConnectErrorCodes constant
        null            => "No result",
    });
}

// Steam — "Sign in through Steam" on a website (OpenID 2.0; returns the Steam ID)
if (social.HasSteamOpenId)
{
    // 1. Start: a random per-attempt state goes into return_to AND into an HttpOnly cookie, then redirect.
    var state = SteamOpenIdService.CreateState();
    response.Cookies.Append("steam_state", state, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax });
    string loginUrl = social.OpenId.BuildLoginUrl($"https://example.com/auth/steam/callback?state={state}");

    // 2. Callback: read the state back from the cookie, rebuild the exact return_to, verify.
    //    A missing cookie, or one that differs from the ?state= Steam echoes, means this browser never started the login.
    var expectedState = request.Cookies["steam_state"];
    if (expectedState is null || expectedState != request.Query["state"]) return Unauthorized();
    var returnTo = $"https://example.com/auth/steam/callback?state={expectedState}";
    Result<SteamOpenIdResult> signIn = await social.OpenId.VerifyAsync(request.QueryString.Value, returnTo);
    response.Cookies.Delete("steam_state");
    if (signIn is SteamOpenIdResult user)
        Console.WriteLine($"Signed in as {user.SteamId}");   // a replayed assertion fails with social.steam.openid_replayed
}

Set Steam.OpenIdRealm (for example https://example.com/) so a return_to built from user input cannot send the browser elsewhere.

Accessing a disabled service throws InvalidOperationException. Guard with HasDiscord / HasSteam / HasSteamAuth / HasSteamOpenId when a service may be turned off — Steam is disabled by default.

Features

  • Discord webhooks — SendMessageAsync (plain text, max 2000 chars), SendEmbedAsync (up to 10 rich embeds), and SendAsync (full DiscordWebhookMessage); all return Result. Only https://discord.com/api/webhooks/{id}/{token} URLs (and discordapp.com, ptb./canary.) are accepted (IsValidWebhookUrl); logs and events carry the URL with the token redacted.
  • Rich embeds — title, description, colour, fields, author, footer, image, thumbnail, and timestamp via DiscordEmbed / DiscordEmbedField.
  • Mention control — DiscordAllowedMentions (with .None / .All) whitelists which roles/users/@everyone may ping; opt-in TTS via DiscordWebhookMessage.Tts.
  • Steam profiles — GetPlayerAsync, GetPlayerBansAsync, GetOwnedGamesAsync return Result<T> with computed helpers (IsPublic, IsInGame, HasAnyBan, TotalPlaytime, LastPlayed). Steam IDs must be SteamID64s (SteamIds.IsSteamId64), otherwise social.steam.invalid_steamid.
  • Built-in cache — Steam reads are cached for CacheTtlSeconds (default 300), at most MaxCacheEntries (default 10 000) per cache with the oldest evicted when full; clear on demand with Steam.ClearCache().
  • Steam authentication — Auth.AuthenticateAsync validates a session ticket (per-call appId override) and returns Result<SteamTicketIdentity>; a rejected ticket is an UnauthorizedError.
  • Steam sign-in — OpenId.BuildLoginUrl / OpenId.VerifyAsync implement "Sign in through Steam" (OpenID 2.0): local checks of the signed assertion (endpoint, return_to, identity, signed fields, response_nonce age), a replay check against an IOpenIdNonceStore (social.steam.openid_replayed), then confirmation by Steam (check_authentication). OpenIdRealm pins return_to to your site; CreateState() gives the per-attempt state.
  • Cancellation — every network call takes a CancellationToken (last parameter); a cancelled call is an UnavailableError (social.steam.cancelled / social.discord.cancelled), never an exception.
  • Stable error codes — every failure is a CodeLogic Error whose Code is a SocialConnectErrorCodes constant (social.discord.*, social.steam.*); switch on the code or the Error kind, not on the message.
  • Testable — every service has a constructor taking an HttpMessageHandler (fakes, outbound proxies).
  • Events — WebhookSentEvent, SteamProfileFetchedEvent, SteamAuthenticatedEvent, and SteamOpenIdVerifiedEvent on the CodeLogic event bus.

Configuration

Auto-generated on first run as config.socialconnect.json:

{
  "enabled": true,
  "discord": {
    "enabled": true,
    "defaultWebhookUrl": "https://discord.com/api/webhooks/xxx/yyy",
    "timeoutSeconds": 10,
    "defaultUsername": "MyApp",
    "defaultAvatarUrl": "https://example.com/bot.png"
  },
  "steam": {
    "enabled": false,
    "apiKey": "your-steam-web-api-key",
    "authEnabled": false,
    "appId": "",
    "openIdEnabled": true,
    "openIdNonceMaxAgeSeconds": 300,
    "openIdRealm": "https://example.com/",
    "cacheTtlSeconds": 300,
    "maxCacheEntries": 10000,
    "timeoutSeconds": 15,
    "apiBaseUrl": "https://api.steampowered.com"
  }
}
Setting Default Description
Enabled true Master switch for the whole library.
Discord.Enabled true Enable the Discord webhook service.
Discord.DefaultWebhookUrl "" Webhook used when a call omits webhookUrl (secret); must be a Discord webhook URL.
Discord.TimeoutSeconds 10 HTTP timeout for Discord calls (1–120).
Discord.DefaultUsername "" Default sender name for SendMessageAsync / SendEmbedAsync.
Discord.DefaultAvatarUrl "" Default sender avatar for SendMessageAsync / SendEmbedAsync.
Steam.Enabled false Enable the Steam services. The profile service requires ApiKey; sign-in (OpenID) alone does not.
Steam.ApiKey "" Steam Web API key (secret) — get one at steamcommunity.com/dev/apikey.
Steam.AuthEnabled false Enable ticket authentication; requires AppId and ApiKey.
Steam.AppId "" Your game's Steam App ID (used by auth).
Steam.OpenIdEnabled true Enable "Sign in through Steam" (OpenID) while Steam is enabled. OpenID itself needs no key.
Steam.OpenIdNonceMaxAgeSeconds 300 Oldest sign-in assertion accepted, by its response_nonce timestamp (social.steam.openid_expired); 0 = no age limit.
Steam.OpenIdRealm "" Site every sign-in return_to must lie inside (absolute http(s) URL). Empty = derived from the return URL; set it unless return_to is a constant.
Steam.CacheTtlSeconds 300 Profile cache lifetime in seconds (must be > 0).
Steam.MaxCacheEntries 10000 Upper bound per profile cache (players, bans, games); the oldest entries are evicted when full.
Steam.TimeoutSeconds 15 HTTP timeout for Steam calls (1–120).
Steam.ApiBaseUrl https://api.steampowered.com Steam Web API base URL; must be https without user info (the API key is sent to it).

Changing config while running

config.socialconnect.json applies live when changed through Libraries.UpdateConfigAsync, Libraries.ReloadConfigAsync, or the config file watcher. Only the service whose settings changed is rebuilt:

  • Discord settings: social.Discord is replaced.
  • Steam settings: social.Steam, social.Auth and social.OpenId are replaced; the profile cache starts empty.
  • Enabled flags: each service, and the whole library, can be switched on or off.

A replaced service is disposed only after its request timeout, so requests already running on it finish. Read social.Discord / social.Steam / social.Auth / social.OpenId when you use them rather than keeping an old instance. An invalid configuration is logged and ignored; the last valid one stays active.

Documentation

Full guide: CL.SocialConnect documentation

Requirements

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.121 42 11/10/2026
5.0.118-preview 34 11/8/2026
5.0.117-preview 38 11/7/2026
5.0.116-preview 41 11/1/2026
5.0.115-preview 92 11/1/2026
5.0.114-preview 39 10/31/2026
5.0.113-preview 39 10/31/2026
4.9.106 52 10/30/2026
4.9.100 51 10/26/2026
4.9.98 54 10/25/2026
4.8.93 73 9/23/2026
4.8.87 81 9/13/2026
4.8.85 65 9/12/2026
4.6.72 234 6/20/2026
4.6.69-preview 63 6/20/2026
4.5.2 142 5/24/2026
4.5.2-preview.68 94 6/20/2026
4.5.1 196 5/24/2026
4.5.1-preview.56 125 5/24/2026
4.4.1 141 5/24/2026
Loading failed

# CL.SocialConnect — Changelog

All notable changes to **CodeLogic.SocialConnect** are documented here. Versions follow
[Semantic Versioning](https://semver.org/).

## 5.0 (2026-11-10)

### Changed

- Record only provider-verified nonces; cap verification concurrency and replay-store capacity, refusing overload without evicting valid nonces. Failed assertions consume no replay entries. Use the shared bounded log formatter.

### Breaking

- Targets **.NET 11** (`net11.0`) and requires **CodeLogic 5.0** (`[5.0.0, 6.0.0)`). On .NET 10, stay on
 4.9.x. CodeLogic 5 makes `Result<T>` / `Result` C# 15 unions and `Error` a closed record with
 kind subtypes (`NotFoundError`, `ValidationError`, ...). Results can now be matched
 exhaustively with `switch`.
- **The `SocialError` enum is removed; error codes are `SocialConnectErrorCodes` constants.** The codes keep their
 values (`social.discord.*`, `social.steam.*`), and messages no longer start with the enum name in brackets.

 | Old | New |
 |---|---|
 | `SocialError` enum | `SocialConnectErrorCodes` string constants, matched against `Error.Code` |
 | `"[WebhookDeliveryFailed] HTTP 400"` | `"Webhook delivery failed: HTTP 400"` (`social.discord.delivery_failed`) |
 | `"[Timeout] ..."`, `"[NetworkError] ..."`, `"[AuthenticationFailed] ..."`, `"[UserNotFound] ..."`, `"[SerializationError] ..."`, `"[ConfigurationError] ..."` | the same message without the bracketed prefix; also in the failure text of `WebhookSentEvent`, `SteamAuthenticatedEvent` and `SteamOpenIdVerifiedEvent` |
 | `"social.discord.no_url"`, `"social.steam.timeout"`, ... (string literals) | `SocialConnectErrorCodes.DiscordNoUrl`, `SocialConnectErrorCodes.SteamTimeout`, ... (same values) |

 Code that parsed the `[Name]` prefix out of `Error.Message` must switch on `Error.Code` (or the `Error` kind).
- `Auth.AuthenticateAsync` returns `Result<SteamTicketIdentity>` instead of `Result<SteamAuthResult>`.
 The mutable `SteamAuthResult` class (with `IsAuthenticated`, `ErrorDescription` and `0`/`1` ints for
 bans) is gone: the immutable `SteamTicketIdentity(SteamId, OwnerSteamId, AppId, VacBanned, PublisherBanned)`
 record (plus `IsFamilyShared`) is only ever a success, and every failure is an `Error` kind —
 `ValidationError` `social.steam.invalid_ticket` / `social.steam.no_appid`, `UnauthorizedError`
 `social.steam.auth_failed`, `TimeoutError` `social.steam.timeout`, `UnavailableError`
 `social.steam.network_error`, `InternalError` `social.steam.request_rejected` / `parse_error` / `error`.
- A ticket that is not hex is now refused locally with `social.steam.invalid_ticket` (it used to be
 pasted unescaped into the request URL). A 4xx from Steam (e.g. a bad API key) is now
 `InternalError` `social.steam.request_rejected` instead of `UnavailableError` `social.steam.network_error`.
- **Steam OpenID checks `openid.response_nonce`.** `VerifyAsync` never looked at the nonce, so an intercepted
 assertion stayed usable indefinitely. It now refuses, before asking Steam, a nonce without a valid
 `yyyy-MM-ddTHH:mm:ssZ` prefix or dated more than a minute ahead (`social.steam.openid_invalid`) and an
 assertion older than the new `SteamConfig.OpenIdNonceMaxAgeSeconds` (default 300; `0` turns the age check
 off) with `UnauthorizedError` `social.steam.openid_expired`. `SteamOpenIdResult` gains the required
 `ResponseNonce` and `NonceIssuedAtUtc`: record accepted nonces and refuse a repeat to stop replays inside
 the window. Code that constructs `SteamOpenIdResult` itself must set both.
- **Discord webhook URLs are restricted to Discord (SSRF).** `SendMessageAsync` / `SendEmbedAsync` / `SendAsync`
 posted to any URL they were given. Only `https` URLs on `discord.com` / `discordapp.com` (and their `ptb.` /
 `canary.` subdomains) with a `/api[/v{n}]/webhooks/{id}/{token}` path are accepted now; anything else fails
 with `ValidationError` `social.discord.invalid_url` before a request, and redirects are not followed. A
 `Discord.DefaultWebhookUrl` that is not such a URL fails configuration validation.
- **`WebhookSentEvent.WebhookUrl` no longer carries the webhook token.** It holds the redacted URL
 (`https://discord.com/api/webhooks/{id}/***`); the new `WebhookId` property holds the id.
- **Steam profile reads validate the Steam ID.** `GetPlayerAsync`, `GetPlayerBansAsync` and `GetOwnedGamesAsync`
 spliced `steamId` and the API key unescaped into the query string, so `"7656…&key=…"` could add or override
 parameters. A Steam ID that is not a SteamID64 (17 digits, `7656119…`) now fails with `ValidationError`
 `social.steam.invalid_steamid` before a request, and both values are URL-escaped.
- **`Steam.ApiBaseUrl` must be an absolute `https` URL without user info, query or fragment** (the API key travels
 in every request's query string, so `http` would send it in clear). `SocialConnectConfig.Validate` refuses
 anything else, and `SteamProfileService` / `SteamAuthenticationService` throw `ArgumentException` for such a
 config instead of building a client for it. `SteamConfig.TryGetApiBaseUri` is the check.
- **Steam sign-in refuses a replayed assertion.** `SteamOpenIdService.VerifyAsync` records each verified
 `openid.response_nonce` in an `IOpenIdNonceStore` (default: in memory, per process) and refuses one it has seen
 with `UnauthorizedError` `social.steam.openid_replayed`. The nonce is recorded only after Steam confirms the
 assertion, so a failed confirmation consumes no entry. Applications no longer need their own
 nonce tracking, unless they run several servers without a shared store.
- **Cancellation is an error, not an exception.** `SteamOpenIdService.VerifyAsync` rethrew the caller's
 `OperationCanceledException`; it now returns `UnavailableError` `social.steam.cancelled`. The new
 `CancellationToken` parameters on the other services map the same way (`social.steam.cancelled`,
 `social.discord.cancelled`), following the CL.Storage convention.
- **`Auth.AuthenticateAsync` checks Steam's answer.** A `steamid` or `ownersteamid` that is not a SteamID64 fails
 with `InternalError` `social.steam.parse_error`; no identity is published or returned.
- `SteamProfileService` throws `ArgumentNullException` for a `null` config (it used to throw a
 `NullReferenceException` later).

### Added

- `DiscordWebhookService.IsValidWebhookUrl(url)` and `DiscordWebhookService.RedactWebhookUrl(url)`.
- `SteamIds.IsSteamId64(value)` (`CL.SocialConnect.Models.Steam`).
- `WebhookSentEvent.WebhookId`.

- `SteamAuthenticationService(SteamConfig, HttpMessageHandler, ILogger?, IEventBus?)` constructor for
 tests and outbound proxies, matching `SteamOpenIdService`.
- `DiscordWebhookService(DiscordConfig, HttpMessageHandler, …)` and `SteamProfileService(SteamConfig, HttpMessageHandler, …)`
 constructors, so every service can be tested or routed through a proxy. A `SocketsHttpHandler` or
 `HttpClientHandler` given to the Discord service has automatic redirects switched off.
- **`Steam.OpenIdRealm`** (default empty): the site Steam sign-in belongs to. When set, every `return_to` passed to
 `BuildLoginUrl` must lie inside it (same scheme, host and port, path under the realm's path) or the call throws
 `ArgumentException`, and it is the realm Steam shows unless the call gives one. Without it, and without a
 `realm` argument, the realm is still derived from `return_to` itself, which accepts any absolute URL: set
 `OpenIdRealm` whenever `return_to` is not a constant in your code.
- `SteamOpenIdService.CreateState()`: 256 random bits as a URL-safe base64url string, for the per-attempt state in
 `return_to` and the cookie.
- `IOpenIdNonceStore` / `InMemoryOpenIdNonceStore`, and a `nonceStore` parameter on both `SteamOpenIdService`
 constructors. The library shares one in-memory store across live config changes.
- **`Steam.MaxCacheEntries`** (default 10 000): each profile cache (players, bans, games) is bounded; expired entries
 are dropped when read and the oldest entries are evicted when a cache is full. They grew without limit before.
- `CancellationToken` (last parameter, default `default`) on `SteamProfileService.GetPlayerAsync` /
 `GetPlayerBansAsync` / `GetOwnedGamesAsync`, `SteamAuthenticationService.AuthenticateAsync` and
 `DiscordWebhookService.SendMessageAsync` / `SendEmbedAsync` / `SendAsync`.

### Fixed

- **A live config change cannot wipe the Steam services.** `OnConfigChangedAsync` cleared `Steam`, `Auth` and
 `OpenId` before constructing their replacements, so a constructor failure left all three gone. The new services
 are built first and swapped in afterwards, and a configuration the services refuse is logged and ignored.
- **A hostile OpenID parameter name no longer reaches logs and events verbatim.** The name of a repeated
 `openid.*` parameter was echoed into the log line, the `SteamOpenIdVerifiedEvent` and the error message after
 URL-decoding, so `%0D%0A` could forge log lines. Control characters are now replaced and the name is cut to 64
 characters.
- `DiscordWebhookService.SendAsync` disposes the `HttpResponseMessage`.

- **Steam sign-in works without a Steam Web API key**, as documented. Validation demanded `ApiKey` whenever
 Steam was enabled, so OpenID could not be used on its own. With `Steam.Enabled` and `OpenIdEnabled` on and no
 key, the config is valid and only `OpenId` is created (`HasSteam` is `false`). Ticket authentication
 (`AuthEnabled`) now requires the key explicitly.
- **Live config changes are validated by the library too.** An invalid configuration that reaches
 `OnConfigChangedAsync` is logged and ignored, and the running services keep the last valid one; later changes
 are compared with the configuration actually running.
- `DiscordUser.GetAvatarUrl` no longer throws on a non-numeric discriminator, and escapes the id and avatar hash.

### Documentation

- The model reference matches the code: the Discord and Steam models are (non-sealed) classes with `List<>`
 collections, `DiscordEmbed.Timestamp` is a `DateTime?`, and the embed sub-models use `required` members.
 The examples use collection expressions and compile against the library.
- `DiscordUser` and `DiscordOAuthToken` are documented as data models for applications that run Discord OAuth2
 themselves; the library does not implement Discord OAuth2.
- Configuration examples in the README and the docs use the camelCase keys the CodeLogic loader reads.
 The PascalCase keys shown before are matched case-sensitively and were ignored, leaving defaults active.
- The live config section no longer says "CodeLogic 4.9+". The docs say the nonce is recorded after Steam confirms
 the sign-in, list `social.steam.openid_busy`, the Discord and Steam profile error codes, and that only
 `SendMessageAsync` / `SendEmbedAsync` apply the default username and avatar and only `GetPlayerAsync` publishes
 `SteamProfileFetchedEvent`. Examples match on the result union (`r is SteamPlayer p`) instead of reading
 `Value` without a null check. The README's trailing relative link, which broke on nuget.org, is gone.

## 2026-10-30

### Fixed

- **Health with every service switched off is Healthy** (`No services enabled`). It reported Unhealthy
 "Not initialized — no services active", although services are only ever absent because the config
 turns them off — an app that uses only some of the library (or none of it yet) showed as broken.

## 2026-10-26

### Added

- **Steam sign-in (OpenID 2.0)** for websites: `SteamOpenIdService`, exposed as `social.OpenId` with
 `HasSteamOpenId`. `BuildLoginUrl(returnTo, realm?)` builds the "Sign in through Steam" redirect;
 `VerifyAsync(query, expectedReturnTo)` checks the signed assertion locally (Steam's endpoint and
 namespace, exact `return_to`, `claimed_id` = `identity` as a SteamID64, required signed fields, no
 repeated parameters), then asks Steam to confirm it (`check_authentication`), and returns the
 player's 64-bit Steam ID as `Result<SteamOpenIdResult>`. Publishes `SteamOpenIdVerifiedEvent`.
- `Steam.OpenIdEnabled` (default `true`): sign-in is available whenever Steam is enabled (which
 requires `Steam.ApiKey`). It follows live config changes like the other Steam services: a Steam
 settings change replaces `social.OpenId`, and it can be switched on or off without a restart.

## 2026-10-24

### Added

- **Live config changes** (requires CodeLogic 4.9+). `SocialConnectLibrary` implements
 `IConfigReloadable`: a change to `config.socialconnect.json` made through `Libraries.UpdateConfigAsync`,
 `Libraries.ReloadConfigAsync` or the config file watcher is applied without a restart, rebuilding only
 the service whose settings changed (`Discord`, or `Steam` + `Auth`). Services and the library can be
 switched on or off. A replaced service is disposed after its request timeout, so running requests finish.

## 2026-09-12

### Changed

- Unified the version line with the CodeLogic framework on **4.8.x**. Every official
 library and the framework now share one `major.minor`, so a given `4.8.<patch>`
 means the same generation across all packages.
- `version.txt` moved from `4.6` to `4.8`. The patch component remains the CI run
 number, composed at pack time; `AssemblyVersion` stays pinned at `Major.Minor.0.0`
 (now `4.8.0.0`) so every patch in the line loads interchangeably.

## 2026-06-20

### Fixed

- Discord sends now validate the 2000-character content limit and 10-embed limit
 up front, returning a clear validation error instead of a failed HTTP
 round-trip.
- Corrected the `CommunityVisibilityState` documentation comment (the Steam Web
 API returns 1 = not visible / 3 = public to third parties; the previous
 comment listed nonexistent levels).

### Documentation

- Full rewrite of the README and the `docs/libs/socialconnect.md` guide to the
 current house style.
- README restructured to the standard layout (badges, tagline, intro, Install,
 Quick start, Features, Configuration with table + JSON, Documentation,
 Requirements, License) and now links the published docs page.
- Corrected the `Result` contract throughout: Discord methods return `Result`
 (no value); Steam profile and auth methods return `Result<T>`. Examples use
 `IsSuccess` / `IsFailure`, `.Value`, and `Error?.Message`.
- Documented the canonical load sequence (`LoadAsync` → `ConfigureAsync` →
 `StartAsync` → `Libraries.Get`) and the `HasDiscord` / `HasSteam` /
 `HasSteamAuth` guards (Steam disabled by default).
- Added a deep guide covering the three services, Discord webhook methods and
 full model reference (with a rich-embed example), the Steam profile/bans/games
 models and computed properties, Steam ticket authentication, caching
 (`CacheTtlSeconds`, `ClearCache`), configuration, events, and the health check.

## [4.5.2] — 2026-06-20

### Documentation

- Corrected the README Quick Start: the text-message helper is
 `Discord.SendMessageAsync(...)`, not `SendAsync(string, username:)`.
- Fixed the README configuration example to use PascalCase JSON keys and added the
 previously omitted `DefaultAvatarUrl`, `AuthEnabled`, `AppId`, and `ApiBaseUrl` fields.
- Documented previously undocumented user-facing API surface:
 - Discord `SendAsync(DiscordWebhookMessage)`, `DiscordAllowedMentions`
   (`.None` / `.All`), and the `Tts` flag.
 - Steam `SteamProfileService.ClearCache()` and the `GetOwnedGamesAsync`
   `includeAppInfo` parameter.
 - Per-call `appId` override on `Auth.AuthenticateAsync`.
 - `SteamPlayer` (`IsPublic`, `IsInGame`, `AccountCreated`), `SteamPlayerBans`
   (`HasAnyBan`), and `SteamGame` (`TotalPlaytime`, `RecentPlaytime`, `LastPlayed`,
   `GetIconUrl`) computed helpers; the `DiscordUser` model.
 - `HasDiscord` / `HasSteam` / `HasSteamAuth` availability properties and the
   `InvalidOperationException` thrown when a disabled service is accessed.
 - Event-bus events `WebhookSentEvent`, `SteamProfileFetchedEvent`, and
   `SteamAuthenticatedEvent`, and the `SocialError` failure-code enum.

## [4.5.0] — 2026-05-24

### Changed

- **Unified versioning.** All CodeLogic.Libs now share a single version line
 controlled by `version.txt` in the repo root. This is a version alignment
 release — no functional changes to this library.
## [4.0.4] — 2026-04-16

### Changed

- README + manifest refresh for the v4 baseline. No functional changes vs 4.0.3.
- `LibraryManifest.Version` now reads from assembly metadata.

## [4.0.2] — 2026-04-09

### Changed

- Annotated SocialConnect configuration with `[ConfigField]` for the admin UI surface.
- Aligned with the v4 baseline across all libraries.

### Fixed

- Closed resource leaks in the OAuth client + reworked the health check so
 it no longer mutates internal state as a side-effect.

## [4.0.0] — 2026-04-09

Major rewrite. Republished as v4.0.0 to reset the version line under the
unified v4 baseline. OAuth/OIDC providers (Google, Discord, Steam,
generic OIDC) with a uniform abstraction.

### Notes

- Earlier history is retained in the
 [git log](https://github.com/zyntal-com/CodeLogic.Libs/commits/main/CL.SocialConnect).