corteq-onepassword 0.1.5

Secure 1Password SDK wrapper with FFI bindings for Rust applications
Documentation
# corteq-onepassword

This is a 1Password SDK wrapper for Rust applications. This does NOT use the 1Password CLI!
Providing a safe interface to 1Password secrets using FFI bindings for the official 1Password SDK Core library.

## Features

- **Secure by default** - Secrets wrapped in `SecretString` with automatic memory zeroization
- **Simple API** - Retrieve secrets with a single function call
- **Thread-safe** - Client is `Send + Sync` for use in async applications
- **Builder pattern** - Flexible configuration with sensible defaults
- **Type-safe** - Compile-time guarantees for secret handling

## Quick Start

```rust
use corteq_onepassword::{OnePassword, ExposeSecret};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client from OP_SERVICE_ACCOUNT_TOKEN environment variable
    let client = OnePassword::from_env()?
        .integration("my-app", "1.0.0") // Name of your app for audit purposes on 1password side
        .connect()
        .await?;

    // Resolve a secret
    let api_key = client.secret("op://vault/item/api-key").await?;

    // Use the secret (expose only when needed)
    println!("API key length: {}", api_key.expose_secret().len());

    Ok(())
}
```

Refer to the [Implementation Guide](docs/IMPLEMENTATION_GUIDE.md) for detailed instructions.

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
corteq-onepassword = "0.1"
```

## Installing from crates.io

When you install this crate from crates.io, the native library is **not** included
(due to crates.io's 10MB size limit). The library is automatically downloaded during
`cargo build`:

1. **First build** - Downloads the library from PyPI (~15-18MB)
2. **Subsequent builds** - Uses the cached library in your target directory

### Requirements

- Network access during first build to:
  - `pypi.org` (package metadata)
  - `files.pythonhosted.org` (library download)

### Offline Builds

For environments without network access:

1. Download the library on a connected machine:

   ```bash
   # Download for your platform (example for Linux x86_64)
   curl -L "https://pypi.org/pypi/onepassword-sdk/json" | \
     jq -r '.urls[] | select(.filename | contains("manylinux")) | .url' | \
     head -1 | xargs curl -L -o sdk.whl
   unzip sdk.whl "onepassword/*.so" -d extracted/
   ```

2. Set the library path before building:
   ```bash
   export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
   cargo build
   ```

## Authentication

This crate uses 1Password service account tokens. Personal account tokens are not supported.

### Environment Variable (Recommended)

```bash
export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
```

```rust
let client = OnePassword::from_env()?.connect().await?;
```

or use `dotenvy`

```rust
    dotenvy::dotenv().ok();
```

### Explicit Token (Not recommended for production use!)

```rust
let client = OnePassword::from_token("ops_...").connect().await?;
```

## Secret References

Secrets are referenced using the `op://vault/item/field` format:

- `op://Production/Database/password` - Simple reference
- `op://Production/Database/admin/password` - Section-scoped reference

See https://developer.1password.com/docs/cli/secret-reference-syntax/

## API

### Single Secret

```rust
let api_key = client.secret("op://prod/stripe/api-key").await?;
```

### Batch Resolution

```rust
let secrets = client.secrets(&[
    "op://prod/db/host",
    "op://prod/db/user",
    "op://prod/db/pass",
]).await?;

let host = secrets[0].expose_secret();
let user = secrets[1].expose_secret();
let pass = secrets[2].expose_secret();
```

### Named Resolution

```rust
let secrets = client.secrets_named(&[
    ("host", "op://prod/db/host"),
    ("user", "op://prod/db/user"),
    ("pass", "op://prod/db/pass"),
]).await?;

let host = secrets.get("host").unwrap().expose_secret();
```

## Sharing the Client

The client is thread-safe and can be shared via `Arc`:

```rust
use std::sync::Arc;

let client = Arc::new(OnePassword::from_env()?.connect().await?);

let client1 = Arc::clone(&client);
let client2 = Arc::clone(&client);

tokio::join!(
    async move { client1.secret("op://vault/item/field1").await },
    async move { client2.secret("op://vault/item/field2").await },
);
```

