From 689d01dcecfa32dfd7b45624690cbeff184f86ac Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Tue, 7 Apr 2026 01:25:23 -0400 Subject: [PATCH] Sync SDK: add landing pages, credential profiles, and missing card fields - Add landing pages support (list/create/update) - Add credential profiles support (list/create) - Add missing AccessCard fields: OrganizationName, Department, Location, SiteName, Workstation, MailStop, CompanyAddress - Add 9 new tests for all new features - Update feature matrix in README - Bump version to 1.4.0 --- AccessGridTest/ConsoleServiceTests.cs | 281 ++++++++++++++++++++++++++ README.md | 144 ++++++++++++- src/AccessGrid.csproj | 2 +- src/AccessGrid/AccessGridClient.cs | 2 +- src/AccessGrid/ConsoleService.cs | 70 +++++++ src/AccessGrid/Models.cs | 234 +++++++++++++++++++++ 6 files changed, 730 insertions(+), 3 deletions(-) diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index 6349044..8f56aa2 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -953,4 +953,285 @@ public async Task HIDOrgsActivateAsync_ShouldCompleteRegistration() } #endregion + + #region LandingPages + + [Test] + public async Task ListLandingPagesAsync_ShouldReturnLandingPages() + { + var json = """ + [ + { + "id": "lp_1", + "name": "Miami Office", + "created_at": "2025-03-01T00:00:00Z", + "kind": "universal", + "password_protected": false, + "logo_url": "https://example.com/logo.png" + }, + { + "id": "lp_2", + "name": "NYC Office", + "created_at": "2025-03-02T00:00:00Z", + "kind": "universal", + "password_protected": true, + "logo_url": null + } + ] + """; + StubHttpResponse(json); + + var result = await _client.Console.ListLandingPagesAsync(); + + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0].Id, Is.EqualTo("lp_1")); + Assert.That(result[0].Name, Is.EqualTo("Miami Office")); + Assert.That(result[0].Kind, Is.EqualTo("universal")); + Assert.That(result[0].PasswordProtected, Is.False); + Assert.That(result[0].LogoUrl, Is.EqualTo("https://example.com/logo.png")); + Assert.That(result[1].PasswordProtected, Is.True); + Assert.That(result[1].LogoUrl, Is.Null); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Get && + req.RequestUri!.ToString().Contains("/v1/console/landing-pages") + )), Times.Once); + } + + [Test] + public async Task ListLandingPagesAsync_ShouldHandleEmptyList() + { + StubHttpResponse("[]"); + + var result = await _client.Console.ListLandingPagesAsync(); + + Assert.That(result, Is.Empty); + } + + [Test] + public async Task CreateLandingPageAsync_ShouldCreateLandingPage() + { + var json = """ + { + "id": "lp_new", + "name": "Miami Office Access Pass", + "created_at": "2025-03-01T00:00:00Z", + "kind": "universal", + "password_protected": false, + "logo_url": null + } + """; + StubHttpResponse(json); + + var result = await _client.Console.CreateLandingPageAsync(new CreateLandingPageRequest + { + Name = "Miami Office Access Pass", + Kind = "universal", + AdditionalText = "Welcome to the Miami Office", + BgColor = "#f1f5f9", + AllowImmediateDownload = true + }); + + Assert.That(result.Id, Is.EqualTo("lp_new")); + Assert.That(result.Name, Is.EqualTo("Miami Office Access Pass")); + Assert.That(result.Kind, Is.EqualTo("universal")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/landing-pages") + )), Times.Once); + } + + [Test] + public async Task UpdateLandingPageAsync_ShouldUpdateLandingPage() + { + var json = """ + { + "id": "lp_1", + "name": "Updated Miami Office", + "created_at": "2025-03-01T00:00:00Z", + "kind": "universal", + "password_protected": false, + "logo_url": null + } + """; + StubHttpResponse(json); + + var result = await _client.Console.UpdateLandingPageAsync("lp_1", new UpdateLandingPageRequest + { + Name = "Updated Miami Office", + AdditionalText = "Welcome! Tap below to get your access pass.", + BgColor = "#e2e8f0" + }); + + Assert.That(result.Id, Is.EqualTo("lp_1")); + Assert.That(result.Name, Is.EqualTo("Updated Miami Office")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Put && + req.RequestUri!.ToString().Contains("/v1/console/landing-pages/lp_1") + )), Times.Once); + } + + #endregion + + #region CredentialProfilesService + + [Test] + public async Task CredentialProfilesListAsync_ShouldReturnProfiles() + { + var json = """ + [ + { + "id": "cp_1", + "aid": "F0394148", + "name": "Main Office Profile", + "apple_id": "apple-123", + "created_at": "2025-03-01T00:00:00Z", + "card_storage": "2K", + "keys": [ + { "ex_id": "key_1", "label": "Master Key", "keys_diversified": true, "source_key_index": 0 } + ], + "files": [ + { "ex_id": "file_1", "file_type": "Standard", "file_size": 32, "communication_settings": "Full", "read_rights": "key_1", "write_rights": "key_1", "read_write_rights": "key_1", "change_rights": "key_1" } + ] + } + ] + """; + StubHttpResponse(json); + + var result = await _client.Console.CredentialProfiles.ListAsync(); + + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0].Id, Is.EqualTo("cp_1")); + Assert.That(result[0].Aid, Is.EqualTo("F0394148")); + Assert.That(result[0].Name, Is.EqualTo("Main Office Profile")); + Assert.That(result[0].AppleId, Is.EqualTo("apple-123")); + Assert.That(result[0].CardStorage, Is.EqualTo("2K")); + Assert.That(result[0].Keys, Has.Count.EqualTo(1)); + Assert.That(result[0].Keys[0].ExId, Is.EqualTo("key_1")); + Assert.That(result[0].Keys[0].KeysDiversified, Is.True); + Assert.That(result[0].Files, Has.Count.EqualTo(1)); + Assert.That(result[0].Files[0].FileType, Is.EqualTo("Standard")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Get && + req.RequestUri!.ToString().Contains("/v1/console/credential-profiles") + )), Times.Once); + } + + [Test] + public async Task CredentialProfilesListAsync_ShouldHandleEmptyList() + { + StubHttpResponse("[]"); + + var result = await _client.Console.CredentialProfiles.ListAsync(); + + Assert.That(result, Is.Empty); + } + + [Test] + public async Task CredentialProfilesCreateAsync_ShouldCreateProfile() + { + var json = """ + { + "id": "cp_new", + "aid": "F0394148", + "name": "Main Office Profile", + "apple_id": null, + "created_at": "2025-03-01T00:00:00Z", + "card_storage": "2K", + "keys": [ + { "ex_id": "key_1", "label": "Master Key", "keys_diversified": false, "source_key_index": null }, + { "ex_id": "key_2", "label": "Read Key", "keys_diversified": false, "source_key_index": null } + ], + "files": [] + } + """; + StubHttpResponse(json); + + var result = await _client.Console.CredentialProfiles.CreateAsync(new CreateCredentialProfileRequest + { + Name = "Main Office Profile", + AppName = "KEY-ID-main", + Keys = new[] + { + new KeyParam { Value = "your_32_char_hex_master_key_here" }, + new KeyParam { Value = "your_32_char_hex__read_key__here" } + } + }); + + Assert.That(result.Id, Is.EqualTo("cp_new")); + Assert.That(result.Aid, Is.EqualTo("F0394148")); + Assert.That(result.Name, Is.EqualTo("Main Office Profile")); + Assert.That(result.Keys, Has.Count.EqualTo(2)); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/credential-profiles") + )), Times.Once); + } + + #endregion + + #region AccessCard New Fields + + [Test] + public async Task AccessCard_ShouldDeserializeNewFields() + { + var json = """ + { + "id": "card_1", + "install_url": "https://install.example.com/card_1", + "state": "active", + "full_name": "Jane Doe", + "organization_name": "Acme Corp", + "title": "Engineering Manager", + "department": "Engineering", + "location": "San Francisco", + "site_name": "HQ Building A", + "workstation": "4F-207", + "mail_stop": "MS-401", + "company_address": "123 Main St, San Francisco, CA 94105" + } + """; + StubHttpResponse(json); + + var result = await _client.AccessCards.GetAsync("card_1"); + + Assert.That(result.OrganizationName, Is.EqualTo("Acme Corp")); + Assert.That(result.Title, Is.EqualTo("Engineering Manager")); + Assert.That(result.Department, Is.EqualTo("Engineering")); + Assert.That(result.Location, Is.EqualTo("San Francisco")); + Assert.That(result.SiteName, Is.EqualTo("HQ Building A")); + Assert.That(result.Workstation, Is.EqualTo("4F-207")); + Assert.That(result.MailStop, Is.EqualTo("MS-401")); + Assert.That(result.CompanyAddress, Is.EqualTo("123 Main St, San Francisco, CA 94105")); + } + + [Test] + public async Task AccessCard_NewFieldsAreNullWhenAbsent() + { + var json = """ + { + "id": "card_2", + "install_url": "https://install.example.com/card_2", + "state": "active", + "full_name": "John Smith" + } + """; + StubHttpResponse(json); + + var result = await _client.AccessCards.GetAsync("card_2"); + + Assert.That(result.OrganizationName, Is.Null); + Assert.That(result.Department, Is.Null); + Assert.That(result.Location, Is.Null); + Assert.That(result.SiteName, Is.Null); + Assert.That(result.Workstation, Is.Null); + Assert.That(result.MailStop, Is.Null); + Assert.That(result.CompanyAddress, Is.Null); + } + + #endregion } diff --git a/README.md b/README.md index 3c27604..4188eac 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Official C# SDK for interacting with the AccessGrid API. ## Installation ``` -Install-Package accessgrid -Version 1.3.0 +Install-Package accessgrid -Version 1.4.0 ``` ## Authentication @@ -449,6 +449,143 @@ public async Task GenerateProvisioningCredentialsAsync() } ``` +### Landing Pages + +#### List Landing Pages + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task ListLandingPagesAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + var landingPages = await client.Console.ListLandingPagesAsync(); + + foreach (var page in landingPages) + { + Console.WriteLine($"ID: {page.Id}, Name: {page.Name}, Kind: {page.Kind}"); + Console.WriteLine($" Password Protected: {page.PasswordProtected}"); + if (page.LogoUrl != null) + Console.WriteLine($" Logo URL: {page.LogoUrl}"); + } +} +``` + +#### Create a Landing Page + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task CreateLandingPageAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + var landingPage = await client.Console.CreateLandingPageAsync(new CreateLandingPageRequest + { + Name = "Miami Office Access Pass", + Kind = "universal", + AdditionalText = "Welcome to the Miami Office", + BgColor = "#f1f5f9", + AllowImmediateDownload = true + }); + + Console.WriteLine($"Landing page created: {landingPage.Id}"); + Console.WriteLine($"Name: {landingPage.Name}, Kind: {landingPage.Kind}"); +} +``` + +#### Update a Landing Page + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task UpdateLandingPageAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + var landingPage = await client.Console.UpdateLandingPageAsync("0xlandingpage1d", new UpdateLandingPageRequest + { + Name = "Updated Miami Office Access Pass", + AdditionalText = "Welcome! Tap below to get your access pass.", + BgColor = "#e2e8f0" + }); + + Console.WriteLine($"Landing page updated: {landingPage.Id}"); + Console.WriteLine($"Name: {landingPage.Name}"); +} +``` + +### Credential Profiles + +#### List Credential Profiles + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task ListProfilesAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + var profiles = await client.Console.CredentialProfiles.ListAsync(); + + foreach (var profile in profiles) + { + Console.WriteLine($"ID: {profile.Id}, Name: {profile.Name}, AID: {profile.Aid}"); + } +} +``` + +#### Create a Credential Profile + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task CreateProfileAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + var profile = await client.Console.CredentialProfiles.CreateAsync(new CreateCredentialProfileRequest + { + Name = "Main Office Profile", + AppName = "KEY-ID-main", + Keys = new[] + { + new KeyParam { Value = "your_32_char_hex_master_key_here" }, + new KeyParam { Value = "your_32_char_hex__read_key__here" } + } + }); + + Console.WriteLine($"Profile created: {profile.Id}"); + Console.WriteLine($"AID: {profile.Aid}"); +} +``` + ### Webhooks ```csharp @@ -909,6 +1046,11 @@ public class AccessCardsApiTests | GET /v1/console/pass-template-pairs | `Console.ListPassTemplatePairsAsync()` | Y | | POST /v1/console/card-templates/{id}/ios_preflight | `Console.IosPreflightAsync()` | Y | | GET /v1/console/ledger-items | `Console.GetLedgerItemsAsync()` | Y | +| GET /v1/console/landing-pages | `Console.ListLandingPagesAsync()` | Y | +| POST /v1/console/landing-pages | `Console.CreateLandingPageAsync()` | Y | +| PUT /v1/console/landing-pages/{id} | `Console.UpdateLandingPageAsync()` | Y | +| GET /v1/console/credential-profiles | `Console.CredentialProfiles.ListAsync()` | Y | +| POST /v1/console/credential-profiles | `Console.CredentialProfiles.CreateAsync()` | Y | | GET /v1/console/webhooks | `Console.Webhooks.ListAsync()` | Y | | POST /v1/console/webhooks | `Console.Webhooks.CreateAsync()` | Y | | DELETE /v1/console/webhooks/{id} | `Console.Webhooks.DeleteAsync()` | Y | diff --git a/src/AccessGrid.csproj b/src/AccessGrid.csproj index 0673fbd..361cac5 100644 --- a/src/AccessGrid.csproj +++ b/src/AccessGrid.csproj @@ -4,7 +4,7 @@ netstandard2.0 8.0 accessgrid - 1.3.0 + 1.4.0 AccessGrid AccessGrid Official C# SDK for the AccessGrid API diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 6aa4135..1afe9b8 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -19,7 +19,7 @@ public class AccessGridClient : IAccessGridClient, IApiService private readonly string _accountId; private readonly string _secretKey; private readonly JsonSerializerOptions _jsonOptions; - private const string Version = "1.3.0"; + private const string Version = "1.4.0"; /// /// Service for managing access cards diff --git a/src/AccessGrid/ConsoleService.cs b/src/AccessGrid/ConsoleService.cs index 7b05e75..36643f9 100644 --- a/src/AccessGrid/ConsoleService.cs +++ b/src/AccessGrid/ConsoleService.cs @@ -20,11 +20,17 @@ public class ConsoleService /// public WebhooksService Webhooks { get; } + /// + /// Credential profile management services + /// + public CredentialProfilesService CredentialProfiles { get; } + internal ConsoleService(IApiService apiService) { _apiService = apiService; HID = new HIDService(apiService); Webhooks = new WebhooksService(apiService); + CredentialProfiles = new CredentialProfilesService(apiService); } /// @@ -137,6 +143,39 @@ public async Task GetLedgerItemsAsync(int? page = null, int return response ?? new LedgerItemsResponse(); } + /// + /// Lists all landing pages + /// + /// List of landing pages + public async Task> ListLandingPagesAsync() + { + var response = await _apiService.GetAsync>("/v1/console/landing-pages"); + return response ?? new List(); + } + + /// + /// Creates a new landing page + /// + /// Landing page creation details + /// Newly created landing page + public async Task CreateLandingPageAsync(CreateLandingPageRequest request) + { + var response = await _apiService.PostAsync("/v1/console/landing-pages", request); + return response; + } + + /// + /// Updates an existing landing page + /// + /// ID of the landing page to update + /// Landing page update details + /// Updated landing page + public async Task UpdateLandingPageAsync(string landingPageId, UpdateLandingPageRequest request) + { + var response = await _apiService.PutAsync($"/v1/console/landing-pages/{landingPageId}", request); + return response; + } + /// /// Retrieves iOS In-App Provisioning identifiers for a card template and access pass /// @@ -217,6 +256,37 @@ internal HIDService(IApiService apiService) } } + /// + /// Service for managing credential profiles + /// + public class CredentialProfilesService + { + private readonly IApiService _apiService; + + internal CredentialProfilesService(IApiService apiService) + { + _apiService = apiService; + } + + /// + /// Lists all credential profiles + /// + public async Task> ListAsync() + { + var response = await _apiService.GetAsync>("/v1/console/credential-profiles"); + return response ?? new List(); + } + + /// + /// Creates a new credential profile + /// + public async Task CreateAsync(CreateCredentialProfileRequest request) + { + var response = await _apiService.PostAsync("/v1/console/credential-profiles", request); + return response; + } + } + /// /// Service for managing HID organizations /// diff --git a/src/AccessGrid/Models.cs b/src/AccessGrid/Models.cs index 2395f14..d535c36 100644 --- a/src/AccessGrid/Models.cs +++ b/src/AccessGrid/Models.cs @@ -120,6 +120,48 @@ public AccessCard() [JsonPropertyName("title")] public string Title { get; set; } + /// + /// Name of the organization the employee belongs to + /// + [JsonPropertyName("organization_name")] + public string OrganizationName { get; set; } + + /// + /// Department within the organization + /// + [JsonPropertyName("department")] + public string Department { get; set; } + + /// + /// Location or office name + /// + [JsonPropertyName("location")] + public string Location { get; set; } + + /// + /// Site name (e.g., building or campus) + /// + [JsonPropertyName("site_name")] + public string SiteName { get; set; } + + /// + /// Workstation identifier + /// + [JsonPropertyName("workstation")] + public string Workstation { get; set; } + + /// + /// Mail stop code + /// + [JsonPropertyName("mail_stop")] + public string MailStop { get; set; } + + /// + /// Company address + /// + [JsonPropertyName("company_address")] + public string CompanyAddress { get; set; } + /// /// ISO8601 timestamp when the card becomes active /// @@ -1036,4 +1078,196 @@ public class CardTemplateEvent [JsonPropertyName("hid_org_id")] public string HIDOrgId { get; set; } } + + /// + /// A landing page configuration + /// + public class LandingPage + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } + + [JsonPropertyName("kind")] + public string Kind { get; set; } + + [JsonPropertyName("password_protected")] + public bool PasswordProtected { get; set; } + + [JsonPropertyName("logo_url")] + public string LogoUrl { get; set; } + } + + /// + /// Parameters for creating a landing page + /// + public class CreateLandingPageRequest + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("kind")] + public string Kind { get; set; } + + [JsonPropertyName("additional_text")] + public string AdditionalText { get; set; } + + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + [JsonPropertyName("allow_immediate_download")] + public bool? AllowImmediateDownload { get; set; } + + [JsonPropertyName("password")] + public string Password { get; set; } + + [JsonPropertyName("is_2fa_enabled")] + public bool? Is2faEnabled { get; set; } + + [JsonPropertyName("logo")] + public string Logo { get; set; } + } + + /// + /// Parameters for updating a landing page + /// + public class UpdateLandingPageRequest + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("additional_text")] + public string AdditionalText { get; set; } + + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + [JsonPropertyName("allow_immediate_download")] + public bool? AllowImmediateDownload { get; set; } + + [JsonPropertyName("password")] + public string Password { get; set; } + + [JsonPropertyName("is_2fa_enabled")] + public bool? Is2faEnabled { get; set; } + + [JsonPropertyName("logo")] + public string Logo { get; set; } + } + + /// + /// A key within a credential profile + /// + public class CredentialProfileKey + { + [JsonPropertyName("ex_id")] + public string ExId { get; set; } + + [JsonPropertyName("label")] + public string Label { get; set; } + + [JsonPropertyName("keys_diversified")] + public bool KeysDiversified { get; set; } + + [JsonPropertyName("source_key_index")] + public int? SourceKeyIndex { get; set; } + } + + /// + /// A file within a credential profile + /// + public class CredentialProfileFile + { + [JsonPropertyName("ex_id")] + public string ExId { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + [JsonPropertyName("file_size")] + public int FileSize { get; set; } + + [JsonPropertyName("communication_settings")] + public string CommunicationSettings { get; set; } + + [JsonPropertyName("read_rights")] + public string ReadRights { get; set; } + + [JsonPropertyName("write_rights")] + public string WriteRights { get; set; } + + [JsonPropertyName("read_write_rights")] + public string ReadWriteRights { get; set; } + + [JsonPropertyName("change_rights")] + public string ChangeRights { get; set; } + } + + /// + /// A credential profile + /// + public class CredentialProfile + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("aid")] + public string Aid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("apple_id")] + public string AppleId { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } + + [JsonPropertyName("card_storage")] + public string CardStorage { get; set; } + + [JsonPropertyName("keys")] + public List Keys { get; set; } + + [JsonPropertyName("files")] + public List Files { get; set; } + } + + /// + /// A key parameter for creating a credential profile + /// + public class KeyParam + { + [JsonPropertyName("value")] + public string Value { get; set; } + + [JsonPropertyName("keys_diversified")] + public bool? KeysDiversified { get; set; } + + [JsonPropertyName("source_key_index")] + public int? SourceKeyIndex { get; set; } + } + + /// + /// Parameters for creating a credential profile + /// + public class CreateCredentialProfileRequest + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("app_name")] + public string AppName { get; set; } + + [JsonPropertyName("file_id")] + public string FileId { get; set; } + + [JsonPropertyName("keys")] + public KeyParam[] Keys { get; set; } + } } \ No newline at end of file