diff --git a/.gitignore b/.gitignore
index 4184f938..db81db57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,4 +105,7 @@ tools/*
coverage-results/*
# Rider
-**/.idea/*
\ No newline at end of file
+**/.idea/*
+
+# macOS
+.DS_Store
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0c1ea17b..77504b07 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,5 +11,11 @@
"files.insertFinalNewline": true,
"editor.detectIndentation": false,
"editor.tabSize": 2,
- "editor.insertSpaces": true
+ "editor.insertSpaces": true,
+ "[csharp]": {
+ "editor.tabSize": 4
+ },
+ "explorer.fileNesting.patterns": {
+ "*.cs": "I${capture}.cs",
+ },
}
diff --git a/Octokit.Reactive/Clients/IObservableMetaClient.cs b/Octokit.Reactive/Clients/IObservableMetaClient.cs
index 97d63d19..35556703 100644
--- a/Octokit.Reactive/Clients/IObservableMetaClient.cs
+++ b/Octokit.Reactive/Clients/IObservableMetaClient.cs
@@ -10,6 +10,11 @@ namespace Octokit.Reactive
///
public interface IObservableMetaClient
{
+ ///
+ /// Returns a client to get public keys for validating request signatures.
+ ///
+ IObservablePublicKeysClient PublicKeys { get; }
+
///
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
///
diff --git a/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs
new file mode 100644
index 00000000..02aae89a
--- /dev/null
+++ b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Octokit.Reactive
+{
+ ///
+ /// A client for GitHub's meta public keys API.
+ ///
+ ///
+ /// See the Secret scanning documentation for more details.
+ ///
+ public interface IObservablePublicKeysClient
+ {
+ ///
+ /// Retrieves public keys for validating request signatures.
+ ///
+ /// Thrown when a general API error occurs.
+ /// An containing public keys for validating request signatures.
+ IObservable Get(PublicKeyType keysType);
+ }
+}
diff --git a/Octokit.Reactive/Clients/ObservableMetaClient.cs b/Octokit.Reactive/Clients/ObservableMetaClient.cs
index 3df88872..d8283ed6 100644
--- a/Octokit.Reactive/Clients/ObservableMetaClient.cs
+++ b/Octokit.Reactive/Clients/ObservableMetaClient.cs
@@ -18,9 +18,16 @@ namespace Octokit.Reactive
{
Ensure.ArgumentNotNull(client, nameof(client));
+ PublicKeys = new ObservablePublicKeysClient(client);
+
_client = client.Meta;
}
+ ///
+ /// Returns a client to manage get public keys for validating request signatures.
+ ///
+ public IObservablePublicKeysClient PublicKeys { get; private set; }
+
///
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
///
diff --git a/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs
new file mode 100644
index 00000000..53396455
--- /dev/null
+++ b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+
+namespace Octokit.Reactive
+{
+ ///
+ /// A client for GitHub's public keys API.
+ ///
+ ///
+ /// See the Secret scanning documentation for more details.
+ ///
+ public class ObservablePublicKeysClient : IObservablePublicKeysClient
+ {
+ private readonly IPublicKeysClient _client;
+
+ public ObservablePublicKeysClient(IGitHubClient client)
+ {
+ Ensure.ArgumentNotNull(client, nameof(client));
+
+ _client = client.Meta.PublicKeys;
+ }
+
+ ///
+ /// Retrieves public keys for validating request signatures.
+ ///
+ /// Thrown when a general API error occurs.
+ /// An containing public keys for validating request signatures.
+ public IObservable Get(PublicKeyType keysType)
+ {
+ return _client.Get(keysType).ToObservable();
+ }
+ }
+}
diff --git a/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs
new file mode 100644
index 00000000..a6b2effb
--- /dev/null
+++ b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs
@@ -0,0 +1,28 @@
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Octokit.Tests.Integration.Clients
+{
+ public class PublicKeysClientTests
+ {
+ public class TheGetMethod
+ {
+ [IntegrationTest]
+ public async Task CanRetrievePublicKeys()
+ {
+ var github = Helper.GetAnonymousClient();
+
+ var result = await github.Meta.PublicKeys.Get(PublicKeyType.SecretScanning);
+
+ Assert.NotNull(result);
+ Assert.Equal(2, result.PublicKeys.Count);
+
+ Assert.NotNull(result.PublicKeys[0].KeyIdentifier);
+ Assert.NotNull(result.PublicKeys[0].Key);
+
+ Assert.NotNull(result.PublicKeys[1].KeyIdentifier);
+ Assert.NotNull(result.PublicKeys[1].Key);
+ }
+ }
+ }
+}
diff --git a/Octokit.Tests/Clients/PublicKeysClientTests.cs b/Octokit.Tests/Clients/PublicKeysClientTests.cs
new file mode 100644
index 00000000..ee416a66
--- /dev/null
+++ b/Octokit.Tests/Clients/PublicKeysClientTests.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Threading.Tasks;
+using NSubstitute;
+using Xunit;
+
+namespace Octokit.Tests.Clients
+{
+ public class PublicKeysClientTests
+ {
+ public class TheCtor
+ {
+ [Fact]
+ public void EnsuresNonNullArguments()
+ {
+ Assert.Throws(() => new PublicKeysClient(null));
+ }
+ }
+
+ public class TheGetMethod
+ {
+ [Fact]
+ public async Task RequestsTheCorrectUrl()
+ {
+ var connection = Substitute.For();
+ var client = new PublicKeysClient(connection);
+
+ await client.Get(PublicKeyType.CopilotApi);
+
+ connection.Received()
+ .Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api"));
+ }
+
+ [Fact]
+ public async Task RequestsCopilotApiPublicKeysEndpoint()
+ {
+ var publicKeys = new MetaPublicKeys(publicKeys: new[] {
+ new MetaPublicKey("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", true),
+ new MetaPublicKey("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", false)
+ });
+
+ var apiConnection = Substitute.For();
+ apiConnection.Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api")).Returns(Task.FromResult(publicKeys));
+
+ var client = new PublicKeysClient(apiConnection);
+
+ var result = await client.Get(PublicKeyType.CopilotApi);
+
+ Assert.Equal(2, result.PublicKeys.Count);
+ Assert.Equal("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", result.PublicKeys[0].KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key);
+ Assert.True(result.PublicKeys[0].IsCurrent);
+
+ Assert.Equal("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", result.PublicKeys[1].KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key);
+ Assert.False(result.PublicKeys[1].IsCurrent);
+
+ apiConnection.Received()
+ .Get(Arg.Is(u => u.ToString() == "meta/public_keys/copilot_api"));
+ }
+
+ [Fact]
+ public async Task RequestSecretScanningPublicKeysEndpoint()
+ {
+ var publicKeys = new MetaPublicKeys(publicKeys: new[] {
+ new MetaPublicKey("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", false),
+ new MetaPublicKey("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", true)
+ });
+
+ var apiConnection = Substitute.For();
+ apiConnection.Get(Arg.Is(u => u.ToString() == "meta/public_keys/secret_scanning")).Returns(Task.FromResult(publicKeys));
+
+ var client = new PublicKeysClient(apiConnection);
+
+ var result = await client.Get(PublicKeyType.SecretScanning);
+
+ Assert.Equal(2, result.PublicKeys.Count);
+ Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", result.PublicKeys[0].KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key);
+ Assert.False(result.PublicKeys[0].IsCurrent);
+
+ Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", result.PublicKeys[1].KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key);
+ Assert.True(result.PublicKeys[1].IsCurrent);
+
+ apiConnection.Received()
+ .Get(Arg.Is(u => u.ToString() == "meta/public_keys/secret_scanning"));
+ }
+ }
+ }
+}
diff --git a/Octokit.Tests/Models/MetaPublicKeysTests.cs b/Octokit.Tests/Models/MetaPublicKeysTests.cs
new file mode 100644
index 00000000..9e5f64ea
--- /dev/null
+++ b/Octokit.Tests/Models/MetaPublicKeysTests.cs
@@ -0,0 +1,44 @@
+using Octokit.Internal;
+using Xunit;
+
+namespace Octokit.Tests.Models
+{
+ public class MetaPublicKeysTests
+ {
+ [Fact]
+ public void CanBeDeserialized()
+ {
+ const string json = @"{
+ ""public_keys"": [
+ {
+ ""key_identifier"": ""90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a"",
+ ""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n"",
+ ""is_current"": false
+ },
+ {
+ ""key_identifier"": ""bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c"",
+ ""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n"",
+ ""is_current"": true
+ }
+ ]
+}
+";
+ var serializer = new SimpleJsonSerializer();
+
+ var keys = serializer.Deserialize(json);
+
+ Assert.NotNull(keys);
+ Assert.Equal(2, keys.PublicKeys.Count);
+
+ var key1 = keys.PublicKeys[0];
+ Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", key1.KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", key1.Key);
+ Assert.False(key1.IsCurrent);
+
+ var key2 = keys.PublicKeys[1];
+ Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", key2.KeyIdentifier);
+ Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", key2.Key);
+ Assert.True(key2.IsCurrent);
+ }
+ }
+}
diff --git a/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs
new file mode 100644
index 00000000..14939de5
--- /dev/null
+++ b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs
@@ -0,0 +1,33 @@
+using System;
+using NSubstitute;
+using Octokit.Reactive;
+using Xunit;
+
+namespace Octokit.Tests.Reactive
+{
+ public class ObservablePublicKeysClientTests
+ {
+ public class TheGetMethod
+ {
+ [Fact]
+ public void CallsIntoClient()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservablePublicKeysClient(gitHubClient);
+
+ client.Get(PublicKeyType.SecretScanning);
+
+ gitHubClient.Meta.PublicKeys.Received(1).Get(PublicKeyType.SecretScanning);
+ }
+ }
+
+ public class TheCtor
+ {
+ [Fact]
+ public void EnsuresNonNullArguments()
+ {
+ Assert.Throws(() => new ObservablePublicKeysClient((IGitHubClient)null));
+ }
+ }
+ }
+}
diff --git a/Octokit/Clients/IMetaClient.cs b/Octokit/Clients/IMetaClient.cs
index f4c21714..9c790060 100644
--- a/Octokit/Clients/IMetaClient.cs
+++ b/Octokit/Clients/IMetaClient.cs
@@ -10,6 +10,11 @@ namespace Octokit
///
public interface IMetaClient
{
+ ///
+ /// Returns a client to get public keys for validating request signatures.
+ ///
+ IPublicKeysClient PublicKeys { get; }
+
///
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
///
diff --git a/Octokit/Clients/IPublicKeysClient.cs b/Octokit/Clients/IPublicKeysClient.cs
new file mode 100644
index 00000000..eae756c8
--- /dev/null
+++ b/Octokit/Clients/IPublicKeysClient.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+
+namespace Octokit
+{
+ ///
+ /// A client for GitHub's meta public keys API.
+ ///
+ ///
+ /// See the Secret scanning documentation for more details.
+ ///
+ public interface IPublicKeysClient
+ {
+ ///
+ /// Retrieves public keys for validating request signatures.
+ ///
+ /// Thrown when a general API error occurs.
+ /// An containing public keys for validating request signatures.
+ Task Get(PublicKeyType keysType);
+ }
+}
diff --git a/Octokit/Clients/MetaClient.cs b/Octokit/Clients/MetaClient.cs
index c683a34b..52a61f14 100644
--- a/Octokit/Clients/MetaClient.cs
+++ b/Octokit/Clients/MetaClient.cs
@@ -18,8 +18,14 @@ namespace Octokit
public MetaClient(IApiConnection apiConnection)
: base(apiConnection)
{
+ PublicKeys = new PublicKeysClient(apiConnection);
}
+ ///
+ /// Returns a client to manage get public keys for validating request signatures.
+ ///
+ public IPublicKeysClient PublicKeys { get; private set; }
+
///
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
///
diff --git a/Octokit/Clients/PublicKeysClient.cs b/Octokit/Clients/PublicKeysClient.cs
new file mode 100644
index 00000000..cc34141f
--- /dev/null
+++ b/Octokit/Clients/PublicKeysClient.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+
+namespace Octokit
+{
+ ///
+ /// A client for GitHub's public keys API.
+ ///
+ ///
+ /// See the Secret scanning documentation for more details.
+ ///
+ public class PublicKeysClient : ApiClient, IPublicKeysClient
+ {
+ ///
+ /// Initializes a new GitHub Meta Public Keys API client.
+ ///
+ /// An API connection.
+ public PublicKeysClient(IApiConnection apiConnection)
+ : base(apiConnection)
+ {
+ }
+
+ ///
+ /// Retrieves public keys for validating request signatures.
+ ///
+ /// Thrown when a general API error occurs.
+ /// An containing public keys for validating request signatures.
+ [ManualRoute("GET", "/meta/public_keys/{keysType}")]
+ public Task Get(PublicKeyType keysType)
+ {
+ return ApiConnection.Get(ApiUrls.PublicKeys(keysType));
+ }
+ }
+}
diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs
index 62c752a5..b70ae025 100644
--- a/Octokit/Helpers/ApiUrls.cs
+++ b/Octokit/Helpers/ApiUrls.cs
@@ -4833,6 +4833,16 @@ namespace Octokit
return "meta".FormatUri();
}
+ ///
+ /// Returns the that returns meta in
+ /// response to a GET request.
+ ///
+ /// The to meta.
+ public static Uri PublicKeys(PublicKeyType keysType)
+ {
+ return "meta/public_keys/{0}".FormatUri(keysType.ToParameter());
+ }
+
///
/// Returns the that returns all organization credentials in
/// response to a GET request.
diff --git a/Octokit/Models/Common/PublicKeyType.cs b/Octokit/Models/Common/PublicKeyType.cs
new file mode 100644
index 00000000..3a8c09a6
--- /dev/null
+++ b/Octokit/Models/Common/PublicKeyType.cs
@@ -0,0 +1,19 @@
+using Octokit.Internal;
+
+namespace Octokit
+{
+ public enum PublicKeyType
+ {
+ ///
+ /// Copilot API public keys for validating request signatures
+ ///
+ [Parameter(Value = "copilot_api")]
+ CopilotApi,
+
+ ///
+ /// Secret scanning public keys for validating request signatures
+ ///
+ [Parameter(Value = "secret_scanning")]
+ SecretScanning
+ }
+}
diff --git a/Octokit/Models/Response/MetaPublicKey.cs b/Octokit/Models/Response/MetaPublicKey.cs
new file mode 100644
index 00000000..05b6a86c
--- /dev/null
+++ b/Octokit/Models/Response/MetaPublicKey.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Octokit
+{
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class MetaPublicKey
+ {
+ public MetaPublicKey() { }
+
+ public MetaPublicKey(string keyIdentifier, string key, bool isCurrent)
+ {
+ KeyIdentifier = keyIdentifier;
+ Key = key;
+ IsCurrent = isCurrent;
+ }
+
+ public string KeyIdentifier { get; protected set; }
+
+ public string Key { get; protected set; }
+
+ public bool IsCurrent { get; protected set; }
+
+ internal string DebuggerDisplay
+ {
+ get { return string.Format(CultureInfo.InvariantCulture, "KeyIdentifier: {0} IsCurrent: {1}", KeyIdentifier, IsCurrent); }
+ }
+ }
+}
diff --git a/Octokit/Models/Response/MetaPublicKeys.cs b/Octokit/Models/Response/MetaPublicKeys.cs
new file mode 100644
index 00000000..7927ab7a
--- /dev/null
+++ b/Octokit/Models/Response/MetaPublicKeys.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Octokit
+{
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class MetaPublicKeys
+ {
+ public MetaPublicKeys() { }
+
+ public MetaPublicKeys(IReadOnlyList publicKeys)
+ {
+ PublicKeys = publicKeys;
+ }
+
+ public IReadOnlyList PublicKeys { get; protected set; }
+
+ internal string DebuggerDisplay
+ {
+ get { return string.Format(CultureInfo.InvariantCulture, "PublicKeys: {0}", PublicKeys.Count); }
+ }
+ }
+}