api-swift

api-swift/

ird-api-swift

Native Swift client for the whole ird API: iOS 16+ and macOS 13+, one library product, IrdAPI. Every one of the API's 161 operations is a typed async throws method — domains, DNS, servers, IPFS, tunnels, p2p apps, credits, support, sharing, transfers, jobs and the account's API keys — with models generated from the OpenAPI spec, so a native app reads instance.state and balance.total, never a dictionary.

It is the control-plane companion of ird-swift (IrdP2P, the p2p SDK), and behaves exactly like the TypeScript, ClojureScript and Clojure clients: same encoding rules, same error table, same client-side domain gate, same flows.

Install

There is no registry; the package is a git dependency served straight from this domain:

// Package.swift
dependencies: [
    .package(url: "https://ird.cod-sursa.ro/api-swift.git", from: "0.1.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [.product(name: "IrdAPI", package: "api-swift")]),
]

In Xcode: File ▸ Add Package Dependencies…, paste https://ird.cod-sursa.ro/api-swift.git, choose "Up to Next Major Version" from 0.1.0, and add the IrdAPI product to your app target. The package has no dependencies.

Or pinned to an exact commit:

git ls-remote https://ird.cod-sursa.ro/api-swift.git
.package(url: "https://ird.cod-sursa.ro/api-swift.git", revision: "…")

The landing page at https://ird.cod-sursa.ro/api-swift/ shows the current release coordinates ready to paste, and https://ird.cod-sursa.ro/releases.json carries them for scripts and agents (head, tag, tag_sha per repo).

Quickstart

import IrdAPI

let client = IrdClient(apiKey: "ird_...")          // or a Keychain-backed credential, see below

let balance = try await client.credits.balance()
print(balance.total)                                // "509.72" — money is always a decimal String

let check = try await IrdClient().domains.check(domain: "numele-meu.ro")   // anonymous is fine here
if check.status == .available {
    let queued = try await client.registrations.register(body: RegisterBody(domain: check.domain, years: 1, useOurDns: true))
    let action = try await client.registrations.waitForAction(id: queued.action.id)   // polls until the registry answers
    print(action.state)                             // .succeeded / .failed / .cancelled
}

for instance in try await client.vps.listInstances().instances where instance.state == .running {
    print(instance.name, instance.ipv4 ?? "-")
}

let upload = try await client.ipfs.addFile(url: fileURL, name: "site.tar")   // grant → stream the bytes → poll
print(upload.cid ?? "")

let result = try await client.jobs.run(.llmChat(messages: [.user("Salut!")], maxTokens: 256))
print(result.result)                                // [String: JSONValue]

Module accessors mirror the API's areas: client.auth, .credits, .support, .domains, .registrations, .dns, .sharing, .vps, .transfers, .tunnels, .ipfs, .pins, .p2p, .jobs. Path parameters are labeled arguments, an optional query: struct comes next, and the JSON body (a generated struct) comes last. Listings with pages have an All companion (registrations.listAll, ipfs.listAllPins, …).

Authentication

One credential slot takes either an API key (ird_..., created in the account panel) or an account session (a JWT from the device login). Account-only operations (API keys, sharing, VPS transfers, sensitive domains) answer 401 to an API key; the generated doc comment on each method says which.

// Fixed key
let client = IrdClient(apiKey: "ird_...")

// Keychain-backed: consulted on every request, so a token stored later is picked up
let store = KeychainCredentialStore(service: "com.example.myapp")
let client = IrdClient(credential: store)

// Device login (`ird login`) in three phases a SwiftUI app can drive
let start = try await client.auth.startDeviceLogin()
open(URL(string: start.verificationUriComplete)!)   // show start.userCode too
try await client.auth.waitForDeviceApproval(start)   // 400 "pending" and 429 "slow down" are just "not yet"
let session = try await client.auth.completeDeviceLogin(start, confirmationCode: codeFromTheBrowser)
try store.save(session.accessToken)

CredentialProvider is a one-method protocol, so any token source works; InMemoryCredentialStore is for tests and previews. The device-login operations are anonymous and never send a bearer, whatever the client holds.

Errors

Everything throws IrdError:

case when
.api(status:code:message:body:) the API answered non-2xx; code and message come from the {error:{code,message}} envelope, body is the parsed JSON
.transport(_) DNS, TLS, socket, timeout, or a 2xx body that is not JSON / does not decode into the typed model
.config(_) a mistake caught before any request (missing path parameter, gate without a credential)
.domainDenied(domain:pattern:op:) the domain gate refused a mutation
.timeout(message:last:) a poll loop (jobs.run, addFile, waitForAction, device login) passed its deadline; last is the last response
.jobFailed(_), .uploadFailed(_) the job or upload settled in a failed state; the value carries the server's reason

Helpers read the common cases: error.isAuth (401), .isInsufficientCredits (402), .isRateLimited (429), .isNotFound, .needsTOTP (totp-required / invalid-totp: repeat the call with a fresh authenticator code in totpCode), .isAPIKeyBlocked (sensitive-domain-api-key-blocked: the operation needs an account session). There is no HTTP-level retry: a failed request is reported at once.

Domain gate

For agents and automation, a client can carry a domain allow/deny list that refuses domain-touching mutations (POST/PUT/PATCH/DELETE) before any request is sent. Reads are never gated.

let client = try await IrdClient.gated(Configuration(apiKey: "ird_..."),
                                       rules: GateRules(deniedDomains: ["*.prod.ro"], allowedDomains: ["sandbox.ro"]))
try await client.tunnels.create(body: CreateTunnelBody(hostname: "dev.prod.ro"))
// throws IrdError.domainDenied(domain: "dev.prod.ro", pattern: "*.prod.ro", op: "POST /tunnels")

Names are compared lower-case without trailing dots; *.x.ro also matches x.ro; deny wins over allow; a nil allow-list means "no allow-list" while [] allows nothing. Mutations that address a domain by registration id resolve it through an index built when the client is created (refreshGate() rebuilds it); a few resolve it with one read first (a tunnel, a claim, a gateway domain). An unresolvable domain passes under a pure deny-list and fails closed under an allow-list. The 43 gated operations are the same as in the sibling clients (Sources/IrdAPI/Gate/GatedOperations.swift).

Modules

Module Endpoints Covers
domains 9 availability check/search, utilities, .ro web directory
registrations 27 register, renew, transfer, trade, hosts, DS data, registrant
dns 18 hosted DNS zones, records, DNSSEC, nameserver sets, bring-your-own-domain claims
sharing 7 delegated-access grants, both directions
vps 34 tiers, instances, one-click apps, volumes, snapshots
transfers 9 cross-account VPS resource handover
tunnels 6 HTTPS tunnels on your own domain
ipfs 18 pinning, direct uploads, IPNS, gateway domains and the pins they serve
pins 5 standard IPFS Pinning Service API at /pins
p2p 12 p2p apps, TURN and offline-mailbox credentials, custom domains
jobs 3 async jobs: LLM chat/completion, embeddings, NLP, bulk checks
credits 3 balance, ledger, prices
support 4 support threads
auth 6 device login, API keys

The p2p offline mailbox itself (deposit/replay on the app's own node) is not an api.ird.ro operation; it ships natively in IrdP2P. Minting its credentials (p2p.mintMailboxCredentials) is here.

Conventions

Async patterns

Example app

Examples/IrdManager is a SwiftUI account manager for macOS 14+ built on the package: sign in with an API key or the device login (stored in the Keychain), then credits and ledger, domains (register with an availability check, renew, auto-renew, nameservers, glue hosts), hosted DNS zones with a record editor, servers (create, power actions, destroy), IPFS pins (pin, unpin, upload a file), tunnels, p2p apps, support threads with replies, and API keys. Sensitive-domain step-ups open an authenticator prompt and retry with the code. Open Examples/IrdManager/IrdManager.xcodeproj and run it.

Tests

swift test                       # offline: request building, decode table, gate, flows, and the generated inventory
IRD_API_KEY=... swift test --filter Live   # read-only smoke tests and the spec conformance sweep against the live API

The generated inventory pins the client to the spec three ways: every spec operation has a method and nothing else does (OperationTableTests), every method issues exactly one request with the exact method and URL and decodes a synthesised response (CoverageTests, 161 cases), and every body and response type decodes and re-encodes the synthesised JSON losslessly (SynthRoundTripTests). The live sweep then pins the spec to the API: each read endpoint is fetched raw, checked against its declared schema — types, required, enum, and fields the server returns that the schema never mentions — and decoded through the typed method. Drift is printed and fails the run.

Build

swift build
swift test
scripts/fetch-spec.sh            # refresh spec/openapi.json from https://api.ird.ro/openapi.json
python3 scripts/gen-api.py       # regenerate the models, operations and test tables (--check verifies, --report inventories)
scripts/release-check.sh         # manifest, generated code up to date, build, offline tests, clean tree (MACAPP=1 builds the example)

SwiftPM versions come from git tags (vX.Y.Z); there is no version field to bump. The generator is Python 3 standard library only; its operation-name table and naming overrides at the top of scripts/gen-api.py are the only hand-edited part, and it refuses to run when the spec gains an operation the table does not know.

License

MIT.

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git