Skip to content

Repository files navigation

ZETA command line client

zeta CLI demo

Command-line client for resources protected by ZETA Guard. Handles the full OAuth2 client lifecycle — dynamic client registration, SMC-B authentication via a gematik Konnektor or local PKCS#12 keystore, token refresh and revocation — and ships a curl-like HTTP client and a JSON-over-WebSocket client that transparently attach the bearer token to every request. Also obtains Proof-of-Patient-Presence (PoPP) tokens via Konnektor-driven eGK flows.

Quick start

For a full end-to-end walkthrough (configure the .kon, read the SMC-B ICCSN, log in, get a PoPP token), see docs/quickstart.md.

# 1. See what a Zeta-Guard-protected resource advertises (no auth needed)
zeta discover https://popp.dev.poppservice.de

# 2. Log in (register + authenticate) using your SMC-B via the Konnektor
zeta login https://popp.dev.poppservice.de \
  --scope popp \
  --auth-method connector \
  --auth-connector-telematik-id 5-2-1234567

# 3. Check local SDK state
zeta status

# 4. Call the protected resource — the bearer token is added for you
zeta http https://popp.dev.poppservice.de/some/api --scope popp

# 5. Get a PoPP token by driving the Konnektor's eGK
zeta popp connector

For headless / no-Konnektor environments, substitute the auth options:

zeta login https://popp.dev.poppservice.de \
  --scope popp \
  --auth-method p12 \
  --auth-p12-file ./smcb.p12

