clj

clj/

ird-clj

Full-coverage Clojure client for the ird public API — .ro domains, DNS (hosted zones, DNSSEC, bring-your-own-domain claims), VPS, tunnels, IPFS, P2P apps (including their offline mailbox), and async jobs (LLM, NLP, embeddings, bulk domain checks).

Every one of the API's 161 operations is wrapped by a thin, documented function, verified one-to-one against the OpenAPI spec by the test suite.

Install

;; deps.edn
ro.ird/ird-clj {:git/url "https://ird.cod-sursa.ro/clj.git" :git/sha "…"}

Pin :git/sha to the latest commit:

git ls-remote https://ird.cod-sursa.ro/clj.git HEAD

The landing page at https://ird.cod-sursa.ro/clj/ 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

(require '[ird.client :as ird]
         '[ird.domains :as domains]
         '[ird.credits :as credits])

;; API key from your account panel (or an account JWT — same field)
(def c (ird/make-client {:api-key (System/getenv "IRD_API_KEY")}))

(credits/balance c)
;; => {:balance {:credits 1234.5 ...}}

(domains/check c "example.ro")
;; => {:domain "example.ro" :status "registered" ...}

;; a few endpoints work without a key:
(domains/check (ird/make-client {}) "example.ro")

Authentication

One Bearer credential, two forms — pass either as :api-key:

(require '[ird.auth :as auth])
(def tokens (auth/device-login (ird/make-client {})))
;; prints a verification URL, polls until you approve, prompts for the code
(def c (ird/make-client {:api-key (:access_token tokens)}))

A few account-level areas accept only an account JWT (API keys are rejected with 401): everything in ird.sharing and ird.transfers, plus the API-key management functions in ird.auth.

Domain gate (client-side safety net)

Restrict what a client may touch, independent of account permissions — useful for agents and automation:

;; this client can never mutate prod.ro or anything under it,
;; even though the account can:
(def c (ird/make-client {:api-key key
                         :denied-domains ["prod.ro" "*.prod.ro"]}))

;; or allow-list: ONLY these may be mutated
(def c (ird/make-client {:api-key key
                         :allowed-domains ["staging.ro" "*.dev.ro"]}))

Domain-touching mutations (registrations, DNS zones and claims, sharing invites, tunnel/gateway/p2p domain attach and detach/delete, changes to the pins a gateway domain serves, VPS domain connect/disconnect, action cancellation) are checked before any request is sent; violations throw an :ird/domain-denied ex-info. Reads are never gated. Registration-id-based operations resolve through a domain cache built at client creation; a cache miss falls back to fetching the single registration (and caches it) — ird.gate/refresh! still re-syncs the whole cache. Ids that still cannot be resolved fail closed under an allow-list. An empty :allowed-domains collection denies every domain mutation.

Namespaces

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

* ird.mailbox talks to https://<app-host>/mailbox/v1 rather than api.ird.ro, so its two endpoints sit outside the 161 and outside the coverage table.

Conventions (shared by every function):

Pagination

Cursor-paginated listings have a *-all companion that follows next_cursor and returns the collected vector: registrations/list-all-registrations, registrations/list-all-actions, credits/ledger-all, domains/directory-all, support/list-all-threads.

Offset-paginated listings have one too, walking limit/offset until a short page: ipfs/list-all-pins, ipfs/list-all-domain-pins. Both take an optional query map (filters plus a page :limit, default 100).

Async patterns

Jobs — enqueue → poll → result, or in one call:

(require '[ird.jobs :as jobs])

(jobs/run c (jobs/domain-availability-bulk ["a.ro" "b.ro"]))
(jobs/run c (jobs/llm-chat [{:role "user" :content "Salut!"}] {:max_tokens 256}))
(jobs/run c (jobs/embedding "text to embed"))
(jobs/run c (jobs/nlp-detect-language "ce limbă e asta?"))

Builders exist for all job types: domain-availability-bulk, embedding, llm-chat, llm-raw-completion, nlp-analyze, nlp-detect-language. Multimodal content-part helpers (jobs/text-part, jobs/image-part, jobs/user-message) are included forward-compatibly; the public API currently accepts string content only.

Registry / VPS actions — 202 responses return an action; poll registrations/get-action / vps/get-action until terminal.

IPFS uploads — grant → raw-byte POST → poll, or in one call:

(require '[ird.ipfs :as ipfs])
(ipfs/add-file c "site.tar" {:name "site"})
;; => {:cid "bafy..." :pin_id "..." :status "completed" ...}

IPFS gateway domains — serve your own pins from your own domain:

(ipfs/attach-domain c {:domain "cdn.example.ro" :pin_ids ["pin-1"]})
;; => {:domain {:id "gw-1" :dns_target "1.2.3.4" :pin_count 1 ...}}
;; point cdn.example.ro's A record at :dns_target (linked rows get it for free)

(ipfs/update-domain-pins c "gw-1" {:add ["pin-2"] :remove ["pin-1"]})
(ipfs/list-all-domain-pins c "gw-1")
;; => [{:id "pin-2" :cid "bafy..." :cid_v1 "bafy..." :state "pinned" ...}]

Bring-your-own-domain DNS — host a domain registered elsewhere on ird DNS:

(require '[ird.dns :as dns])

;; take the copy while the domain still resolves at its current host
(let [snapshot (domains/dns-snapshot c "example.com")
      claim (:claim (dns/create-claim c {:domain "example.com"}))]
  (dns/seed-claim c (:id claim) {:snapshot_id (:id snapshot)})
  ;; set (:nameservers claim) at the registrar, then:
  (dns/verify-claim c (:id claim)))

P2P offline mailbox — the catch-up path for peers that are never online at the same time. Turn it on per app, then deposit an encrypted payload for a room and let a peer arriving later replay it:

(require '[ird.p2p :as p2p]
         '[ird.mailbox :as mailbox])

(p2p/update-app c "app-1" {:mailbox_enabled true})

;; one short-lived token, reused across calls (every mint is charged)
(def creds (mailbox/credentials c "app-1" {:room db-address :ttl_sec 3600}))
;; => {:token "..." :endpoint "https://sueta.ap2p.ro/mailbox/v1" :ttl 3600 ...}

(mailbox/deposit creds db-address ciphertext)
;; => {:seq "1874163641836634112" :id "51f2..." :expires_at ... :deduped false}

(mailbox/replay-all creds db-address {:after cursor})
;; => {:messages [{:seq "..." :id "..." :ts ... :payload "<base64>"}]
;;     :cursor "1874163641836634112" :gap? false}

Calls go to the app's own node, not api.ird.ro; credentials mints the token and hands you the :endpoint in one map. Payloads are opaque to the node, which holds no keys — encrypt before depositing. mailbox/payload-bytes and payload-string decode a replayed message.

Two things the node's design asks of you:

Identical payloads deduplicate per room, so retries — and the same block deposited by five peers — store and bill once (:deduped true).

Tests

clojure -M:test                            # unit + full-coverage table (offline)
clojure -M:test --focus-meta :integration  # live read-only tests; needs IRD_API_KEY (env or .env)

The coverage test invokes every one of the 161 wrapped operations against a capturing transport and asserts the exact method and URL, so client and spec cannot drift silently. spec/openapi.json is the spec snapshot the client was built against.

That pins the client to the spec, but says nothing about whether the spec still matches the API. The :integration run adds a conformance sweep that closes the gap: it calls every read endpoint live and validates each response body against the schema the spec declares for it — types, required keys, enum membership, and (since every object is additionalProperties: false) fields the server returns that the schema never mentions. It is read-only, creates nothing, and skips endpoints whose resource the account does not have.

It has found three real spec bugs so far — two endpoints returning fields their own schema forbade, and an action-type enum missing a value the API demonstrably returns — all since fixed upstream. ird.spec-check-test runs offline and pins each class of drift the detector is meant to catch, because a conformance sweep whose detector quietly stopped detecting would pass forever.

Build

clojure -T:build jar      # target/ird-clj-0.2.0.jar
clojure -T:build install  # to local maven repo
clojure -T:build deploy   # to Clojars (CLOJARS_USERNAME/CLOJARS_PASSWORD)

License

MIT

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