Authentication¶
The SDK authenticates using an Affinity API key.
from affinity import Affinity
with Affinity(api_key="your-api-key") as client:
me = client.whoami()
print(me.user.email)
Resolution chain¶
The SDK and CLI resolve the API key through the following chain (first non-empty value wins):
- The key you pass explicitly —
Affinity(api_key="…")in the SDK;--api-key-file <path>or--api-key-stdinin the CLI (the CLI has no--api-keyflag) AFFINITY_API_KEY— standard environment variableAFFINITY_API_KEY_FILE— path to a file containing the key, or a.envfile / named pipe withAFFINITY_API_KEY=(12-factor / Docker secrets / 1Password)AFFINITY_API_KEY_COMMAND— shell command whose stdout is the key (credential-helper)- User config file —
api_keyinconfig.toml, written byxaffinity config setup-key --scope user(file mode0600on Unix;xaffinity config pathprints the location). CLI only.
The SDK uses steps 1–4; step 5 is CLI only. When the CLI uses a key flag while
AFFINITY_API_KEY, AFFINITY_API_KEY_FILE or AFFINITY_API_KEY_COMMAND is also set, it warns
that those are ignored (the file is not read and the command is not run). With load_dotenv=True (SDK) or --dotenv (CLI),
a .env file is loaded into the environment before step 2, so its AFFINITY_API_KEY counts as
step 2. That is how a key saved by xaffinity config setup-key --scope project (a .env in the
current directory) is used: run commands with --dotenv.
Empty string is treated as unset at every step (safe against stale export AFFINITY_API_KEY= lines).
Environment variables¶
If you prefer reading from the environment:
from affinity import Affinity
# Reads AFFINITY_API_KEY by default
client = Affinity.from_env()
# Use a custom environment variable name
client = Affinity.from_env(env_var="MY_AFFINITY_KEY")
For local development, you can load a .env file (requires python-dotenv):
from affinity import Affinity
# Load .env from current directory
client = Affinity.from_env(load_dotenv=True)
# Load from a specific path
client = Affinity.from_env(load_dotenv=True, dotenv_path=".env.local")
AFFINITY_API_KEY_FILE — file-based secrets¶
Set this env var to the path of a file containing your API key. Used by Docker secrets, Kubernetes mounted Secrets, and Hashicorp Vault agent sidecars.
# Docker
docker run -e AFFINITY_API_KEY_FILE=/run/secrets/affinity_api_key …
# Kubernetes — mount a Secret as a file and set the env var
env:
- name: AFFINITY_API_KEY_FILE
value: /etc/secrets/affinity-api-key
The file can hold just the key, or be a .env file: the AFFINITY_API_KEY= line is used
(export and quotes are allowed). The same applies to --api-key-file.
1Password Environments. An Environment's locally mounted .env file is a named pipe that 1Password
fills when it is read, after you approve the read. Point the variable at it:
export AFFINITY_API_KEY_FILE=~/projects/crm/.env # the path you chose when mounting it
xaffinity whoami
The first read asks you to approve it in 1Password; if 1Password is locked, it asks you to
unlock it. The CLI waits up to 30 seconds for that. Further reads need nothing until 1Password
locks again, and several commands can read at once. If 1Password isn't running or the read
isn't approved, the CLI stops with a message saying so. The key is never written to disk.
config check-key reports a named pipe as configured without reading it (so it never prompts).
While 1Password is unlocked, any program on your computer can read the mounted file (that is
how 1Password's locally mounted .env files work), so a short auto-lock time in 1Password limits that
window.
On Posix systems, a UserWarning is emitted if the file is group- or world-readable
(mode 0644 or looser). Use chmod 600 to silence it. The check is a no-op on Windows.
An empty file or missing path raises ValueError in the SDK (Affinity.from_env()); the CLI
reports it as a usage error (exit code 2).
AFFINITY_API_KEY_COMMAND — command-based secrets¶
Set this env var to a shell command. The SDK runs it at startup and uses its stdout as the key.
Follows the same convention as git credential.helper, gpg --passphrase-program, and similar tools.
# 1Password CLI
export AFFINITY_API_KEY_COMMAND="op read op://Personal/Affinity/credential"
# macOS Keychain
export AFFINITY_API_KEY_COMMAND="security find-generic-password -a affinity -w"
# pass (Unix password manager)
export AFFINITY_API_KEY_COMMAND="pass show affinity/api-key"
# HashiCorp Vault
export AFFINITY_API_KEY_COMMAND="vault kv get -field=api_key secret/affinity"
The default timeout is 30 seconds; override with AFFINITY_API_KEY_COMMAND_TIMEOUT=<seconds>.
A non-zero exit code, empty stdout, or timeout raises ValueError in the SDK, or a usage error
(exit code 2) in the CLI. stderr is included in the error message, capped at 500 chars.
For defensive "no writes" usage (scripts, audits), disable writes via policy:
from affinity import Affinity
from affinity.policies import Policies, WritePolicy
client = Affinity.from_env(policies=Policies(write=WritePolicy.DENY))
CLI Authentication¶
For the CLI, use the built-in setup commands:
# Check if a key is configured
xaffinity config check-key
# Set up a new key securely (hidden input)
xaffinity config setup-key
See CLI Authentication for details.
Caveats¶
.env files can silently shadow AFFINITY_API_KEY_FILE / _COMMAND¶
load_dotenv=True (SDK) and --dotenv (CLI) load .env files before the
resolver reads any environment variable. By default, dotenv values do not override
already-set shell variables (dotenv_override=False).
The resolver checks AFFINITY_API_KEY first, then AFFINITY_API_KEY_FILE, then
AFFINITY_API_KEY_COMMAND. So if your .env file contains
AFFINITY_API_KEY=… and you also have AFFINITY_API_KEY_FILE set in your shell:
- After dotenv load, the shell-set
AFFINITY_API_KEY_FILEis unchanged. - But
AFFINITY_API_KEYis now also set (from.env). - The resolver returns the
.envvalue at step 2; the file at the_FILEpath is never read.
This is rarely intended. If you mix dotenv and _FILE/_COMMAND, either:
- Keep one mechanism per environment, or
- Set
dotenv_override=Truedeliberately and own the precedence in the.envfile.
File-on-disk caveat for AFFINITY_API_KEY_FILE¶
The SDK opens the file in three syscalls (existence check, permission stat, read).
A co-tenant with write access to the parent directory could in principle swap
the file between checks. This is the standard Unix file-handle race; the
mitigation is to keep the key file in a directory you control (your home dir,
a locked-down /etc subtree, or a Kubernetes-mounted Secret volume), not in a
world-writable temp dir.