(--auth-p12-alias defaults to alias, --auth-p12-password to 00 — see the PKCS#12 method table.)

Commands

Lifecycle & state

  • zeta discover URL — fetch + cache protected-resource and AS metadata (RFC 9728). No auth.
  • zeta status [URL] — show cached SDK state (registration + token validity). Read-only.
  • zeta register URL — dynamic client registration with the AS.
  • zeta authenticate URL --scope … — exchange SMC-B subject token → access + refresh token.
  • zeta login URL --scope … — idempotent register + authenticate.
  • zeta logout URL — revoke tokens; keep client registration.
  • zeta forget URL / --all — wipe local state for one resource, or the whole profile.

Calling protected resources

  • zeta http URL — HTTP request to a Zeta-protected resource (curl-like).
  • zeta ws URL — WebSocket to a Zeta-protected resource, round-tripping JSON from stdin.
  • zeta vsdm get [POPP-TOKEN] — read a patient's VSDM bundle straight from a PoPP token: derives the environment and insurer from the token, resolves the VSDM endpoint via the TI service-discovery catalog, and fetches the bundle (scope vsdservice). Token falls back to ZETA_POPP_TOKEN; --endpoint URL overrides the catalog routing with an explicit VSDM endpoint (base URL only).

Konnektor & PoPP

  • zeta connector inspect — SDS / product info from the Konnektor.
  • zeta connector get cards — list cards visible in the Konnektor's terminals.
  • zeta connector configs — list discoverable .kon files.
  • zeta connector use <name> — set the default .kon config for later commands.
  • zeta popp connector — get a PoPP token via the Konnektor's eGK flow.
  • zeta popp kartos — get a PoPP token via a kartos smartcard simulator.

Tooling

  • zeta version — print the CLI and zeta-sdk version.
  • zeta stress … — load-test ZETA Guard with a fleet of SMC-B-backed clients. See docs/stress.md.

Examples for the less obvious ones:

# Status of a single resource as JSON (good for jq pipelines)
zeta status https://popp.dev.poppservice.de -o json | jq

# HTTP POST with body and custom header
zeta http https://api.example.org/v1/foo \
  --scope foo -X POST -d '{"x":1}' -H 'X-Trace-Id: dev-42'

# WebSocket with PoPP header inlined; messages from stdin
zeta ws wss://service.example.org/stream --scope foo --popp-token "$TOKEN" <<<'{"hello":1}'

# Same in PowerShell — note the $env: prefix; bare $ZETA_POPP_TOKEN won't reach the JVM
# $env:ZETA_POPP_TOKEN = (zeta popp connector)
# zeta http https://api.example.org/v1/foo --scope foo

# List .kon files the CLI can resolve by short name
zeta connector configs

# Wipe everything in a profile (asks for confirmation on a TTY; --force skips it)
zeta forget --all --profile staging

Options reference

All options below are sticky — they're accepted at any depth: zeta -v http … and zeta http -v … both work. The same goes for -c/--connector-config, --profile, etc.

Values resolve in this precedence (high → low): CLI flag → environment variable → zeta.yaml → built-in default.

PowerShell users: $ZETA_POPP_TOKEN = … sets a PowerShell variable that child processes never see. Use $env:ZETA_POPP_TOKEN = … (same for any other ZETA_* env var) so the JVM picks it up.

Global options

Available on every command.

Option Env var Default
-v, --verbose (repeatable) warn (0). -v=info, -vv=debug, -vvv=trace
--connect-timeout=<seconds> ZETA_CONNECT_TIMEOUT 5
--request-timeout=<seconds> ZETA_REQUEST_TIMEOUT 30
-k, --insecure ZETA_INSECURE false (TLS verified)
--asl-prod ZETA_ASL_PROD false (non-prod)
--ca-cert=<file> (repeatable) ZETA_CA_CERT JVM default trust store
-o, --output-format=text|json|raw ZETA_OUTPUT_FORMAT text
-f, --file=<file> ZETA_CONFIG auto-discovered zeta.yaml
--no-config ZETA_NO_CONFIG false (auto-discovery on)
-c, --connector-config=<name> ZETA_CONNECTOR_CONFIG default
--proxy=<url> ZETA_PROXY
--proxy-user=<user> ZETA_PROXY_USER
--proxy-password=<password> ZETA_PROXY_PASSWORD
--trace — print in-process span tree at end of command ZETA_TRACE false

-v is the one exception to the env-var rule: Clikt's repeat-count flag doesn't pair with a single env value. Use -v/-vv/-vvv on the CLI, or set verbose: in zeta.yaml.

--proxy accepts http[s]://[user:pass@]host[:port]. Use --proxy-user / --proxy-password to keep credentials out of the URL.

Profile

State (registrations, tokens) is kept in a per-profile SQLite database at $XDG_CONFIG_HOME/telematik/zeta/<profile>.storage.db, scoped internally per resource + scope-set.

Available on: discover, status, register, authenticate, login, logout, forget, http, ws, vsdm get, popp ….

Option Env var Default
--profile=<name> ZETA_PROFILE default

Authentication

Required by: register, authenticate, login, logout, http, ws, vsdm get, popp ….

Pick a method via --auth-method, then supply that method's options.

Option Env var Default
--auth-method=connector|p12|db ZETA_AUTH_METHOD required

Connector method (--auth-method connector)

Signs the SMC-B token via a Konnektor described by --connector-config. Pick exactly one card identifier (stability order, best → worst):

Option Env var Default
--auth-connector-telematik-id=<tid> ZETA_AUTH_CONNECTOR_TELEMATIK_ID
--auth-connector-card-iccsn=<iccsn> ZETA_AUTH_CONNECTOR_CARD_ICCSN
--auth-connector-card-handle=<handle> ZETA_AUTH_CONNECTOR_CARD_HANDLE
.kon connector files

The Konnektor itself is described by a .kon file (JSON). The CLI resolves --connector-config=<name> (env ZETA_CONNECTOR_CONFIG) to <name>.kon, looking in the current directory first, then $XDG_CONFIG_HOME/telematik/connectors/. zeta connector use <name> records a sticky default so later commands can omit the flag, and zeta connector configs lists the files it can resolve by short name.

${VAR} placeholders are substituted from the environment before the file is parsed, so secrets stay out of the file. See docs/kon-format.md for the full format specification.

mTLS via a PKCS#12 client certificate, with a pinned trust anchor (RU):

{
  "url": "https://konnektor.example.com:8443",
  "mandantId": "M1",
  "workplaceId": "W1",
  "clientSystemId": "C1",
  "userId": "U1",
  "credentials": {
    "type": "pkcs12",
    "data": "<base64-encoded PKCS#12 container>",
    "password": "00"
  },
  "env": "ru",
  "expectedHost": "konnektor.example.com",
  "trustStore": ["${KONNEKTOR_CA_DER_BASE64}"]
}

Dev Konnektor over basic auth (RU), TLS verification off for self-signed setups:

{
  "url": "https://10.10.10.5:443",
  "mandantId": "M1",
  "workplaceId": "W1",
  "clientSystemId": "C1",
  "credentials": {
    "type": "basic",
    "username": "konnektor",
    "password": "${KONNEKTOR_PASSWORD}"
  },
  "env": "ru",
  "insecureSkipVerify": true
}

PKCS#12 method (--auth-method p12)

For headless / no-Konnektor environments. Signs locally with a .p12 keystore.

Option Env var Default
--auth-p12-file=<file> ZETA_AUTH_P12_FILE required
--auth-p12-alias=<name> ZETA_AUTH_P12_ALIAS alias
--auth-p12-password=<password> ZETA_AUTH_P12_PASSWORD 00

Identity-database method (--auth-method db)

Signs with an SMC-B identity from a zeta-stress identity database (SQLite, built by zeta stress import-identities). For headless / bulk runs.

Option Env var Default
--auth-db=<file> ZETA_AUTH_DB stress.db
--auth-db-telematik-id=<tid> ZETA_AUTH_DB_TELEMATIK_ID required — except zeta vsdm get, which derives it from the token's actorId

Command-specific options

zeta authenticate / zeta login / zeta register / zeta logout / zeta status

Option Env var Default
-s, --scope=<name> (repeatable, required) — authenticate / login only ZETA_SCOPE
--reveal — expose redacted secrets; on the action verbs also appends the full cached entry ZETA_REVEAL false

zeta forget

Option Env var Default
--all — wipe entire profile ZETA_FORGET_ALL false
--force — skip interactive confirmation ZETA_FORGET_FORCE false

zeta http

Option Env var Default
-X, --request=<method> ZETA_HTTP_METHOD GET (or POST if -d set)
-H, --header=<name: value> (repeatable) ZETA_HTTP_HEADER
-d, --data=<body> ZETA_HTTP_DATA
-i, --include — print status + headers ZETA_HTTP_INCLUDE false
-s, --scope=<name> (repeatable, required) ZETA_SCOPE
-p, --popp-token=<token> ZETA_POPP_TOKEN

zeta ws

Option Env var Default
-H, --header=<name: value> (repeatable) ZETA_WS_HEADER
-s, --scope=<name> (repeatable, required) ZETA_SCOPE
-p, --popp-token=<token> ZETA_POPP_TOKEN

zeta vsdm get [POPP-TOKEN]

Option Env var Default
POPP-TOKEN (positional, optional) ZETA_POPP_TOKEN
--endpoint=<url> — (resolve via catalog)
--profile-version=<version> 1.1
-H, --header=<name: value> ZETA_VSDM_HEADER

The POPP-TOKEN argument accepts the token itself or a path to a file holding one (auto-detected). Everything else is derived from the token: the environment (from the issuer), the insurer's VSDM endpoint (from the TI service-discovery catalog), the vsdservice scope, and — for --auth-method db — the signing identity (from the token's actorId). --endpoint overrides the catalog routing with an explicit VSDM endpoint (base URL only — scheme + host[:port], any path ignored). --profile-version sets the profileVersion query parameter sent to the endpoint (default 1.1; pass --profile-version 1.0 for the older profile). -H, --header overrides (or adds) headers on the inner VSDM request — it replaces the built-in Accept / If-None-Match / PoPP defaults by name (case-insensitive) and does not affect the outer ASL/CBOR transport. Supply a profile and an auth method as for zeta http.

