diff --git a/AGENTS.md b/AGENTS.md index bce1399..f898c88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,3 +19,8 @@ This repository contains `OnePassword.NET`, a .NET wrapper for the 1Password CLI - Use the GitHub CLI (`gh`) for GitHub-related operations whenever possible. - Prefer `gh` for pull request workflows, including creating, viewing, checking out, and reviewing pull requests. - If `gh` is unavailable or unauthenticated, note that clearly before falling back to another approach. + +## Generated Assets + +- Do not read, search, or summarize generated documentation/site assets unless the user explicitly asks for them. +- In particular, avoid generated docfx output and bundled vendor assets such as minified JavaScript, CSS, or copied third-party files; prefer the markdown and source files under `docfx/` instead. diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md new file mode 100644 index 0000000..9862d96 --- /dev/null +++ b/NEXT_RELEASE.md @@ -0,0 +1,29 @@ +# Draft release description for x.x.x + +## Breaking changes + +- `ShareItem(...)` now returns `ItemShareResult` instead of `void`. +- The single-email `ShareItem(...)` overloads were removed. Use a collection of email addresses for restricted links, or omit the collection entirely for unrestricted links. + +## Highlights + +- Merged PR `#92` to fix `ArchiveDocument(IDocument, IVault)` so it archives documents instead of deleting them. +- Redesigned item sharing so unrestricted links are supported and the created share URL is returned to the caller. +- Trimmed the CLI version string returned on Windows so `Version` no longer includes a trailing newline. +- Clarified in the docs that `GetItems(...)` returns summary items and `GetItem(...)` should be used before working with hydrated fields. +- Added regression coverage for archive behavior, item sharing, version handling, and adding a new field to an existing built-in item. + +## Migration + +Before: + +```csharp +onePassword.ShareItem(item, vault, "recipient@example.com"); +``` + +After: + +```csharp +var restrictedShare = onePassword.ShareItem(item, vault, new[] { "recipient@example.com" }); +var unrestrictedShare = onePassword.ShareItem(item, vault); +``` diff --git a/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs new file mode 100644 index 0000000..d538658 --- /dev/null +++ b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs @@ -0,0 +1,285 @@ +using System.Globalization; +using System.Runtime.InteropServices; +using OnePassword.Common; +using OnePassword.Documents; +using OnePassword.Items; +using OnePassword.Vaults; + +namespace OnePassword; + +[TestFixture] +public class OnePasswordManagerCommandTests +{ + private static readonly string[] ParsedRecipients = ["one@example.com", "two@example.com"]; + + [Test] + public void VersionIsTrimmed() + { + using var fakeCli = new FakeCli(versionOutput: "2.32.1\r\n"); + + var manager = fakeCli.CreateManager(); + + Assert.That(manager.Version, Is.EqualTo("2.32.1")); + } + + [Test] + public void ArchiveDocumentObjectOverloadUsesArchiveCommand() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ArchiveDocument(new TestDocument("document-id"), new TestVault("vault-id")); + + Assert.That(fakeCli.LastArguments, Does.StartWith("document delete document-id --vault vault-id --archive")); + } + + [Test] + public void ArchiveDocumentStringOverloadUsesArchiveCommand() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ArchiveDocument("document-id", "vault-id"); + + Assert.That(fakeCli.LastArguments, Does.StartWith("document delete document-id --vault vault-id --archive")); + } + + [Test] + public void ArchiveItemObjectOverloadUsesArchiveCommand() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ArchiveItem(new TestItem("item-id"), new TestVault("vault-id")); + + Assert.That(fakeCli.LastArguments, Does.StartWith("item delete item-id --vault vault-id --archive")); + } + + [Test] + public void ArchiveItemStringOverloadUsesArchiveCommand() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ArchiveItem("item-id", "vault-id"); + + Assert.That(fakeCli.LastArguments, Does.StartWith("item delete item-id --vault vault-id --archive")); + } + + [Test] + public void ShareItemWithoutEmailsOmitsEmailsFlag() + { + using var fakeCli = new FakeCli(nextOutput: "https://share.example/item\r\n"); + var manager = fakeCli.CreateManager(); + + var result = manager.ShareItem("item-id", "vault-id"); + + Assert.Multiple(() => + { + Assert.That(result.Url, Is.EqualTo(new Uri("https://share.example/item"))); + Assert.That(result.RawResponse, Is.EqualTo("https://share.example/item")); + Assert.That(fakeCli.LastArguments, Does.StartWith("item share item-id --vault vault-id")); + Assert.That(fakeCli.LastArguments, Does.Not.Contain("--emails")); + }); + } + + [Test] + public void ShareItemWithSingleEmailUsesEmailsFlag() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ShareItem("item-id", "vault-id", ["recipient@example.com"]); + + Assert.That(fakeCli.LastArguments, Does.Contain("--emails recipient@example.com")); + } + + [Test] + public void ShareItemWithMultipleEmailsUsesCommaSeparatedEmails() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ShareItem("item-id", "vault-id", ["one@example.com", "two@example.com"]); + + Assert.That(fakeCli.LastArguments, Does.Contain("--emails one@example.com,two@example.com")); + } + + [Test] + public void ShareItemWithEmptyEmailCollectionOmitsEmailsFlag() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ShareItem("item-id", "vault-id", Array.Empty()); + + Assert.That(fakeCli.LastArguments, Does.Not.Contain("--emails")); + } + + [Test] + public void ShareItemWithExpiresInUsesExpiresInFlag() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ShareItem("item-id", "vault-id", expiresIn: TimeSpan.FromDays(7)); + + Assert.That(fakeCli.LastArguments, Does.Contain("--expires-in 7d")); + } + + [Test] + public void ShareItemWithViewOnceUsesViewOnceFlag() + { + using var fakeCli = new FakeCli(); + var manager = fakeCli.CreateManager(); + + manager.ShareItem("item-id", "vault-id", viewOnce: true); + + Assert.That(fakeCli.LastArguments, Does.Contain("--view-once")); + } + + [Test] + public void ShareItemParsesStructuredShareResult() + { + const string response = """ + { + "share_link": "https://share.example/item", + "expires_at": "2026-03-15T12:00:00Z", + "view_once": true, + "recipients": [ + { "email": "one@example.com" }, + { "address": "two@example.com" } + ] + } + """; + + using var fakeCli = new FakeCli(nextOutput: response); + var manager = fakeCli.CreateManager(); + + var result = manager.ShareItem("item-id", "vault-id"); + + Assert.Multiple(() => + { + Assert.That(result.Url, Is.EqualTo(new Uri("https://share.example/item"))); + Assert.That(result.ExpiresAt, Is.EqualTo(DateTimeOffset.Parse("2026-03-15T12:00:00Z", CultureInfo.InvariantCulture))); + Assert.That(result.ViewOnce, Is.True); + Assert.That(result.Recipients, Is.EqualTo(ParsedRecipients)); + Assert.That(result.RawResponse, Does.Contain("\"share_link\": \"https://share.example/item\"")); + }); + } + + private sealed class FakeCli : IDisposable + { + private readonly string _argumentsPath; + private readonly string _directoryPath; + private readonly string _nextOutputPath; + private readonly string _versionOutputPath; + + public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}") + { + _directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + _argumentsPath = Path.Combine(_directoryPath, "last-arguments.txt"); + _nextOutputPath = Path.Combine(_directoryPath, "next-output.txt"); + _versionOutputPath = Path.Combine(_directoryPath, "version-output.txt"); + + Directory.CreateDirectory(_directoryPath); + File.WriteAllText(_nextOutputPath, nextOutput); + File.WriteAllText(_versionOutputPath, versionOutput); + + var executablePath = Path.Combine(_directoryPath, ExecutableName); + File.WriteAllText(executablePath, GetScript()); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + File.SetUnixFileMode(executablePath, + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute); + } + } + + public string ExecutableName { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "op.cmd" : "op"; + + public string LastArguments => File.Exists(_argumentsPath) ? File.ReadAllText(_argumentsPath) : ""; + + public OnePasswordManager CreateManager() + { + return new OnePasswordManager(options => + { + options.Path = _directoryPath; + options.Executable = ExecutableName; + options.ServiceAccountToken = "test-token"; + }); + } + + public void Dispose() + { + if (Directory.Exists(_directoryPath)) + Directory.Delete(_directoryPath, true); + } + + private static string GetScript() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? """ + @echo off + setlocal + > "%~dp0last-arguments.txt" echo %* + if "%~1"=="--version" ( + type "%~dp0version-output.txt" + exit /b 0 + ) + type "%~dp0next-output.txt" + """ + : """ + #!/bin/sh + script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) + printf '%s' "$*" > "$script_dir/last-arguments.txt" + if [ "$1" = "--version" ]; then + cat "$script_dir/version-output.txt" + exit 0 + fi + cat "$script_dir/next-output.txt" + """; + } + } + + private sealed class TestDocument(string id) : IDocument + { + public string Id { get; } = id; + } + + private sealed class TestItem(string id) : IItem + { + public string Id { get; } = id; + } + + private sealed class TestVault(string id, string name = "Test Vault") : IVault + { + public string Id { get; } = id; + + public string Name { get; } = name; + + public void Deconstruct(out string vaultId, out string vaultName) + { + vaultId = Id; + vaultName = Name; + } + + public bool Equals(IResult? other) => other is not null && string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase); + + public override bool Equals(object? obj) => obj is IResult other && Equals(other); + + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Id); + + public int CompareTo(IResult? other) => other is null ? 1 : string.Compare(Id, other.Id, StringComparison.Ordinal); + + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + if (obj is IResult other) + return CompareTo(other); + throw new ArgumentException($"Object must be of type {nameof(IResult)}.", nameof(obj)); + } + } +} diff --git a/OnePassword.NET.Tests/TestItems.cs b/OnePassword.NET.Tests/TestItems.cs index a95b0ff..cf84273 100644 --- a/OnePassword.NET.Tests/TestItems.cs +++ b/OnePassword.NET.Tests/TestItems.cs @@ -21,6 +21,8 @@ public class TestItems : TestsBase private const string FinalUsername = "Test Username"; private const FieldType FinalType = FieldType.Concealed; private const string FinalValue = "Test Value"; + private const string AddedField = "Added Field"; + private const string AddedValue = "Added Value"; private const string Tag1 = "Tag1"; private const string Tag2 = "Tag2"; @@ -99,6 +101,24 @@ public void EditItem() [Test] [Order(3)] + public void EditItemAddsNewField() + { + if (!RunLiveTests) + Assert.Ignore(); + + Run(RunType.Test, () => + { + var item = OnePassword.GetItem(_initialItem, TestVault); + item.Fields.Add(new Field(AddedField, FieldType.String, AddedValue)); + + var editedItem = OnePassword.EditItem(item, TestVault); + + Assert.That(editedItem.Fields.FirstOrDefault(x => x.Label == AddedField)?.Value, Is.EqualTo(AddedValue)); + }); + } + + [Test] + [Order(4)] public void GetItems() { if (!RunLiveTests) @@ -123,7 +143,7 @@ public void GetItems() } [Test] - [Order(4)] + [Order(5)] public void GetItem() { if (!RunLiveTests) @@ -140,6 +160,7 @@ public void GetItem() Assert.That(item.Created, Is.Not.EqualTo(default)); Assert.That(item.Fields.First(x => x.Label == "username").Value, Is.EqualTo(FinalUsername)); Assert.That(item.Fields.First(x => x.Section?.Label == EditSection && x.Label == EditField).Value, Is.EqualTo(FinalValue)); + Assert.That(item.Fields.First(x => x.Label == AddedField).Value, Is.EqualTo(AddedValue)); Assert.That(item.Fields.FirstOrDefault(x => x.Section?.Label == DeleteSection && x.Label == DeleteField), Is.Null); }); }); diff --git a/OnePassword.NET/IOnePasswordManager.Items.cs b/OnePassword.NET/IOnePasswordManager.Items.cs index cb2e901..8d2261f 100644 --- a/OnePassword.NET/IOnePasswordManager.Items.cs +++ b/OnePassword.NET/IOnePasswordManager.Items.cs @@ -6,13 +6,15 @@ namespace OnePassword; public partial interface IOnePasswordManager { - /// Gets a vault's items. + /// Gets a vault's items as summary records. + /// Use or to retrieve hydrated field data before reading or editing fields. /// The vault that contains the items to retrieve. /// The vault's items. /// Thrown when there is an invalid argument. public ImmutableList GetItems(IVault vault); - /// Gets a vault's items. + /// Gets a vault's items as summary records. + /// Use or to retrieve hydrated field data before reading or editing fields. /// The ID of the vault that contains the items to retrieve. /// The vault's items. /// Thrown when there is an invalid argument. @@ -146,36 +148,20 @@ public ImmutableList SearchForItems(string? vaultId = null, bool? includeA /// Shares an item. /// The item to share. /// The vault that contains the item to share. - /// The email address to share the item with. + /// The recipient email addresses. Leave or empty to create an unrestricted share link. /// The delay before the link expires. /// Expires the link after a single view. + /// The created share result. /// Thrown when there is an invalid argument. - public void ShareItem(IItem item, IVault vault, string emailAddress, TimeSpan? expiresIn = null, bool? viewOnce = null); + public ItemShareResult ShareItem(IItem item, IVault vault, IReadOnlyCollection? emailAddresses = null, TimeSpan? expiresIn = null, bool? viewOnce = null); /// Shares an item. /// The ID of the item to share. /// The ID of the vault that contains the item to share. - /// The email address to share the item with. + /// The recipient email addresses. Leave or empty to create an unrestricted share link. /// The delay before the link expires. /// Expires the link after a single view. + /// The created share result. /// Thrown when there is an invalid argument. - public void ShareItem(string itemId, string vaultId, string emailAddress, TimeSpan? expiresIn = null, bool? viewOnce = null); - - /// Shares an item. - /// The item to share. - /// The vault that contains the item to share. - /// The email address to share the item with. - /// The delay before the link expires. - /// Expires the link after a single view. - /// Thrown when there is an invalid argument. - public void ShareItem(IItem item, IVault vault, IReadOnlyCollection emailAddresses, TimeSpan? expiresIn = null, bool? viewOnce = null); - - /// Shares an item. - /// The ID of the item to share. - /// The ID of the vault that contains the item to share. - /// The email address to share the item with. - /// The delay before the link expires. - /// Expires the link after a single view. - /// Thrown when there is an invalid argument. - public void ShareItem(string itemId, string vaultId, IReadOnlyCollection emailAddresses, TimeSpan? expiresIn = null, bool? viewOnce = null); -} \ No newline at end of file + public ItemShareResult ShareItem(string itemId, string vaultId, IReadOnlyCollection? emailAddresses = null, TimeSpan? expiresIn = null, bool? viewOnce = null); +} diff --git a/OnePassword.NET/Items/ItemShareResult.cs b/OnePassword.NET/Items/ItemShareResult.cs new file mode 100644 index 0000000..6c4bd68 --- /dev/null +++ b/OnePassword.NET/Items/ItemShareResult.cs @@ -0,0 +1,32 @@ +namespace OnePassword.Items; + +/// +/// Represents the result of sharing a 1Password item. +/// +public sealed class ItemShareResult +{ + /// + /// The generated share URL. + /// + public Uri? Url { get; internal set; } + + /// + /// The date and time when the share expires, when returned by the CLI. + /// + public DateTimeOffset? ExpiresAt { get; internal set; } + + /// + /// The recipients associated with the share, when returned by the CLI. + /// + public ImmutableList Recipients { get; internal set; } = []; + + /// + /// Whether the share is view-once, when returned by the CLI. + /// + public bool? ViewOnce { get; internal set; } + + /// + /// The raw CLI response used to build the result. + /// + public string RawResponse { get; internal set; } = ""; +} diff --git a/OnePassword.NET/OnePasswordManager.Items.cs b/OnePassword.NET/OnePasswordManager.Items.cs index 1b4618a..e77bebf 100644 --- a/OnePassword.NET/OnePasswordManager.Items.cs +++ b/OnePassword.NET/OnePasswordManager.Items.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Globalization; using OnePassword.Common; using OnePassword.Items; using OnePassword.Templates; @@ -249,51 +250,161 @@ public void MoveItem(string itemId, string currentVaultId, string destinationVau } /// - public void ShareItem(IItem item, IVault vault, string emailAddress, TimeSpan? expiresIn = null, bool? viewOnce = null) + public ItemShareResult ShareItem(IItem item, IVault vault, IReadOnlyCollection? emailAddresses = null, TimeSpan? expiresIn = null, bool? viewOnce = null) { if (item is null || item.Id.Length == 0) throw new ArgumentException($"{nameof(item.Id)} cannot be empty.", nameof(item)); if (vault is null || vault.Id.Length == 0) throw new ArgumentException($"{nameof(vault.Id)} cannot be empty.", nameof(vault)); - ShareItem(item.Id, vault.Id, [emailAddress], expiresIn, viewOnce); + return ShareItem(item.Id, vault.Id, emailAddresses, expiresIn, viewOnce); } /// - public void ShareItem(string itemId, string vaultId, string emailAddress, TimeSpan? expiresIn = null, bool? viewOnce = null) + public ItemShareResult ShareItem(string itemId, string vaultId, IReadOnlyCollection? emailAddresses = null, TimeSpan? expiresIn = null, bool? viewOnce = null) { if (itemId is null || itemId.Length == 0) throw new ArgumentException($"{nameof(itemId)} cannot be empty.", nameof(itemId)); if (vaultId is null || vaultId.Length == 0) throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId)); - ShareItem(itemId, vaultId, [emailAddress], expiresIn, viewOnce); + var command = $"item share {itemId} --vault {vaultId}"; + var normalizedEmailAddresses = NormalizeEmailAddresses(emailAddresses); + if (normalizedEmailAddresses.Count > 0) + command += $" --emails {string.Join(',', normalizedEmailAddresses)}"; + if (expiresIn is not null) + command += $" --expires-in {expiresIn.Value.ToHumanReadable()}"; + if (viewOnce is not null && viewOnce.Value) + command += " --view-once"; + return ParseItemShareResult(Op(command)); } - /// - public void ShareItem(IItem item, IVault vault, IReadOnlyCollection emailAddresses, TimeSpan? expiresIn = null, bool? viewOnce = null) + private static DateTimeOffset? GetDateTimeOffsetProperty(JsonElement root, params string[] propertyNames) { - if (item is null || item.Id.Length == 0) - throw new ArgumentException($"{nameof(item.Id)} cannot be empty.", nameof(item)); - if (vault is null || vault.Id.Length == 0) - throw new ArgumentException($"{nameof(vault.Id)} cannot be empty.", nameof(vault)); + var stringValue = GetStringProperty(root, propertyNames); + return DateTimeOffset.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset) ? dateTimeOffset : null; + } - ShareItem(item.Id, vault.Id, emailAddresses, expiresIn, viewOnce); + private static Uri? GetUriProperty(JsonElement root, params string[] propertyNames) + { + var stringValue = GetStringProperty(root, propertyNames); + return Uri.TryCreate(stringValue, UriKind.Absolute, out var uri) ? uri : null; } - /// - public void ShareItem(string itemId, string vaultId, IReadOnlyCollection emailAddresses, TimeSpan? expiresIn = null, bool? viewOnce = null) + private static bool? GetBooleanProperty(JsonElement root, params string[] propertyNames) { - if (itemId is null || itemId.Length == 0) - throw new ArgumentException($"{nameof(itemId)} cannot be empty.", nameof(itemId)); - if (vaultId is null || vaultId.Length == 0) - throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId)); + foreach (var propertyName in propertyNames) + { + if (!root.TryGetProperty(propertyName, out var property)) + continue; + + return property.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.String when bool.TryParse(property.GetString(), out var value) => value, + _ => null + }; + } - var command = $"item share {itemId} --vault {vaultId} --emails {string.Join(',', emailAddresses)}"; - if (expiresIn is not null) - command += $" --expires-in {expiresIn.Value.ToHumanReadable()}"; - if (viewOnce is not null && viewOnce.Value) - command += " --view-once"; - Op(command); + return null; + } + + private static string? GetStringProperty(JsonElement root, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + if (!root.TryGetProperty(propertyName, out var property)) + continue; + + if (property.ValueKind == JsonValueKind.String) + return property.GetString(); + } + + return null; + } + + private static ImmutableList GetRecipients(JsonElement root, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + if (!root.TryGetProperty(propertyName, out var property)) + continue; + + return property.ValueKind switch + { + JsonValueKind.String => property.GetString() is { Length: > 0 } recipient ? [recipient] : [], + JsonValueKind.Array => [.. property + .EnumerateArray() + .Select(static recipient => recipient.ValueKind switch + { + JsonValueKind.String => recipient.GetString(), + JsonValueKind.Object => GetStringProperty(recipient, "email", "address", "recipient", "value"), + _ => null + }) + .Where(static recipient => !string.IsNullOrWhiteSpace(recipient)) + .Select(static recipient => recipient!)], + _ => [] + }; + } + + return []; + } + + private static ImmutableList NormalizeEmailAddresses(IReadOnlyCollection? emailAddresses) + { + if (emailAddresses is null || emailAddresses.Count == 0) + return []; + + return [.. emailAddresses + .Where(static emailAddress => !string.IsNullOrWhiteSpace(emailAddress)) + .Select(static emailAddress => emailAddress.Trim())]; + } + + private static ItemShareResult ParseItemShareResult(string result) + { + var trimmedResult = result.Trim(); + if (trimmedResult.Length == 0) + return new ItemShareResult(); + + try + { + using var jsonDocument = JsonDocument.Parse(trimmedResult); + var root = jsonDocument.RootElement; + + if (root.ValueKind == JsonValueKind.String) + { + return new ItemShareResult + { + Url = Uri.TryCreate(root.GetString(), UriKind.Absolute, out var uri) ? uri : null, + RawResponse = trimmedResult + }; + } + + if (root.ValueKind != JsonValueKind.Object) + { + return new ItemShareResult + { + RawResponse = trimmedResult + }; + } + + return new ItemShareResult + { + Url = GetUriProperty(root, "url", "link", "share_link", "shareLink"), + ExpiresAt = GetDateTimeOffsetProperty(root, "expires_at", "expiresAt", "expiry", "expires"), + Recipients = GetRecipients(root, "recipients", "emails", "email_addresses", "emailAddresses"), + ViewOnce = GetBooleanProperty(root, "view_once", "viewOnce"), + RawResponse = trimmedResult + }; + } + catch (JsonException) + { + return new ItemShareResult + { + Url = Uri.TryCreate(trimmedResult, UriKind.Absolute, out var uri) ? uri : null, + RawResponse = trimmedResult + }; + } } -} \ No newline at end of file +} diff --git a/OnePassword.NET/OnePasswordManager.cs b/OnePassword.NET/OnePasswordManager.cs index 8d47947..5da29e3 100644 --- a/OnePassword.NET/OnePasswordManager.cs +++ b/OnePassword.NET/OnePasswordManager.cs @@ -172,7 +172,7 @@ private static OnePasswordManagerOptions ValidateOptions(OnePasswordManagerOptio private string GetVersion() { const string command = "--version"; - return Op(command); + return Op(command).Trim(); } private static string GetStandardError(Process process) diff --git a/README.md b/README.md index a4e2da5..4c0a6ff 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ This library targets .NET 6.0, .NET 7.0, and .NET 8.0. This library has no dependencies. +## Breaking changes for the next x.x.x release + +- `ShareItem(...)` now returns `ItemShareResult` instead of `void`. +- The single-email `ShareItem(...)` overloads were removed. Pass a collection of email addresses for restricted links, or omit the collection entirely for unrestricted links. + ## Quick start ### Creating an instance of the manager @@ -96,6 +101,8 @@ var server2 = serverTemplate.Clone(); ### Getting all items in a vault +`GetItems(...)` returns summary items. Fetch the item details with `GetItem(...)` before relying on `Fields`, `Sections`, or other hydrated properties. + ```csharp var items = onePassword.GetItems(vault); ``` @@ -103,7 +110,8 @@ var items = onePassword.GetItems(vault); ### Selecting a specific item ```csharp -var item = items.First(x => x.Title == "Your Item's Title"); +var itemSummary = items.First(x => x.Title == "Your Item's Title"); +var item = onePassword.GetItem(itemSummary, vault); ``` ### Editing a specific item @@ -113,6 +121,36 @@ item.Fields.First(x => x.Label == "password").Value = "newpass"; onePassword.EditItem(item, vault); ``` +### Adding a field to an existing item + +```csharp +var itemToExtend = onePassword.GetItem(itemSummary, vault); +itemToExtend.Fields.Add(new Field("Environment", FieldType.String, "Production")); + +onePassword.EditItem(itemToExtend, vault); +``` + +### Sharing an item without email restrictions + +```csharp +var share = onePassword.ShareItem(item, vault); + +Console.WriteLine(share.Url); +``` + +### Sharing an item with email restrictions + +```csharp +var share = onePassword.ShareItem( + item, + vault, + new[] { "recipient@example.com" }, + expiresIn: TimeSpan.FromDays(7), + viewOnce: true); + +Console.WriteLine(share.Url); +``` + ### Archiving an item ```csharp diff --git a/docfx/docs/quick-start.md b/docfx/docs/quick-start.md index 3b5a7cf..62592de 100644 --- a/docfx/docs/quick-start.md +++ b/docfx/docs/quick-start.md @@ -1,5 +1,7 @@ # Quick start +> Starting with the next x.x.x release, `ShareItem(...)` returns `ItemShareResult` and the single-email overloads are removed. Pass a collection of email addresses for restricted links, or omit the collection entirely for unrestricted links. + ### Creating an instance of the manager ```csharp @@ -43,10 +45,6 @@ Subsequently, the following commands are not used or supported when using servic For more information, see the documentation on [1Password Service Accounts](https://developer.1password.com/docs/service-accounts). -```csharp -var onePassword = new OnePasswordManager(serviceAccountToken: token); -``` - ### Getting all vaults ```csharp @@ -81,6 +79,8 @@ var server2 = serverTemplate.Clone(); ### Getting all items in a vault +`GetItems(...)` returns summary items. Fetch the item details with `GetItem(...)` before relying on `Fields`, `Sections`, or other hydrated properties. + ```csharp var items = onePassword.GetItems(vault); ``` @@ -88,7 +88,8 @@ var items = onePassword.GetItems(vault); ### Selecting a specific item ```csharp -var item = items.First(x => x.Title == "Your Item's Title"); +var itemSummary = items.First(x => x.Title == "Your Item's Title"); +var item = onePassword.GetItem(itemSummary, vault); ``` ### Editing a specific item @@ -98,6 +99,36 @@ item.Fields.First(x => x.Label == "password").Value = "newpass"; onePassword.EditItem(item, vault); ``` +### Adding a field to an existing item + +```csharp +var itemToExtend = onePassword.GetItem(itemSummary, vault); +itemToExtend.Fields.Add(new Field("Environment", FieldType.String, "Production")); + +onePassword.EditItem(itemToExtend, vault); +``` + +### Sharing an item without email restrictions + +```csharp +var share = onePassword.ShareItem(item, vault); + +Console.WriteLine(share.Url); +``` + +### Sharing an item with email restrictions + +```csharp +var share = onePassword.ShareItem( + item, + vault, + new[] { "recipient@example.com" }, + expiresIn: TimeSpan.FromDays(7), + viewOnce: true); + +Console.WriteLine(share.Url); +``` + ### Archiving an item ```csharp