Metadata-Version: 2.3
Name: secflow-sdk
Version: 0.1.4
Summary: SecFlow Python SDK for application runtime secret access
License: MIT
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/yeon3724/secflow#readme
Project-URL: Repository, https://github.com/yeon3724/secflow
Project-URL: Issues, https://github.com/yeon3724/secflow/issues
Description-Content-Type: text/markdown

# secflow-sdk

Python SDK for SecFlow Runtime/Data API secret access.

Runtime Resolve is the only API path that returns plaintext secret values. Use this SDK from application runtime code with an account-bound SDK token only.

Do not use Admin API session tokens with this SDK. Do not print, log, trace, persist, or package plaintext secret values.

## Install

```powershell
python -m pip install secflow-sdk
```

## Configure

Set SDK configuration through runtime environment variables:

```powershell
$env:SECFLOW_BASE_URL = "<secflow-base-url>"
$env:SECFLOW_SDK_TOKEN = "<account-bound-sdk-token>"
$env:SECFLOW_TARGET_ENVIRONMENT = "dev"
$env:SECFLOW_TIMEOUT_SECONDS = "30"
```

`SECFLOW_BASE_URL` defaults to `https://secflow.intflow.dev` when omitted. `SECFLOW_TARGET_ENVIRONMENT` defaults to `dev`. `SECFLOW_SDK_TOKEN` is required.
`SECFLOW_TIMEOUT_SECONDS` is optional, defaults to `30`, and must be a positive number no greater than `300`.

Remote `SECFLOW_BASE_URL` values must use `https://`. Plain `http://` is accepted only for local loopback addresses such as `localhost`, `127.0.0.1`, and `[::1]`.

Use `SecFlowClient.from_dotenv(".env.secflow")` only for local-only workflows that keep the file out of git, logs, tickets, screenshots, and package artifacts.

## Resolve A Secret

```python
from secflow_sdk import SecFlowClient, SecFlowHttpError, SecFlowItemError

SECRET_REF = "secret://dev/example-service/token/app-config"

client = SecFlowClient.from_env()

try:
    secret_value = client.resolve_text(SECRET_REF)
except SecFlowItemError as error:
    raise RuntimeError(f"SecFlow item failed: code={error.code} retryable={error.retryable}") from error
except SecFlowHttpError as error:
    raise RuntimeError(f"SecFlow request failed: code={error.code} retryable={error.retryable}") from error

configure_application(secret_value)
```

Use `resolve_bytes()` for binary values.

## Resolve Many

Use `resolve_many_partial()` when an application can continue with the secrets that were resolved successfully.

```python
from secflow_sdk import SecFlowClient, SecFlowHttpError

secret_refs = (
    "secret://dev/example-service/token/app-config",
    "secret://dev/example-service/password/database",
)

client = SecFlowClient.from_env()

try:
    batch = client.resolve_many_partial(secret_refs)
except SecFlowHttpError as error:
    raise RuntimeError(f"SecFlow request failed: code={error.code} retryable={error.retryable}") from error

for secret_ref, failure in batch.failures.items():
    report_safe_failure(secret_ref=secret_ref, code=failure.code, retryable=failure.retryable)

for secret_ref, resolved in batch.resolved.items():
    configure_application_secret(secret_ref, resolved.as_text())
```

Use `resolve_many()` when all requested SecretRefs must resolve successfully.

## Cached Provider / Auto-refresh

Use `SecFlowSecretProvider` when an application repeatedly reads the same SecretRefs and wants an SDK-supported TTL cache.

```python
from secflow_sdk import SecFlowClient, SecFlowSecretProvider

client = SecFlowClient.from_env()
provider = SecFlowSecretProvider(client, ttl_seconds=300)

secret_value = provider.get_text(SECRET_REF)
many_values = provider.get_many_text(
    [
        "secret://dev/example-service/token/app-config",
        "secret://dev/example-service/password/database",
    ]
)
```

The provider refreshes on cache miss, TTL expiry, or explicit `invalidate()`. Secret rotation is picked up on the next provider refresh after TTL expiry or invalidation. The provider cache TTL is separate from Runtime Resolve lease TTL.

Provider refresh is fail-closed by default. Item-level failures such as authorization denial, expired secrets, or missing SecretRefs remove that cached item and never return stale values. Retryable request-level failures may return a stale cached value only when `stale_grace_seconds` is explicitly greater than `0`.

## Metadata, Validation, And Leases

Use non-plaintext helpers when an application needs catalog checks or lease cleanup without logging secret values.

```python
validation = client.validate([SECRET_REF])[SECRET_REF]
metadata = client.metadata([SECRET_REF])[SECRET_REF]
permission = client.permission_check([SECRET_REF], action="read")[SECRET_REF]
binary_value = client.resolve_bytes(SECRET_REF)
resolved = client.resolve_many([SECRET_REF])[SECRET_REF]
client.revoke_leases([resolved.lease_id], reason="job_completed")
```

## Performance Notes

Reuse a `SecFlowClient` instance instead of constructing one per application request.

Use `resolve_many()` or `resolve_many_partial()` when latency-sensitive code needs multiple SecretRefs for the same runtime environment. The Python SDK intentionally has no external HTTP dependency and does not provide connection pooling in the current package.

## Async Applications

`SecFlowClient` is synchronous. In an `asyncio` application, run blocking SDK calls in a worker thread so the event loop stays responsive.

```python
import asyncio

from secflow_sdk import SecFlowClient

client = SecFlowClient.from_env()


async def load_secret(secret_ref: str) -> str:
    return await asyncio.to_thread(client.resolve_text, secret_ref)
```

Keep the same plaintext handling rules in async code: do not log returned values, request bodies, bearer tokens, or `ResolvedSecret.data`.

## Retry Request Failures

The SDK does not retry automatically. Retry only bounded request-level failures where `SecFlowHttpError.retryable` is true. Do not retry item-level authorization denials, and never log request bodies, bearer tokens, or plaintext values.

```python
import random
import time

from secflow_sdk import SecFlowHttpError


def resolve_text_with_retry(client, secret_ref: str, attempts: int = 3) -> str:
    delay = 0.25
    for attempt in range(1, attempts + 1):
        try:
            return client.resolve_text(secret_ref)
        except SecFlowHttpError as error:
            if not error.retryable or attempt == attempts:
                raise
            time.sleep(delay + random.uniform(0, delay / 2))
            delay *= 2
    raise RuntimeError("unreachable")
```

## Safe Logging

Safe diagnostic fields include request IDs, trace IDs, item IDs, error codes, retryability, decision IDs, and SecretRef values.

Client object `repr()` and `vars()` do not expose the SDK token, but `client.sdk_token` is still a raw bearer credential.

Never log SDK bearer tokens, `client.sdk_token`, Authorization headers, Runtime Resolve response bodies, `ResolvedSecret.data`, `resolve_text()` return values, `as_text()` return values, `as_bytes()` return values, service key material, KMS key material, database URLs, or plaintext secret values.

## More Details

See the repository guide for full SDK behavior, error mapping, package release notes, and local Docker smoke testing:

https://github.com/yeon3724/secflow/blob/master/docs/14-sdk-developer-experience.md