zeta popp connector [EGK_HANDLE]

Option Env var Default
--service-url=<url> ZETA_POPP_SERVICE_URL popp dev service URL
--connection=contact|contactless ZETA_POPP_CONNECTION contact

EGK_HANDLE is positional and optional — auto-picked when exactly one eGK is visible.

zeta popp kartos

Option Env var Default
-i, --image=<path> ZETA_POPP_KARTOS_IMAGE
--kartos-bin=<path> ZETA_KARTOS_BIN kartos on PATH
--service-url=<url> ZETA_POPP_SERVICE_URL popp dev service URL

Note on repeatable options. --ca-cert, --header, and --scope accept multiple values on the CLI (repeat the flag) and in zeta.yaml (YAML list), but their env var holds only a single value. Use the CLI flag or YAML when you need more than one.

Configuration file: zeta.yaml

zeta.yaml lets you persist defaults so you don't repeat flags. Keys are the long option names without leading dashes (e.g. --auth-methodauth-method).

Lookup

--no-config / ZETA_NO_CONFIG -f / ZETA_CONFIG Loaded
no no ./zeta.yaml if present, else $XDG_CONFIG_HOME/telematik/zeta/zeta.yaml if present, else none
no yes the named file (must exist; missing → usage error)
yes no none — built-in defaults only
yes yes error: mutually exclusive

