From 10b3b9f0af5767085b565c464a0d58144f7c06a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:41:51 +0000 Subject: [PATCH 1/5] Initial plan From ab220e724fc1c63ffe86eee6dfe5be548a73129c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:49:30 +0000 Subject: [PATCH 2/5] Add CreatedAt and UpdatedAt properties to 1Password VaultItem Co-authored-by: detunized <245393+detunized@users.noreply.github.com> --- CHANGELOG.md | 3 +++ src/OnePassword/Response.cs | 6 +++++ src/OnePassword/VaultItem.cs | 2 ++ src/PasswordManagerAccess.csproj | 2 +- test/OnePassword/ClientTest.cs | 2 ++ test/OnePassword/ResponseTest.cs | 38 ++++++++++++++++++++++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9735460..41b41c2de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 28.2.0 + - 1Password: added `VaultItem.CreatedAt` and `VaultItem.UpdatedAt` properties + ## 28.1.0 - 1Password: fixed incorrect item and vault IDs detection in Client.GetItem - 1Password: added `NoItem.Inaccessible` for inaccessible vaults and items diff --git a/src/OnePassword/Response.cs b/src/OnePassword/Response.cs index 0b92ae505..e5d5f0677 100644 --- a/src/OnePassword/Response.cs +++ b/src/OnePassword/Response.cs @@ -313,6 +313,12 @@ internal class VaultItem [JsonProperty("trashed", Required = Required.Always)] public readonly string Deleted; + [JsonProperty("createdAt")] + public readonly string CreatedAt; + + [JsonProperty("updatedAt")] + public readonly string UpdatedAt; + [JsonProperty("itemVersion", Required = Required.Always)] public readonly int Version; diff --git a/src/OnePassword/VaultItem.cs b/src/OnePassword/VaultItem.cs index c0765227d..c41eea56b 100644 --- a/src/OnePassword/VaultItem.cs +++ b/src/OnePassword/VaultItem.cs @@ -17,6 +17,8 @@ public class VaultItem public string Name => Overview.Title ?? ""; public string Description => Overview.AdditionalInfo ?? ""; public string Note => Details.Note ?? ""; + public string CreatedAt => _itemInfo.CreatedAt ?? ""; + public string UpdatedAt => _itemInfo.UpdatedAt ?? ""; public Field[] Fields => _fields ??= ParseFields(); diff --git a/src/PasswordManagerAccess.csproj b/src/PasswordManagerAccess.csproj index b4010205e..95a7f2b5d 100644 --- a/src/PasswordManagerAccess.csproj +++ b/src/PasswordManagerAccess.csproj @@ -8,7 +8,7 @@ detunized.net MIT - 28.1.0 + 28.2.0 diff --git a/test/OnePassword/ClientTest.cs b/test/OnePassword/ClientTest.cs index 050b80de0..a1cb77c63 100644 --- a/test/OnePassword/ClientTest.cs +++ b/test/OnePassword/ClientTest.cs @@ -54,6 +54,8 @@ public void GetItem_returns_account() // Assert result.TryPickT0(out var account, out _).ShouldBeTrue(); account.Id.ShouldBe("wm3uxq4xsmb4mghxw6o3s7zrem"); + account.CreatedAt.ShouldBe("2016-08-04T13:15:10Z"); + account.UpdatedAt.ShouldBe("2016-08-04T13:16:07Z"); } [Fact] diff --git a/test/OnePassword/ResponseTest.cs b/test/OnePassword/ResponseTest.cs index c7208f9d2..068533c6c 100644 --- a/test/OnePassword/ResponseTest.cs +++ b/test/OnePassword/ResponseTest.cs @@ -18,5 +18,43 @@ public void VaultItemDetails_parses_with_all_types_of_fields() Assert.Equal(6, details.Sections[0].Fields.Length); Assert.Equal(6, details.Sections[1].Fields.Length); } + + [Fact] + public void VaultItem_parses_createdAt_and_updatedAt() + { + var json = GetFixture("get-vault-item-response"); + var response = JsonConvert.DeserializeObject(json); + Assert.Equal("2016-08-04T13:15:10Z", response.Item.CreatedAt); + Assert.Equal("2016-08-04T13:16:07Z", response.Item.UpdatedAt); + } + + [Fact] + public void VaultItem_handles_missing_createdAt_and_updatedAt() + { + var json = @"{ + ""uuid"": ""test-id"", + ""templateUuid"": ""001"", + ""trashed"": ""N"", + ""itemVersion"": 1, + ""encryptedBy"": ""test-key"", + ""encOverview"": { + ""kid"": ""test-key"", + ""enc"": ""A256GCM"", + ""cty"": ""b5+jwk+json"", + ""iv"": ""test-iv"", + ""data"": ""test-data"" + }, + ""encDetails"": { + ""kid"": ""test-key"", + ""enc"": ""A256GCM"", + ""cty"": ""b5+jwk+json"", + ""iv"": ""test-iv"", + ""data"": ""test-data"" + } + }"; + var item = JsonConvert.DeserializeObject(json); + Assert.Null(item.CreatedAt); + Assert.Null(item.UpdatedAt); + } } } From d39fde71649a853e9421fea324c0015dadead2e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:19:21 +0000 Subject: [PATCH 3/5] Change CreatedAt and UpdatedAt to DateTime? with safe parsing Co-authored-by: detunized <245393+detunized@users.noreply.github.com> --- src/OnePassword/Response.cs | 41 ++++++++++++++++++++++++++++++-- src/OnePassword/VaultItem.cs | 5 ++-- test/OnePassword/ClientTest.cs | 4 ++-- test/OnePassword/ResponseTest.cs | 36 ++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/OnePassword/Response.cs b/src/OnePassword/Response.cs index e5d5f0677..60aaa83cf 100644 --- a/src/OnePassword/Response.cs +++ b/src/OnePassword/Response.cs @@ -313,11 +313,13 @@ internal class VaultItem [JsonProperty("trashed", Required = Required.Always)] public readonly string Deleted; + [JsonConverter(typeof(SafeDateTimeConverter))] [JsonProperty("createdAt")] - public readonly string CreatedAt; + public readonly DateTime? CreatedAt; + [JsonConverter(typeof(SafeDateTimeConverter))] [JsonProperty("updatedAt")] - public readonly string UpdatedAt; + public readonly DateTime? UpdatedAt; [JsonProperty("itemVersion", Required = Required.Always)] public readonly int Version; @@ -498,4 +500,39 @@ internal class ServiceAccountToken [JsonProperty("muk")] public readonly AesKey MasterUnlockKey; } + + // Safely parse DateTime values without throwing exceptions on invalid input + internal class SafeDateTimeConverter : JsonConverter + { + public override DateTime? ReadJson(JsonReader reader, Type objectType, DateTime? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + if (reader.TokenType == JsonToken.Date) + { + // Newtonsoft.Json already parsed it as a DateTime + return reader.Value as DateTime?; + } + + if (reader.TokenType == JsonToken.String) + { + var value = reader.Value as string; + if (string.IsNullOrWhiteSpace(value)) + return null; + + // Try parsing with standard formats + if (DateTime.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.RoundtripKind, out var result)) + return result; + + // Invalid date format - return null instead of throwing + return null; + } + + return null; + } + + public override void WriteJson(JsonWriter writer, DateTime? value, JsonSerializer serializer) => throw new NotImplementedException(); + } } diff --git a/src/OnePassword/VaultItem.cs b/src/OnePassword/VaultItem.cs index c41eea56b..2238b50b6 100644 --- a/src/OnePassword/VaultItem.cs +++ b/src/OnePassword/VaultItem.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using R = PasswordManagerAccess.OnePassword.Response; @@ -17,8 +18,8 @@ public class VaultItem public string Name => Overview.Title ?? ""; public string Description => Overview.AdditionalInfo ?? ""; public string Note => Details.Note ?? ""; - public string CreatedAt => _itemInfo.CreatedAt ?? ""; - public string UpdatedAt => _itemInfo.UpdatedAt ?? ""; + public DateTime? CreatedAt => _itemInfo.CreatedAt; + public DateTime? UpdatedAt => _itemInfo.UpdatedAt; public Field[] Fields => _fields ??= ParseFields(); diff --git a/test/OnePassword/ClientTest.cs b/test/OnePassword/ClientTest.cs index a1cb77c63..c57503f39 100644 --- a/test/OnePassword/ClientTest.cs +++ b/test/OnePassword/ClientTest.cs @@ -54,8 +54,8 @@ public void GetItem_returns_account() // Assert result.TryPickT0(out var account, out _).ShouldBeTrue(); account.Id.ShouldBe("wm3uxq4xsmb4mghxw6o3s7zrem"); - account.CreatedAt.ShouldBe("2016-08-04T13:15:10Z"); - account.UpdatedAt.ShouldBe("2016-08-04T13:16:07Z"); + account.CreatedAt.ShouldBe(new DateTime(2016, 8, 4, 13, 15, 10, DateTimeKind.Utc)); + account.UpdatedAt.ShouldBe(new DateTime(2016, 8, 4, 13, 16, 7, DateTimeKind.Utc)); } [Fact] diff --git a/test/OnePassword/ResponseTest.cs b/test/OnePassword/ResponseTest.cs index 068533c6c..0603d5824 100644 --- a/test/OnePassword/ResponseTest.cs +++ b/test/OnePassword/ResponseTest.cs @@ -1,6 +1,7 @@ // Copyright (C) Dmitry Yakimenko (detunized@gmail.com). // Licensed under the terms of the MIT license. See LICENCE for details. +using System; using Newtonsoft.Json; using Xunit; using R = PasswordManagerAccess.OnePassword.Response; @@ -24,8 +25,8 @@ public void VaultItem_parses_createdAt_and_updatedAt() { var json = GetFixture("get-vault-item-response"); var response = JsonConvert.DeserializeObject(json); - Assert.Equal("2016-08-04T13:15:10Z", response.Item.CreatedAt); - Assert.Equal("2016-08-04T13:16:07Z", response.Item.UpdatedAt); + Assert.Equal(new DateTime(2016, 8, 4, 13, 15, 10, DateTimeKind.Utc), response.Item.CreatedAt); + Assert.Equal(new DateTime(2016, 8, 4, 13, 16, 7, DateTimeKind.Utc), response.Item.UpdatedAt); } [Fact] @@ -56,5 +57,36 @@ public void VaultItem_handles_missing_createdAt_and_updatedAt() Assert.Null(item.CreatedAt); Assert.Null(item.UpdatedAt); } + + [Fact] + public void VaultItem_handles_invalid_createdAt_and_updatedAt() + { + var json = @"{ + ""uuid"": ""test-id"", + ""templateUuid"": ""001"", + ""trashed"": ""N"", + ""createdAt"": ""not-a-date"", + ""updatedAt"": ""also-not-a-date"", + ""itemVersion"": 1, + ""encryptedBy"": ""test-key"", + ""encOverview"": { + ""kid"": ""test-key"", + ""enc"": ""A256GCM"", + ""cty"": ""b5+jwk+json"", + ""iv"": ""test-iv"", + ""data"": ""test-data"" + }, + ""encDetails"": { + ""kid"": ""test-key"", + ""enc"": ""A256GCM"", + ""cty"": ""b5+jwk+json"", + ""iv"": ""test-iv"", + ""data"": ""test-data"" + } + }"; + var item = JsonConvert.DeserializeObject(json); + Assert.Null(item.CreatedAt); + Assert.Null(item.UpdatedAt); + } } } From a704ffc12b8f87e15101bc2583590fd5bf3902ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:37:15 +0000 Subject: [PATCH 4/5] Simplify date handling: use non-nullable DateTime with default value, parse in VaultItem Co-authored-by: detunized <245393+detunized@users.noreply.github.com> --- src/OnePassword/Response.cs | 41 ++------------------------------ src/OnePassword/VaultItem.cs | 17 +++++++++++-- test/OnePassword/ResponseTest.cs | 37 ++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/OnePassword/Response.cs b/src/OnePassword/Response.cs index 60aaa83cf..e5d5f0677 100644 --- a/src/OnePassword/Response.cs +++ b/src/OnePassword/Response.cs @@ -313,13 +313,11 @@ internal class VaultItem [JsonProperty("trashed", Required = Required.Always)] public readonly string Deleted; - [JsonConverter(typeof(SafeDateTimeConverter))] [JsonProperty("createdAt")] - public readonly DateTime? CreatedAt; + public readonly string CreatedAt; - [JsonConverter(typeof(SafeDateTimeConverter))] [JsonProperty("updatedAt")] - public readonly DateTime? UpdatedAt; + public readonly string UpdatedAt; [JsonProperty("itemVersion", Required = Required.Always)] public readonly int Version; @@ -500,39 +498,4 @@ internal class ServiceAccountToken [JsonProperty("muk")] public readonly AesKey MasterUnlockKey; } - - // Safely parse DateTime values without throwing exceptions on invalid input - internal class SafeDateTimeConverter : JsonConverter - { - public override DateTime? ReadJson(JsonReader reader, Type objectType, DateTime? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - if (reader.TokenType == JsonToken.Date) - { - // Newtonsoft.Json already parsed it as a DateTime - return reader.Value as DateTime?; - } - - if (reader.TokenType == JsonToken.String) - { - var value = reader.Value as string; - if (string.IsNullOrWhiteSpace(value)) - return null; - - // Try parsing with standard formats - if (DateTime.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.RoundtripKind, out var result)) - return result; - - // Invalid date format - return null instead of throwing - return null; - } - - return null; - } - - public override void WriteJson(JsonWriter writer, DateTime? value, JsonSerializer serializer) => throw new NotImplementedException(); - } } diff --git a/src/OnePassword/VaultItem.cs b/src/OnePassword/VaultItem.cs index 2238b50b6..3def36664 100644 --- a/src/OnePassword/VaultItem.cs +++ b/src/OnePassword/VaultItem.cs @@ -18,8 +18,8 @@ public class VaultItem public string Name => Overview.Title ?? ""; public string Description => Overview.AdditionalInfo ?? ""; public string Note => Details.Note ?? ""; - public DateTime? CreatedAt => _itemInfo.CreatedAt; - public DateTime? UpdatedAt => _itemInfo.UpdatedAt; + public DateTime CreatedAt => ParseDateTime(_itemInfo.CreatedAt); + public DateTime UpdatedAt => ParseDateTime(_itemInfo.UpdatedAt); public Field[] Fields => _fields ??= ParseFields(); @@ -75,6 +75,19 @@ internal string FindField(string name) return ""; } + internal static DateTime ParseDateTime(string dateString) + { + if (string.IsNullOrWhiteSpace(dateString)) + return default; + + if (DateTime.TryParse(dateString, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.RoundtripKind, out var result)) + return result; + + // Return default if parsing fails + return default; + } + // // Private // diff --git a/test/OnePassword/ResponseTest.cs b/test/OnePassword/ResponseTest.cs index 0603d5824..372dcf919 100644 --- a/test/OnePassword/ResponseTest.cs +++ b/test/OnePassword/ResponseTest.cs @@ -3,6 +3,7 @@ using System; using Newtonsoft.Json; +using PasswordManagerAccess.OnePassword; using Xunit; using R = PasswordManagerAccess.OnePassword.Response; @@ -25,8 +26,8 @@ public void VaultItem_parses_createdAt_and_updatedAt() { var json = GetFixture("get-vault-item-response"); var response = JsonConvert.DeserializeObject(json); - Assert.Equal(new DateTime(2016, 8, 4, 13, 15, 10, DateTimeKind.Utc), response.Item.CreatedAt); - Assert.Equal(new DateTime(2016, 8, 4, 13, 16, 7, DateTimeKind.Utc), response.Item.UpdatedAt); + Assert.Equal("2016-08-04T13:15:10Z", response.Item.CreatedAt); + Assert.Equal("2016-08-04T13:16:07Z", response.Item.UpdatedAt); } [Fact] @@ -85,8 +86,36 @@ public void VaultItem_handles_invalid_createdAt_and_updatedAt() } }"; var item = JsonConvert.DeserializeObject(json); - Assert.Null(item.CreatedAt); - Assert.Null(item.UpdatedAt); + Assert.Equal("not-a-date", item.CreatedAt); + Assert.Equal("also-not-a-date", item.UpdatedAt); + } + + [Fact] + public void VaultItem_ParseDateTime_parses_valid_date() + { + var result = VaultItem.ParseDateTime("2016-08-04T13:15:10Z"); + Assert.Equal(new DateTime(2016, 8, 4, 13, 15, 10, DateTimeKind.Utc), result); + } + + [Fact] + public void VaultItem_ParseDateTime_returns_default_for_null() + { + var result = VaultItem.ParseDateTime(null); + Assert.Equal(default(DateTime), result); + } + + [Fact] + public void VaultItem_ParseDateTime_returns_default_for_empty() + { + var result = VaultItem.ParseDateTime(""); + Assert.Equal(default(DateTime), result); + } + + [Fact] + public void VaultItem_ParseDateTime_returns_default_for_invalid() + { + var result = VaultItem.ParseDateTime("not-a-date"); + Assert.Equal(default(DateTime), result); } } } From d57374722c55b8095ab03d7c592814824c7f0089 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:08:14 +0000 Subject: [PATCH 5/5] Add backing fields to cache parsed CreatedAt and UpdatedAt dates Co-authored-by: detunized <245393+detunized@users.noreply.github.com> --- src/OnePassword/VaultItem.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OnePassword/VaultItem.cs b/src/OnePassword/VaultItem.cs index 3def36664..54904704c 100644 --- a/src/OnePassword/VaultItem.cs +++ b/src/OnePassword/VaultItem.cs @@ -18,8 +18,8 @@ public class VaultItem public string Name => Overview.Title ?? ""; public string Description => Overview.AdditionalInfo ?? ""; public string Note => Details.Note ?? ""; - public DateTime CreatedAt => ParseDateTime(_itemInfo.CreatedAt); - public DateTime UpdatedAt => ParseDateTime(_itemInfo.UpdatedAt); + public DateTime CreatedAt => _createdAt ??= ParseDateTime(_itemInfo.CreatedAt); + public DateTime UpdatedAt => _updatedAt ??= ParseDateTime(_itemInfo.UpdatedAt); public Field[] Fields => _fields ??= ParseFields(); @@ -97,4 +97,6 @@ internal static DateTime ParseDateTime(string dateString) private R.VaultItemOverview? _overview; private R.VaultItemDetails? _details; private Field[]? _fields; + private DateTime? _createdAt; + private DateTime? _updatedAt; }