## Feature Flags

- `blocking` - Enable synchronous API via `connect_blocking()`
- `tracing` - Enable tracing spans for observability

```toml
[dependencies]
corteq-onepassword = { version = "0.1", features = ["blocking"] }
```

## Platform Support

| Platform | Architecture | Status                  |
| -------- | ------------ | ----------------------- |
| Linux    | x86_64       | ✅ Supported            |
| Linux    | aarch64      | ✅ Supported            |
| macOS    | x86_64       | ✅ Supported            |
| macOS    | aarch64      | ✅ Supported            |
| Windows  | -            | ❌ Not supported        |
| Alpine   | -            | ❌ Not supported (musl) |

## Build Process

The build script looks for the 1Password SDK native library in this order:

1. **`ONEPASSWORD_LIB_PATH`** - Custom path via environment variable
2. **Bundled libraries** - Pre-downloaded in `src/libs/{platform}/`
3. **PyPI download** - Automatic download at build time (requires network)

### Bundled Libraries (Git LFS)

This repository includes pre-downloaded libraries for all supported platforms in `src/libs/`:

```
src/libs/
├── linux-x86_64/libop_uniffi_core.so      (~18MB)
├── linux-aarch64/libop_uniffi_core.so     (~17MB)
├── macos-x86_64/libop_uniffi_core.dylib   (~16MB)
└── macos-aarch64/libop_uniffi_core.dylib  (~15MB)
```

These files are tracked with **Git LFS** due to their size. After cloning:

```bash
git lfs pull  # Download the actual library files
```

**Why bundle libraries?**

- **crates.io size limit**: crates.io enforces a 10MB limit per crate, so we can't include libraries there
- **Offline builds**: No network access required when using bundled libraries
- **Build reproducibility**: Known library versions with verified checksums

### Refreshing Bundled Libraries

To update the bundled libraries (e.g., for a new SDK version):

```bash
./scripts/download-libs.sh
```

This script fetches all 4 platform libraries from PyPI with SHA256 verification.

### PyPI Fallback

If bundled libraries are not found, the build script downloads from PyPI:

1. Fetches metadata from PyPI's JSON API
2. Downloads the appropriate wheel for your target platform
3. Verifies the SHA256 checksum
4. Extracts the native library

### Network Requirements

When downloading from PyPI, network access is required to:

- `pypi.org` - Package metadata and checksums
- `files.pythonhosted.org` - Library downloads

### Custom Library Path

For custom library locations:

```bash
export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
```

## Security

- Tokens wrapped in `SecretString` and zeroized on drop
- Secrets never appear in logs or error messages
- Debug implementations redact sensitive data
- Native library verified via SHA256 checksum at build time

## Error Handling

All errors are typed and implement `std::error::Error`:

```rust
use corteq_onepassword::Error;

match client.secret("op://vault/item/field").await {
    Ok(secret) => { /* use secret */ },
    Err(Error::SecretNotFound { reference }) => {
        eprintln!("Secret not found: {}", reference);
    },
    Err(Error::AccessDenied { vault }) => {
        eprintln!("Access denied to vault: {}", vault);
    },
    Err(e) => {
        eprintln!("Error: {}", e);
    }
}
```

## Troubleshooting

### "Could not find libop_uniffi_core.so"

This error occurs when the native library cannot be located at runtime.

**Solutions:**

1. **Rebuild the crate** - The build script downloads the library automatically:

   ```bash
   cargo clean && cargo build
   ```

2. **Check network access** - The build script needs to reach PyPI:

   ```bash
   curl -I https://pypi.org/pypi/onepassword-sdk/json
   ```

3. **Set custom path** - If you have the library elsewhere:
   ```bash
   export ONEPASSWORD_LIB_PATH="/path/to/libop_uniffi_core.so"
   ```

### Build Script Download Failed

If the automatic download fails during build:

1. Check your network connection
2. Check if PyPI is accessible: `curl https://pypi.org`
3. Try setting `ONEPASSWORD_SKIP_DOWNLOAD=1` and provide the library manually via `ONEPASSWORD_LIB_PATH`