First hit wins — files are never merged. When -f is passed (or ZETA_CONFIG is set) the file must exist; a missing path fails fast with a usage error rather than silently falling through. Without -f, auto-discovery applies and is happy to find no file at all.

Selecting a config file (-f)

-f is the docker-compose idiom for switching between scenarios: keep one zeta.yaml per environment, point at one with the flag.

# ~/.config/telematik/zeta/zeta-popp-ru.yaml
profile: popp-ru                  # SDK state lands in popp-ru.storage.db
auth-method: connector
auth-connector-telematik-id: "${SMCB_TID_RU}"
scope: [popp]
zeta -f ~/.config/telematik/zeta/zeta-popp-ru.yaml http https://popp.dev.poppservice.de/some/api

# Or pin per-shell:
export ZETA_CONFIG=~/.config/telematik/zeta/zeta-popp-ru.yaml
zeta http https://popp.dev.poppservice.de/some/api
zeta popp connector

Pair each scenario file with its own profile: so SDK state (registrations, tokens) is also isolated — popp-ru writes to popp-ru.storage.db, popp-tu writes to popp-tu.storage.db, no cross-contamination.

Single file only — there is no docker-compose-style multi--f merging today.

For a totally clean run with no config at all, see --no-config below.

Disabling the config file (--no-config)

Use --no-config (or ZETA_NO_CONFIG=1) when you need built-in defaults only — no project-local ./zeta.yaml, no XDG fallback, no -f. Useful for:

  • Reproducing an issue without your zeta.yaml interfering.
  • One-off ad-hoc commands from an arbitrary directory.
  • CI runs where output must be predictable regardless of the surrounding environment.
zeta --no-config status         # ignores any zeta.yaml on disk
ZETA_NO_CONFIG=1 zeta status    # env-var equivalent

--no-config is mutually exclusive with -f / ZETA_CONFIG. Passing both is treated as a mistake — the CLI exits with Error: --no-config is mutually exclusive with -f / ZETA_CONFIG rather than silently picking one. Only 1, true, or yes (case-insensitive) activate the env var; 0/empty leave it off.

Structure

  • Top-level keys apply to any command that defines that option.
  • Subcommand-scoped keys (<subcommand>: { … }) only apply when that subcommand runs. Scoped wins over top-level.
  • ${VAR} placeholders are expanded against the process environment before parse — secrets and per-host overrides stay out of the file.
  • Repeatable options take a YAML list.

Reading the schema (TOML-like)

The shape is the same as a TOML file: flat key–value pairs at the top, named sections for scope. Two rules:

  • Top-level kebab keys are flag names verbatim. auth-method--auth-method. The hyphen is part of the option name, not a path separator.
  • Nested mappings are subcommand scope, not name hierarchy. http: { header: [...] } means "the --header flag when invoked with zeta http". Nesting depth mirrors the command path: popp.connector.connection is the --connection flag under zeta popp connector.

So auth-method and popp.connector.connection look different on the page because they are different things: a flag name vs. a command-scoped flag. The two never mix — a key never means "namespaced option".

Example

