Compare commits

..

3 Commits

Author SHA1 Message Date
Jedd Morgan 74d40e40a9 Optimization for the disk store string writing to avoid memory allocations (#466)
.NET Build and Publish / build (push) Has been cancelled
2026-03-31 10:07:08 +00:00
dependabot[bot] c81692ee5a chore(deps): bump codecov/codecov-action from 5 to 6 (#465)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 21:42:44 +01:00
Jedd Morgan 7f50987201 Auth flow updates (#464)
.NET Build and Publish / build (push) Has been cancelled
* Auth flow updates

* remove obsolete flag on refresh
2026-03-26 14:42:21 +01:00
6 changed files with 48 additions and 73 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
continue-on-error: true
with:
fail_ci_if_error: true
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
continue-on-error: true
with:
fail_ci_if_error: true
+2 -2
View File
@@ -154,10 +154,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
activity?.SetStatus(SdkActivityStatusCode.Ok);
return ret;
}
catch (Exception)
catch (Exception ex)
{
activity?.SetStatus(SdkActivityStatusCode.Error);
// Don't record exception as it's rethrown.
activity?.RecordException(ex);
throw;
}
}
+8 -41
View File
@@ -142,59 +142,26 @@ public sealed class AccountManager(
/// <summary>
/// Refetches the <paramref name="account"/> information, including <see cref="ServerInfo"/> and <see cref="UserInfo"/>
/// If the <see cref="Account.token"/> looks to be expired, this function will also attempt to use the <see cref="Account.refreshToken"/> to refresh it.
///
/// Will only mutate <paramref name="account"/> in memory only, and only if successful.
/// </summary>
/// <seealso cref="UpdateAccount"/>
/// <param name="account"></param>
/// <param name="cancellationToken"></param>
/// <exception cref="AggregateException">Thrown if</exception>
/// <exception cref="GraphQLHttpRequestException"></exception>
public async Task UpdateAccountInMemory(Account account, CancellationToken cancellationToken = default)
{
Uri url = account.serverInfo.migration?.movedTo ?? new(account.serverInfo.url);
ActiveUserServerInfoResponse userServerInfo;
try
ActiveUserServerInfoResponse userServerInfo = await accountFactory
.GetUserServerInfo(url, account.token, cancellationToken)
.ConfigureAwait(false);
if (userServerInfo.activeUser == null)
{
userServerInfo = await accountFactory
.GetUserServerInfo(url, account.token, cancellationToken)
.ConfigureAwait(false);
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
}
catch (GraphQLHttpRequestException ex)
{
// Failed to fetch info, perhaps the token is expired?
// Attempt to refresh it
TokenExchangeResponse refreshTokenResponse;
try
{
refreshTokenResponse = await authFlow
.GetRefreshedToken(
account.refreshToken.NotNull("No refresh token provided"),
url,
AuthApp.ConnectorsV3,
cancellationToken
)
.ConfigureAwait(false);
userServerInfo = await accountFactory
.GetUserServerInfo(url, refreshTokenResponse.token, cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex2)
{
throw new AggregateException("Failed to update account information", ex, ex2);
}
account.token = refreshTokenResponse.token;
account.refreshToken = refreshTokenResponse.refreshToken;
logger.LogInformation(ex, "Account token has been refreshed");
}
account.userInfo = userServerInfo.activeUser.NotNull();
account.userInfo = userServerInfo.activeUser;
account.serverInfo = userServerInfo.serverInfo;
//This is a bit gross, since id is not marked nullable
//but this will force re-generate the id (e.g. if the user's email, or servers url has changed)
+30 -27
View File
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
@@ -42,34 +43,45 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
Uri tokenEndpoint = new(serverUrl, "/oauth/token");
string codeVerifier = GenerateCodeVerifier();
string challenge;
string codeChallengeMethod;
var req = await client.GetAsync(tokenEndpoint, cancellationToken).ConfigureAwait(false);
Uri authnVerify;
using var req = await client.GetAsync(tokenEndpoint, cancellationToken).ConfigureAwait(false);
bool useLegacyEndpoint = req.StatusCode != HttpStatusCode.OK;
if (useLegacyEndpoint)
{
challenge = codeVerifier;
string challenge = codeVerifier; // Old endpoint only supports PKCE "plain" method
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}", UriKind.Relative);
tokenEndpoint = new(serverUrl, "/auth/token");
codeChallengeMethod = "";
}
else
{
challenge = GenerateCodeChallenge(codeVerifier);
codeChallengeMethod = "?code_challenge_method=S256";
string challenge = GenerateCodeChallenge(codeVerifier);
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}?code_challenge_method=S256", UriKind.Relative);
}
Uri endpoint = new(serverUrl, $"/authn/verify/{authApp.AppId}/{challenge}{codeChallengeMethod}");
Uri endpoint = new(serverUrl, authnVerify);
_ = Process.Start(new ProcessStartInfo(endpoint.ToString()) { UseShellExecute = true });
string accessCode = await RunListenerWithTimeout(authApp.CallbackUrl, timeout, cancellationToken)
.ConfigureAwait(false);
object body = useLegacyEndpoint
? new
{
appId = authApp.AppId,
appSecret = authApp.AppSecret,
accessCode = accessCode,
challenge = codeVerifier,
}
: new
{
appId = authApp.AppId,
accessCode = accessCode,
codeVerifier = codeVerifier,
};
return await ExchangeAccessCodeForToken(
client,
accessCode,
authApp,
useLegacyEndpoint ? challenge : null,
!useLegacyEndpoint ? codeVerifier : null,
JsonConvert.SerializeObject(body, _serializerSettings),
tokenEndpoint,
cancellationToken
)
@@ -141,7 +153,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
refreshToken = refreshToken,
};
using var content = new StringContent(JsonConvert.SerializeObject(body));
using var content = new StringContent(JsonConvert.SerializeObject(body, _serializerSettings));
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await client
.PostAsync(new Uri(serverUrl, "/auth/token"), content, cancellationToken)
@@ -248,24 +260,12 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
private async Task<TokenExchangeResponse> ExchangeAccessCodeForToken(
HttpClient client,
string accessCode,
AuthApp authApp,
string? challenge,
string? codeVerifier,
string jsonContent,
Uri tokenEndpoint,
CancellationToken cancellationToken
)
{
var body = new
{
appId = authApp.AppId,
appSecret = authApp.AppSecret,
accessCode = accessCode,
challenge = challenge,
codeVerifier = codeVerifier,
};
using StringContent content = new(JsonConvert.SerializeObject(body, _serializerSettings));
using StringContent content = new(jsonContent);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using HttpResponseMessage response = await client
@@ -282,6 +282,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
return JsonConvert.DeserializeObject<TokenExchangeResponse>(read, _serializerSettings).NotNull();
}
[Pure]
public static string GenerateCodeVerifier()
{
#if NET8_0_OR_GREATER
@@ -296,6 +297,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
return Base64UrlEncode(codeVerifierData);
}
[Pure]
public static string GenerateCodeChallenge(string codeVerifier)
{
#if NET8_0_OR_GREATER
@@ -312,6 +314,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
return Base64UrlEncode(challengeData);
}
[Pure]
private static string Base64UrlEncode(
#if NET8_0_OR_GREATER
ReadOnlySpan<byte> bytes
+6 -1
View File
@@ -65,7 +65,12 @@ public sealed class DiskStore
await foreach (var item in _channel.ReadAllAsync(_cancellationToken).ConfigureAwait(false))
{
await writer.WriteLineAsync($"{item.Id}\t{item.SpeckleType}\t{item.Json}").ConfigureAwait(false);
await writer.WriteAsync(item.Id).ConfigureAwait(false);
await writer.WriteAsync('\t').ConfigureAwait(false);
await writer.WriteAsync(item.SpeckleType).ConfigureAwait(false);
await writer.WriteAsync('\t').ConfigureAwait(false);
await writer.WriteAsync(item.Json.Value).ConfigureAwait(false);
await writer.WriteLineAsync().ConfigureAwait(false);
}
#if NET8_0_OR_GREATER
await writer.FlushAsync(_cancellationToken).ConfigureAwait(false);