Docs

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 acme

2. Generate

iohr sdk generate --lang rust --for default --for ci --out src/iohr

Commit 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:

LanguageWhat refuses ci.accounts.get_me()
Rustthe compiler: the profile does not implement the operation's trait
TypeScripttsc: the property does not exist on type Ci (plain JavaScript has no check)
Gothe compiler: the profile's package has no such method
Pythonpyright or mypy: the attribute does not exist on Ci
C#the compiler: Ci does not implement the operation's interface (CS0311)
Javajavac: 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 "":

LanguageRequest field set byTimestamp helper
Rust..Default::default()inorbithr::parse_timestamp (None for "")
TypeScriptleaving the property outparseTimestamp (undefined for "")
Goinorbit.Ptr(v) on a pointerinorbit.ParseTimestamp (the zero time.Time)
Pythonkeyword argumentsparse_timestamp (None for "")
C#object initializersTimestamps.Parse (null for "")
Javathe builderTimestamps.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,macros

C# 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 result

A 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.