From 9ec96794023ec674e8cb4e4eacec21a74f76b3eb Mon Sep 17 00:00:00 2001 From: Matthew Kern Date: Thu, 14 May 2026 15:34:56 -0700 Subject: [PATCH 1/2] fix: normalize OS name to match 1Password core expectations Rust std::env::consts::OS returns "macos" but the 1Password core expects "darwin" (Go runtime.GOOS convention). Map the value at client config construction time. --- src/core.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core.rs b/src/core.rs index f29caad..f166365 100644 --- a/src/core.rs +++ b/src/core.rs @@ -10,6 +10,13 @@ pub(crate) const SDK_LANGUAGE: &str = "Rust"; pub(crate) const DEFAULT_REQUEST_LIBRARY: &str = "reqwest"; const MESSAGE_LIMIT: usize = 50 * 1024 * 1024; +fn normalize_os() -> &'static str { + match std::env::consts::OS { + "macos" => "darwin", + os => os, + } +} + pub(crate) trait Core: Send + Sync { fn init_client(&self, config: &[u8]) -> Result, SdkError>; fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError>; @@ -68,7 +75,7 @@ impl ClientConfig { integration_version: "Unknown".to_string(), request_library_name: DEFAULT_REQUEST_LIBRARY.to_string(), request_library_version: "0.0.0".to_string(), - system_os: std::env::consts::OS.to_string(), + system_os: normalize_os().to_string(), system_os_version: "0.0.0".to_string(), system_arch: std::env::consts::ARCH.to_string(), account_name: None, @@ -152,10 +159,23 @@ mod tests { fn default_config_has_correct_language() { let config = ClientConfig::new_default(); assert_eq!(config.language, "Rust"); - assert_eq!(config.system_os, std::env::consts::OS); + assert_eq!(config.system_os, super::normalize_os()); assert_eq!(config.system_arch, std::env::consts::ARCH); } + #[test] + fn normalize_os_maps_correctly() { + // The function should map "macos" to "darwin" (matching Go's runtime.GOOS) + // On macOS this validates the actual mapping; on other platforms + // it validates that the OS string passes through unchanged + let result = super::normalize_os(); + if cfg!(target_os = "macos") { + assert_eq!(result, "darwin"); + } else { + assert_eq!(result, std::env::consts::OS); + } + } + #[test] fn invoke_config_serializes_correctly() { let config = InvokeConfig { From 6fd32ed864cb680aa9436851216ddf2c3142986c Mon Sep 17 00:00:00 2001 From: Matthew Kern Date: Thu, 14 May 2026 15:50:35 -0700 Subject: [PATCH 2/2] feat(examples): add comprehensive API examples Adds three example programs demonstrating the full SDK surface: - example.rs: secrets, passwords, item CRUD, batch operations, sharing, SSH keys, documents, file attachments, vault lifecycle - desktop_app.rs: desktop app auth, vault/item operations, group permission management using a temporary vault - service_account.rs: minimal service account authentication All credentials are generated dynamically (SDK password generator, ssh-keygen) to avoid hardcoded secrets. Cleanup is best-effort to prevent leaked resources on partial failures. --- examples/README.md | 75 +++++ examples/desktop_app.rs | 237 ++++++++++++++- examples/example.rs | 577 ++++++++++++++++++++++++++++++++++++ examples/service_account.rs | 8 +- 4 files changed, 887 insertions(+), 10 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/example.rs diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..a10bc5e --- /dev/null +++ b/examples/README.md @@ -0,0 +1,75 @@ +# 1Password Rust SDK Examples + +## Prerequisites + +- A [1Password Service Account](https://developer.1password.com/docs/service-accounts/) token, or the 1Password desktop app installed and running +- A vault ID to use for item operations (the service account must have full CRUD permissions on this vault) +- `ssh-keygen` available on PATH (used to generate SSH keys for the SSH key example) + +## Examples + +### `service_account.rs` — Minimal service account auth + +Authenticates with a service account token and resolves a single secret. + +```sh +export OP_SERVICE_ACCOUNT_TOKEN="ops_..." +export OP_SECRET_REF="op://vault/item/field" # optional, defaults to op://vault/item/field +cargo run --example service_account +``` + +### `example.rs` — Comprehensive API usage + +Demonstrates the full SDK surface using a service account: + +- **Secrets**: resolve single and batch references, validate references +- **Passwords**: generate PIN, memorable, and random passwords +- **Items**: create with fields/sections/tags/websites, list with filters, update, archive, delete +- **Batch operations**: create, get, and delete multiple items at once +- **Item sharing**: get account policy, validate recipients, create share link +- **SSH keys**: create an SSH key item (generates an Ed25519 key at runtime) +- **Documents**: create a document item, replace its content, read it back +- **File fields**: attach a file to an item, read it, delete the file field +- **Vaults**: create, list, get overview, get with accessors, update, delete + +```sh +export OP_SERVICE_ACCOUNT_TOKEN="ops_..." +export OP_VAULT_ID="..." +export OP_SHARE_RECIPIENT="user@example.com" # optional, for item sharing demo +cargo run --example example +``` + +### `desktop_app.rs` — Desktop app integration + +Demonstrates the same operations using the 1Password desktop app for authentication, plus group permission management (grant, update, revoke). + +```sh +export OP_ACCOUNT="my.1password.com" +export OP_VAULT_ID="..." +export OP_GROUP_ID="..." # optional, for group permissions demo +cargo run --example desktop_app --features desktop +``` + +## Operations Covered + +| Operation | `example.rs` | `desktop_app.rs` | +|---|:-:|:-:| +| Authenticate (service account) | ✓ | | +| Authenticate (desktop app) | | ✓ | +| Resolve secrets | ✓ | ✓ | +| Resolve all (batch) | ✓ | | +| Validate secret reference | ✓ | | +| Generate passwords | ✓ | | +| Create item (fields, sections, tags, websites) | ✓ | | +| List items (with filters) | ✓ | ✓ | +| Update item | ✓ | | +| Archive item | ✓ | | +| Delete item | ✓ | | +| Batch create/get/delete | ✓ | ✓ | +| Share item | ✓ | | +| SSH key item | ✓ | | +| Document item (create, replace, read) | ✓ | | +| File field (attach, read, delete) | ✓ | | +| List vaults | ✓ | ✓ | +| Vault CRUD | ✓ | ✓ | +| Group permissions (grant, update, revoke) | | ✓ | diff --git a/examples/desktop_app.rs b/examples/desktop_app.rs index ad33e27..8da7f65 100644 --- a/examples/desktop_app.rs +++ b/examples/desktop_app.rs @@ -1,24 +1,247 @@ #[cfg(feature = "desktop")] fn main() { - use onepassword_sdk::{Client, SecretsApi}; + use onepassword_sdk::{ + Client, GroupAccess, GroupGetParams, GroupVaultAccess, GroupsApi, ItemCategory, + ItemCreateParams, ItemsApi, SdkError, SecretsApi, VaultCreateParams, VaultGetParams, + VaultUpdateParams, VaultsApi, permissions, + }; let account = std::env::var("OP_ACCOUNT") .expect("Set OP_ACCOUNT to your 1Password sign-in address (e.g. my.1password.com)"); - let secret_ref = std::env::var("OP_SECRET_REF") - .unwrap_or_else(|_| "op://MyVault/Example/username".to_string()); + let vault_id = + std::env::var("OP_VAULT_ID").expect("Set OP_VAULT_ID to a vault ID in your account"); + let group_id = std::env::var("OP_GROUP_ID").ok(); let client = Client::builder() .desktop_app_integration(&account) - .integration_info("1Password Rust SDK Example", "v0.1.0") + .integration_info("1Password Rust SDK Example", "v1.0.0") .build() - .expect("Failed to create client"); + .expect("Failed to create client — is 1Password desktop app running?"); - let secret = client + // --- Resolve a secret --- + let secret_ref = std::env::var("OP_SECRET_REF") + .unwrap_or_else(|_| "op://MyVault/Example/username".to_string()); + client .secrets() .resolve(&secret_ref) .expect("Failed to resolve secret"); + println!("Secret resolved successfully"); + + // --- List vaults --- + let vaults = client.vaults().list(None).expect("Failed to list vaults"); + println!("\nVaults ({}):", vaults.len()); + for vault in &vaults { + println!(" - {} ({:?})", vault.title, vault.vault_type); + } + + // --- List items --- + let items = client + .items() + .list(&vault_id, &[]) + .expect("Failed to list items"); + println!("\nItems in vault ({}):", items.len()); + for item in items.iter().take(5) { + println!(" - {} ({:?})", item.title, item.category); + } + + // --- Vault CRUD --- + vault_operations(&client); + + // --- Batch item operations --- + batch_item_operations(&client, &vault_id); + + // --- Group permissions --- + group_permission_operations(&client, &group_id).expect("Group permission operations failed"); + + println!("\nAll desktop app examples completed successfully."); + + fn vault_operations(client: &Client) { + println!("\n=== Vault Operations ==="); + + let vault = client + .vaults() + .create(VaultCreateParams { + title: "Desktop SDK Example Vault".to_string(), + description: Some("Created via desktop app integration".to_string()), + allow_admins_access: Some(true), + }) + .expect("Failed to create vault"); + println!("Created vault: {} ({})", vault.title, vault.id); + + let overview = client + .vaults() + .get_overview(&vault.id) + .expect("Failed to get vault overview"); + println!( + "Vault: {} — {} items", + overview.title, overview.active_item_count + ); + + let full = client + .vaults() + .get( + &vault.id, + VaultGetParams { + accessors: Some(true), + }, + ) + .expect("Failed to get vault details"); + println!( + "Vault accessors: {}", + full.access.as_ref().map_or(0, |a| a.len()) + ); + + let updated = client + .vaults() + .update( + &vault.id, + VaultUpdateParams { + title: Some("Desktop SDK Example Vault (Updated)".to_string()), + description: None, + }, + ) + .expect("Failed to update vault"); + println!("Updated vault: {}", updated.title); + + client + .vaults() + .delete(&vault.id) + .expect("Failed to delete vault"); + println!("Deleted vault: {}", vault.id); + } + + fn batch_item_operations(client: &Client, vault_id: &str) { + println!("\n=== Batch Item Operations ==="); + + let params: Vec = (1..=3) + .map(|i| ItemCreateParams { + category: ItemCategory::SecureNote, + vault_id: vault_id.to_string(), + title: format!("Desktop Batch Note {i}"), + fields: None, + sections: None, + notes: Some(format!("Batch note #{i} via desktop app")), + tags: Some(vec!["desktop-batch".to_string()]), + websites: None, + files: None, + document: None, + }) + .collect(); + + let response = client + .items() + .create_all(vault_id, ¶ms) + .expect("Failed to batch create items"); + + let mut created_ids = Vec::new(); + for resp in &response.individual_responses { + match (&resp.content, &resp.error) { + (Some(item), _) => { + println!(" Created: {} ({})", item.title, item.id); + created_ids.push(item.id.clone()); + } + (_, Some(err)) => println!(" Error: {err:?}"), + _ => {} + } + } + + let get_response = client + .items() + .get_all(vault_id, &created_ids) + .expect("Failed to batch get items"); + for resp in &get_response.individual_responses { + match (&resp.content, &resp.error) { + (Some(item), _) => println!(" Got: {} (v{})", item.title, item.version), + (_, Some(err)) => println!(" Error: {err:?}"), + _ => {} + } + } + + let delete_response = client + .items() + .delete_all(vault_id, &created_ids) + .expect("Failed to batch delete items"); + for (id, resp) in &delete_response.individual_responses { + if let Some(err) = &resp.error { + println!(" Failed to delete {id}: {err:?}"); + } + } + println!("Batch deleted {} items", created_ids.len()); + } + + fn group_permission_operations( + client: &Client, + group_id: &Option, + ) -> Result<(), SdkError> { + println!("\n=== Group Permission Operations ==="); + + let group_id = match group_id { + Some(id) => id, + None => { + println!("Skipping group permissions (set OP_GROUP_ID to test)"); + return Ok(()); + } + }; + + // Get group info + let group = client.groups().get( + group_id, + GroupGetParams { + vault_permissions: Some(true), + }, + )?; + println!("Group: {} ({:?})", group.title, group.group_type); + + // Create a temporary vault for the permissions demo + let temp_vault = client.vaults().create(VaultCreateParams { + title: "SDK Permissions Demo (temp)".to_string(), + description: Some("Temporary vault for group permissions demo".to_string()), + allow_admins_access: Some(true), + })?; + let temp_vault_id = temp_vault.id.clone(); + println!( + "Created temp vault: {} ({})", + temp_vault.title, temp_vault_id + ); + + let result = (|| -> Result<(), SdkError> { + // Grant read + reveal permissions to the group on the temp vault + client.vaults().grant_group_permissions( + &temp_vault_id, + &[GroupAccess { + group_id: group_id.to_string(), + permissions: permissions::READ_ITEMS | permissions::REVEAL_ITEM_PASSWORD, + }], + )?; + println!("Granted read permissions to group on vault"); + + // Update to also allow creating items + client + .vaults() + .update_group_permissions(&[GroupVaultAccess { + vault_id: temp_vault_id.clone(), + group_id: group_id.to_string(), + permissions: permissions::READ_ITEMS + | permissions::REVEAL_ITEM_PASSWORD + | permissions::CREATE_ITEMS, + }])?; + println!("Updated group permissions to include create"); + + // Revoke all permissions + client + .vaults() + .revoke_group_permissions(&temp_vault_id, group_id)?; + println!("Revoked group permissions"); + + Ok(()) + })(); + + // Always clean up the temp vault (best-effort) + let _ = client.vaults().delete(&temp_vault_id); + println!("Deleted temp vault: {temp_vault_id}"); - println!("Secret length: {}", secret.len()); + result + } } #[cfg(not(feature = "desktop"))] diff --git a/examples/example.rs b/examples/example.rs new file mode 100644 index 0000000..bc085d3 --- /dev/null +++ b/examples/example.rs @@ -0,0 +1,577 @@ +use onepassword_sdk::{ + AutofillBehavior, Client, DocumentCreateParams, FileCreateParams, Item, ItemCategory, + ItemCreateParams, ItemField, ItemFieldType, ItemListFilter, ItemListFilterByStateInner, + ItemSection, ItemShareDuration, ItemShareParams, ItemsApi, ItemsFilesApi, ItemsSharesApi, + PasswordRecipe, PasswordRecipeMemorableInner, PasswordRecipePinInner, + PasswordRecipeRandomInner, SdkError, Secrets, SecretsApi, SeparatorType, VaultCreateParams, + VaultGetParams, VaultUpdateParams, VaultsApi, Website, WordListType, +}; + +fn main() -> Result<(), SdkError> { + let token = std::env::var("OP_SERVICE_ACCOUNT_TOKEN") + .expect("Set OP_SERVICE_ACCOUNT_TOKEN environment variable"); + let vault_id = + std::env::var("OP_VAULT_ID").expect("Set OP_VAULT_ID to a vault ID in your account"); + + let client = Client::builder() + .service_account_token(&token) + .integration_info("1Password Rust SDK Example", "v1.0.0") + .build()?; + + // --- Passwords (no client state needed) --- + validate_secret_references(); + generate_passwords()?; + + // --- Items (create first, then resolve secrets from it) --- + let item = create_item(&client, &vault_id)?; + let item_id = item.id.clone(); + let item_vault = item.vault_id.clone(); + + // Run item operations, capturing any error + let item_result = (|| -> Result<(), SdkError> { + resolve_secrets(&client, &item)?; + resolve_all_secrets(&client, &item)?; + list_items(&client, &vault_id)?; + let item = update_item(&client, item)?; + share_item(&client, &vault_id, &item)?; + create_ssh_key_item(&client, &vault_id)?; + create_and_replace_document_item(&client, &vault_id)?; + create_attach_and_delete_file_field_item(&client, &vault_id)?; + batch_item_operations(&client, &vault_id)?; + // Archive and delete use separate items to demonstrate the distinction + let archive_target = client.items().create(ItemCreateParams { + category: ItemCategory::SecureNote, + vault_id: vault_id.to_string(), + title: "Archive Demo".to_string(), + fields: None, + sections: None, + notes: Some("This item will be archived".to_string()), + tags: None, + websites: None, + files: None, + document: None, + })?; + archive_item(&client, &vault_id, &archive_target.id)?; + // Archived items can still be deleted + client.items().delete(&vault_id, &archive_target.id)?; + println!("Archived and deleted demo item"); + + // Delete the main example item + client.items().delete(&vault_id, &item.id)?; + println!("Deleted main item: {}", item.id); + Ok(()) + })(); + + // Best-effort cleanup if operations failed partway + if item_result.is_err() { + let _ = client.items().delete(&item_vault, &item_id); + } + + // --- Vaults --- + vault_operations(&client)?; + + match item_result { + Ok(()) => println!("\nAll examples completed successfully."), + Err(e) => { + println!("\nItem operations failed (cleanup attempted): {e}"); + return Err(e); + } + } + + Ok(()) +} + +// ── Secrets ────────────────────────────────────────────────────────────── + +fn resolve_secrets(client: &Client, item: &Item) -> Result<(), SdkError> { + println!("\n=== Resolve Secrets ==="); + + // Resolve a secret using a reference built from the created item + let secret_ref = format!("op://{}/{}/username", item.vault_id, item.id); + client.secrets().resolve(&secret_ref)?; + println!("Secret resolved successfully"); + + Ok(()) +} + +fn resolve_all_secrets(client: &Client, item: &Item) -> Result<(), SdkError> { + println!("\n=== Resolve All Secrets ==="); + + let references = vec![ + format!("op://{}/{}/username", item.vault_id, item.id), + format!("op://{}/{}/password", item.vault_id, item.id), + ]; + + let response = client.secrets().resolve_all(&references)?; + + for (reference, result) in &response.individual_responses { + if result.content.is_some() { + println!("{reference}: resolved successfully"); + } + if let Some(error) = &result.error { + println!("{reference}: error: {error:?}"); + } + } + + Ok(()) +} + +fn validate_secret_references() { + println!("\n=== Validate Secret References ==="); + + match Secrets::validate_secret_reference("op://vault/item/field") { + Ok(()) => println!("Valid reference"), + Err(e) => println!("Invalid reference: {e}"), + } + + match Secrets::validate_secret_reference("not-a-valid-reference") { + Ok(()) => println!("Valid reference"), + Err(e) => println!("Invalid reference (expected): {e}"), + } +} + +fn generate_passwords() -> Result<(), SdkError> { + println!("\n=== Generate Passwords ==="); + + // PIN + Secrets::generate_password(PasswordRecipe::Pin(PasswordRecipePinInner { length: 8 }))?; + println!("PIN generated successfully"); + + // Memorable password + Secrets::generate_password(PasswordRecipe::Memorable(PasswordRecipeMemorableInner { + separator_type: SeparatorType::Hyphens, + capitalize: true, + word_list_type: WordListType::FullWords, + word_count: 4, + }))?; + println!("Memorable password generated successfully"); + + // Random password + Secrets::generate_password(PasswordRecipe::Random(PasswordRecipeRandomInner { + include_digits: true, + include_symbols: true, + length: 32, + }))?; + println!("Random password generated successfully"); + + Ok(()) +} + +// ── Items ──────────────────────────────────────────────────────────────── + +fn create_item(client: &Client, vault_id: &str) -> Result { + println!("\n=== Create Item ==="); + + // Generate a password for the example item using the SDK + let password = Secrets::generate_password(PasswordRecipe::Random(PasswordRecipeRandomInner { + include_digits: true, + include_symbols: true, + length: 20, + }))?; + + let item = client.items().create(ItemCreateParams { + category: ItemCategory::Login, + vault_id: vault_id.to_string(), + title: "Rust SDK Example Login".to_string(), + fields: Some(vec![ + ItemField { + id: "username".to_string(), + title: "username".to_string(), + section_id: None, + field_type: ItemFieldType::Text, + value: "user@example.com".to_string(), + details: None, + }, + ItemField { + id: "password".to_string(), + title: "password".to_string(), + section_id: None, + field_type: ItemFieldType::Concealed, + value: password.password, + details: None, + }, + ItemField { + id: "api_key".to_string(), + title: "API Key".to_string(), + section_id: Some("additional".to_string()), + field_type: ItemFieldType::Concealed, + value: "example-api-key-placeholder".to_string(), + details: None, + }, + ]), + sections: Some(vec![ItemSection { + id: "additional".to_string(), + title: "Additional Details".to_string(), + }]), + notes: Some("Created by Rust SDK example".to_string()), + tags: Some(vec!["example".to_string(), "rust-sdk".to_string()]), + websites: Some(vec![Website { + url: "https://example.com".to_string(), + label: "Example".to_string(), + autofill_behavior: AutofillBehavior::AnywhereOnWebsite, + }]), + files: None, + document: None, + })?; + + println!("Created item: {} ({})", item.title, item.id); + Ok(item) +} + +fn list_items(client: &Client, vault_id: &str) -> Result<(), SdkError> { + println!("\n=== List Items ==="); + + // List all active items + let items = client.items().list(vault_id, &[])?; + println!("Total items in vault: {}", items.len()); + + // List only active items (exclude archived) + let active_items = client.items().list( + vault_id, + &[ItemListFilter::ByState(ItemListFilterByStateInner { + active: true, + archived: false, + })], + )?; + println!("Active items: {}", active_items.len()); + + Ok(()) +} + +fn update_item(client: &Client, mut item: Item) -> Result { + println!("\n=== Update Item ==="); + + item.title = "Rust SDK Example Login (Updated)".to_string(); + item.tags.push("updated".to_string()); + + if let Some(field) = item.fields.iter_mut().find(|f| f.id == "username") { + field.value = "updated-user@example.com".to_string(); + } + + let updated = client.items().put(item)?; + println!("Updated item: {} (v{})", updated.title, updated.version); + Ok(updated) +} + +fn archive_item(client: &Client, vault_id: &str, item_id: &str) -> Result<(), SdkError> { + println!("\n=== Archive Item ==="); + client.items().archive(vault_id, item_id)?; + println!("Archived item: {item_id}"); + Ok(()) +} + +fn share_item(client: &Client, vault_id: &str, item: &Item) -> Result<(), SdkError> { + println!("\n=== Share Item ==="); + + let recipient = match std::env::var("OP_SHARE_RECIPIENT") { + Ok(r) => r, + Err(_) => { + println!("Skipping (set OP_SHARE_RECIPIENT to an email address to test)"); + return Ok(()); + } + }; + + let policy = client + .items() + .shares() + .get_account_policy(vault_id, &item.id)?; + println!("Account policy max expiry: {:?}", policy.max_expiry); + + let valid_recipients = client + .items() + .shares() + .validate_recipients(policy.clone(), &[recipient])?; + println!("Valid recipients: {}", valid_recipients.len()); + + let _share_link = client.items().shares().create( + item.clone(), + policy, + ItemShareParams { + recipients: Some(valid_recipients), + expire_after: Some(ItemShareDuration::OneDay), + one_time_only: true, + }, + )?; + // Don't log the share link — it's a bearer URL + println!("Share link created successfully"); + + Ok(()) +} + +fn batch_item_operations(client: &Client, vault_id: &str) -> Result<(), SdkError> { + println!("\n=== Batch Item Operations ==="); + + // Batch create + let params: Vec = (1..=3) + .map(|i| ItemCreateParams { + category: ItemCategory::SecureNote, + vault_id: vault_id.to_string(), + title: format!("Batch Note {i}"), + fields: None, + sections: None, + notes: Some(format!("Batch-created note #{i}")), + tags: Some(vec!["batch".to_string()]), + websites: None, + files: None, + document: None, + }) + .collect(); + + let create_response = client.items().create_all(vault_id, ¶ms)?; + let mut created_ids = Vec::new(); + for resp in &create_response.individual_responses { + if let Some(item) = &resp.content { + println!(" Created: {} ({})", item.title, item.id); + created_ids.push(item.id.clone()); + } + if let Some(err) = &resp.error { + println!(" Error: {err:?}"); + } + } + + // Batch get + let get_response = client.items().get_all(vault_id, &created_ids)?; + println!( + "Batch get returned {} results", + get_response.individual_responses.len() + ); + + // Batch delete + let delete_response = client.items().delete_all(vault_id, &created_ids)?; + for (id, resp) in &delete_response.individual_responses { + if resp.error.is_some() { + println!(" Failed to delete {id}: {:?}", resp.error); + } + } + println!("Batch deleted {} items", created_ids.len()); + + Ok(()) +} + +fn create_ssh_key_item(client: &Client, vault_id: &str) -> Result<(), SdkError> { + println!("\n=== Create SSH Key Item ==="); + + let private_key = generate_ed25519_key()?; + + let item = client.items().create(ItemCreateParams { + category: ItemCategory::SshKey, + vault_id: vault_id.to_string(), + title: "Rust SDK Example SSH Key".to_string(), + fields: Some(vec![ItemField { + id: "private_key".to_string(), + title: "private key".to_string(), + section_id: None, + field_type: ItemFieldType::SshKey, + value: private_key, + details: None, + }]), + sections: None, + notes: None, + tags: Some(vec!["ssh".to_string(), "example".to_string()]), + websites: None, + files: None, + document: None, + })?; + + println!("Created SSH key item: {} ({})", item.title, item.id); + + // Clean up + client.items().delete(vault_id, &item.id)?; + Ok(()) +} + +fn generate_ed25519_key() -> Result { + let dir = std::env::temp_dir().join(format!("op-sdk-example-{}", std::process::id())); + let key_path = dir.join("key"); + + std::fs::create_dir_all(&dir) + .map_err(|e| SdkError::Config(format!("failed to create temp dir: {e}")))?; + + let result = (|| { + let status = std::process::Command::new("ssh-keygen") + .args(["-t", "ed25519", "-f"]) + .arg(&key_path) + .args(["-N", "", "-q"]) + .status() + .map_err(|e| SdkError::Config(format!("failed to run ssh-keygen: {e}")))?; + + if !status.success() { + return Err(SdkError::Config("ssh-keygen failed".to_string())); + } + + std::fs::read_to_string(&key_path) + .map_err(|e| SdkError::Config(format!("failed to read generated key: {e}"))) + })(); + + let _ = std::fs::remove_dir_all(&dir); + result +} + +fn create_and_replace_document_item(client: &Client, vault_id: &str) -> Result<(), SdkError> { + println!("\n=== Document Item ==="); + + // Create a document item + let item = client.items().create(ItemCreateParams { + category: ItemCategory::Document, + vault_id: vault_id.to_string(), + title: "Rust SDK Example Document".to_string(), + fields: None, + sections: None, + notes: None, + tags: None, + websites: None, + files: None, + document: Some(DocumentCreateParams { + name: "config.json".to_string(), + content: br#"{"key": "value"}"#.to_vec(), + }), + })?; + println!("Created document item: {} ({})", item.title, item.id); + + // Replace the document content + let updated = client.items().files().replace_document( + item, + DocumentCreateParams { + name: "config.json".to_string(), + content: br#"{"key": "updated-value", "version": 2}"#.to_vec(), + }, + )?; + println!("Replaced document in item: {}", updated.id); + + // Read back the document + if let Some(doc) = &updated.document { + let content = client + .items() + .files() + .read(&vault_id, &updated.id, doc.clone())?; + println!("Document content length: {} bytes", content.len()); + } + + // Clean up + client.items().delete(vault_id, &updated.id)?; + Ok(()) +} + +fn create_attach_and_delete_file_field_item( + client: &Client, + vault_id: &str, +) -> Result<(), SdkError> { + println!("\n=== File Field Item ==="); + + let section_id = "attachments"; + let field_id = "config_file"; + + // Create an item with a section for file attachments + let item = client.items().create(ItemCreateParams { + category: ItemCategory::SecureNote, + vault_id: vault_id.to_string(), + title: "Rust SDK Example File Attachment".to_string(), + fields: None, + sections: Some(vec![ItemSection { + id: section_id.to_string(), + title: "Attachments".to_string(), + }]), + notes: None, + tags: None, + websites: None, + files: None, + document: None, + })?; + println!("Created item: {} ({})", item.title, item.id); + + // Attach a file + let item = client.items().files().attach( + item, + FileCreateParams { + name: "notes.txt".to_string(), + content: b"These are some important notes.".to_vec(), + section_id: section_id.to_string(), + field_id: field_id.to_string(), + }, + )?; + println!("Attached file to item"); + + // Read the file back + let file = item.files.first().expect("Item should have a file"); + let content = client + .items() + .files() + .read(vault_id, &item.id, file.attributes.clone())?; + let file_section_id = file.section_id.clone(); + let file_field_id = file.field_id.clone(); + println!( + "Read file '{}': {} bytes", + file.attributes.name, + content.len() + ); + + // Delete the file field + let item = client + .items() + .files() + .delete(item, &file_section_id, &file_field_id)?; + println!("Deleted file field from item"); + + // Clean up + client.items().delete(vault_id, &item.id)?; + Ok(()) +} + +// ── Vaults ─────────────────────────────────────────────────────────────── + +fn vault_operations(client: &Client) -> Result<(), SdkError> { + println!("\n=== Vault Operations ==="); + + // List vaults + let vaults = client.vaults().list(None)?; + println!("Total vaults: {}", vaults.len()); + for vault in &vaults { + println!( + " - {} ({:?}, {} items)", + vault.title, vault.vault_type, vault.active_item_count + ); + } + + // Create a vault + let vault = client.vaults().create(VaultCreateParams { + title: "Rust SDK Example Vault".to_string(), + description: Some("Created by the Rust SDK example".to_string()), + allow_admins_access: Some(true), + })?; + println!("Created vault: {} ({})", vault.title, vault.id); + + // Get vault overview + let overview = client.vaults().get_overview(&vault.id)?; + println!( + "Vault overview: {} — {} items", + overview.title, overview.active_item_count + ); + + // Get full vault details with accessors + let full_vault = client.vaults().get( + &vault.id, + VaultGetParams { + accessors: Some(true), + }, + )?; + println!( + "Vault accessors: {}", + full_vault.access.as_ref().map_or(0, |a| a.len()) + ); + + // Update vault + let updated = client.vaults().update( + &vault.id, + VaultUpdateParams { + title: Some("Rust SDK Example Vault (Updated)".to_string()), + description: Some("Updated description".to_string()), + }, + )?; + println!("Updated vault: {}", updated.title); + + // Delete vault + client.vaults().delete(&vault.id)?; + println!("Deleted vault: {}", vault.id); + + Ok(()) +} diff --git a/examples/service_account.rs b/examples/service_account.rs index 78cf102..1d44b8a 100644 --- a/examples/service_account.rs +++ b/examples/service_account.rs @@ -10,10 +10,12 @@ fn main() { .build() .expect("Failed to create client"); - let secret = client + let secret_ref = + std::env::var("OP_SECRET_REF").unwrap_or_else(|_| "op://vault/item/field".to_string()); + client .secrets() - .resolve("op://vault/item/field") + .resolve(&secret_ref) .expect("Failed to resolve secret"); - println!("Secret length: {}", secret.len()); + println!("Secret resolved successfully"); }