# Defaults for every command
profile: dev
connector-config: my-kon
auth-method: connector
auth-connector-telematik-id: "${SMCB_TID}"

# Per-subcommand overrides
http:
  header:
    - "X-Trace-Id: ${USER}-dev"
  include: true

ws:
  header:
    - "X-WS-Trace: ${USER}-dev"

popp:
  connector:
    connection: contactless

With this in ./zeta.yaml, the example from Quick start shrinks to:

zeta login https://popp.dev.poppservice.de --scope popp
zeta http  https://popp.dev.poppservice.de/some/api --scope popp
zeta popp connector

Precedence reminder

CLI flag > env var > zeta.yaml value > built-in default. So a YAML value never overrides what you typed at the prompt, but it does override what the binary ships with as default.

Caveat: sticky options + project YAML

A sticky option (e.g. --connector-config) declared at a parent depth (zeta --connector-config=X ...) gets re-read by the child from the same YAML key and may be overwritten. Workaround: pass sticky overrides at the deepest depth (zeta connector inspect --connector-config=X).

Logging & output

  • Logs go to stderr; data goes to stdout — safe for shell pipelines.
  • Default log level is warn. Bump with -v (info), -vv (debug), -vvv (trace).
  • -o json emits parseable JSON without colour when stdout is piped. -o raw (for http) prints the body verbatim with no framing.
  • discover, register, authenticate, login, and logout print a compact result — a green / red verdict with the endpoint, scopes, auth server, and resulting status. The verdict is the operation's own success test, not just the raw status (e.g. login is once an access or refresh token exists). Add --reveal for the full cached entry, or use zeta status for the detailed view.
  • Colour follows TTY detection and respects NO_COLOR / FORCE_COLOR.

SDK version

The CLI is built against a single zeta-sdk version (1.2.5), pinned in gradle/libs.versions.toml. zeta version prints it:

$ zeta version
zeta-cli 0.9.0
zeta-sdk 1.2.5

Override the pin at build time with -PzetaSdkVersion=<tag> (e.g. latest from mavenLocal).

Install

Requires JDK 21. The Gradle wrapper auto-provisions one if needed.

Dev mode (no install)

Run straight from sources via :cli:run — the ./zeta-dev wrapper forwards args verbatim:

./zeta-dev version
./zeta-dev discover https://popp.dev.poppservice.de

Local install via Gradle (no Homebrew)

./gradlew :cli:installDist
./cli/build/install/zeta/bin/zeta version
export PATH="$PWD/cli/build/install/zeta/bin:$PATH"   # optional

Local install via Homebrew

just install      # builds tarball, materialises a private tap, brew install
zeta version

Installs into $(brew --prefix)/bin and pins JAVA_HOME to Homebrew's openjdk@21.

Development

Common tasks are wired up in the justfile:

just install   # build and brew-install locally from Formula/zeta.rb
just demo      # render demo.gif from demo.tape (requires vhs)

Building against a different zeta-sdk

By default the CLI builds against the version of de.gematik.zeta:zeta-sdk-jvm pinned in gradle/libs.versions.toml (currently 1.2.5, resolved from Maven Central). To swap in a local SDK build during development without editing the catalog, override the Gradle property at the command line:

# Use whatever you publishToMavenLocal'd as `latest`
./gradlew :cli:installDist -PzetaSdkVersion=latest

# Or any other tag living in your ~/.m2/repository/de/gematik/zeta/zeta-sdk-jvm/
./gradlew :cli:installDist -PzetaSdkVersion=1.0.2-local

Set it persistently in ~/.gradle/gradle.properties (zetaSdkVersion=latest) to avoid passing the flag every time — Gradle will still let project- or command-line values override that.

zeta version prints the resolved SDK version so you always know which one shipped in the binary.

The release tarball (./gradlew :distTar) ships two entry points from one archive — bin/zeta (the CLI) and bin/zeta-stress (the load-test tool) — over a shared lib/.

About

Command-line client for gematik ZETA — discovery, client registration, authentication, and authenticated HTTP/WebSocket access, with Konnektor and PoPP support.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages