Docs
Generate an SDK for your account
A client in Rust, TypeScript, Go, Python, C# or Java with exactly the operations your credentials may call, for one account or several, checked by the compiler and by a CI step when the API's cut moves.
The API serves each credential its own document: the
routes its plan has, narrowed to the scopes it holds. iohr sdk generate turns that
document into a client in your repository. The client holds exactly the operations each
of your profiles may call, so a call a profile may not make does not compile, and
iohr sdk check fails in CI when the set has moved. It writes Rust, TypeScript (and
plain JavaScript), Go, Python, C# and Java.
1. Sign in, one profile per account
Install the command line, then sign in once per credential the client will use. A person's sign-in reads their own account; an API token stands for the account it was made in:
iohr login # you, as a person: profile "default"
iohr login --with-token --profile ci < token.txt # an API token for CI, as profile "ci"A person belongs to several accounts. Point the profile at the team the client is for; a team's id or its slug works, and only a team you belong to is accepted:
iohr accounts list
iohr profile account default acme2. Generate
iohr sdk generate --lang rust --for default --for ci --out src/iohrCommit the output directory and the iohr.lock written beside it.
--for names a profile; repeat it to put several accounts in one client. The command
fetches each profile's document, writes the client into --out, and writes
iohr.lock beside the directory. Nothing in either is a secret: the lock holds profile
names, each cut's plan, scopes and hash, and the operations it held.
# iohr.lock: what this surface was generated from. Commit it beside the surface;
# `iohr sdk check` compares it with what the API serves now and says what moved.
generator = "0.1.0-alpha.5"
lang = "rust"
out = "iohr"
api_version = "0.1.0"
[profiles.ci]
cut = "sha256:…"
plan = "free"
scopes = ["radar:read"]
operations = ["GET /v1/radar/digests", "GET /v1/radar/digests/{id}", "GET /v1/radar/items"]
[profiles.default]
cut = "sha256:…"
plan = "free"
account = "…"
operations = ["GET /v1/accounts/me", "GET /v1/me", "GET /v1/radar/digests", "…"]The output is the same for the same document and the same iohr version, so a
regeneration shows as a real diff in review. Regenerate it, never edit it. To work
offline, save a document with iohr openapi pull and pass it as --from NAME=FILE.
3. Use it
The generated code sits on each language's runtime, which does the credentials, retries
and errors. Each profile is its own type. Its from_env reads INORBIT_CI_TOKEN, or
INORBIT_CI_KEY_ID, INORBIT_CI_KEY_SECRET and INORBIT_CI_SCOPES for an API key, and
nothing else, so a client never calls the wrong account:
mod iohr;
use iohr::prelude::*;
use iohr::RadarListDigestsParams;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Error> {
let ci = Client::<Ci>::from_env()?;
let digests = ci.radar().list_digests(&RadarListDigestsParams::default()).await?;
println!("{} digests, request id {}", digests.value.digests.len(), digests.raw.request_id);
Ok(())
}Operations hang off one handle per area (radar, accounts, events), and GET /v1/me
is me() on the client. Every answer carries the typed body (value) and the raw
answer: status, headers and request ids. Python also has an asyncio class per profile
(AsyncCi), Java an ...Async twin of every call, and C# and TypeScript are async
throughout.
A call the profile may not make
The ci profile above holds radar:read only, so asking it for the account is refused
before the program runs, not with a 403 at run time:
| Language | What refuses ci.accounts.get_me() |
|---|---|
| Rust | the compiler: the profile does not implement the operation's trait |
| TypeScript | tsc: the property does not exist on type Ci (plain JavaScript has no check) |
| Go | the compiler: the profile's package has no such method |
| Python | pyright or mypy: the attribute does not exist on Ci |
| C# | the compiler: Ci does not implement the operation's interface (CS0311) |
| Java | javac: the method does not exist on the profile's class |
The default profile, a person, has it.
Every item of a paged list
A list operation has an iterator beside its page method that follows the next-page token, fetches nothing more once you stop, and stops at the first error (paging):
let mut pages = ci.radar().all_list_digests(&RadarListDigestsParams::default());
while let Some(digest) = pages.next().await {
// ...
}Requests, answers and timestamps
The generated types follow the API's document. Every field of a request is optional: set
what you need, and an unset field is left out of the body, so the API uses its default.
A field of an answer is typed as always present exactly when the document marks it
required; a field that may be absent (a nested message, an optional field) is
optional. Timestamps are RFC 3339 strings, and an unset one is ""; each runtime has one
helper that reads a timestamp and gives no value for "":
| Language | Request field set by | Timestamp helper |
|---|---|---|
| Rust | ..Default::default() | inorbithr::parse_timestamp (None for "") |
| TypeScript | leaving the property out | parseTimestamp (undefined for "") |
| Go | inorbit.Ptr(v) on a pointer | inorbit.ParseTimestamp (the zero time.Time) |
| Python | keyword arguments | parse_timestamp (None for "") |
| C# | object initializers | Timestamps.Parse (null for "") |
| Java | the builder | Timestamps.parse (an empty Optional for "") |
A stream of events
A streaming operation is a method of the same name that yields one model per event:
the account's events (events.stream_events, scope events:read) as they happen. It
opens as server-sent events; a client set to streams: socket
carries every stream over one WebSocket instead, and
opens it again by itself when the server ends the socket. A revoked key ends the
stream with unauthenticated; silence for 45 seconds ends it with a timeout. The
limits say how many a key may hold.
let mut events = ci.events().stream_events(&EventsStreamEventsParams::default()).await?;
while let Some(event) = events.next().await {
let event = event?;
println!("{} {}", event.type_, event.id);
}Breaking out of the loop, cancelling, or closing the stream closes it on the server too. Over the socket, browsers cannot send the token on a WebSocket, so the TypeScript client uses server-sent events there.
The runtime as a dependency
The generated code needs its language's runtime, 0.2.0 or later for the helpers above. Rust, TypeScript, Python and Go are on their registries:
cargo add inorbithr
cargo add tokio --features rt,macrosC# and Java are built, pass the same conformance suite and carry the same version (0.2.2); their NuGet and Maven Central releases come later, and until then each builds from the repository as its README shows.
Configuration and middleware
From 0.2.2 every runtime loads its settings the same way: code, then INORBIT_*
environment variables, then the iohr config file, then defaults. It also has a credential
chain, proxy, CA bundle and mTLS settings, a 120 s total deadline per call, and a named
middleware pipeline for logging, OpenTelemetry, rate limits and retries. Writes that take an
Idempotency-Key are retried with one key per call. The settings are in the SDK
repository's configuration reference,
with worked examples in recipes.
iohr sdk config prints what a program would load, with secrets redacted.
4. Check it in CI
The API's cut moves when a plan changes or a scope is revoked. iohr sdk check fetches
every profile's document again and exits 1 with what moved:
profile ci: the cut moved (sha256:3f… -> sha256:9a…)
- GET /v1/radar/items
1 profile moved; run `iohr sdk generate` again and commit the resultA person cannot sign in on CI, so give each profile a token there: IOHR_TOKEN_CI for
ci, IOHR_TOKEN_DEFAULT for default. A token's cut is its own, so for a person's
profile use a token made for the same account with the scopes the client needs, or leave
that profile out of the lock.
# .github/workflows/sdk.yml
jobs:
sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: curl -fsSL https://packages.inorbit.hr/install.sh | sh
- run: ~/.local/bin/iohr sdk check --files
env:
IOHR_TOKEN_CI: ${{ secrets.IOHR_TOKEN_CI }}--files also regenerates into a temporary directory and compares it with what is
committed, so a hand edit or a stale generator version fails too. Exit codes are the
command line's: 0 nothing moved, 1 the cut or the files moved, 3 a token was
refused, 4 the account is not the token's.
What is not here yet
- A person's client is cut by the team's plan, not by the person's role in it; a write
the team refuses by role still answers
403.