(ns ird.client
"ClojureScript (Node.js) client for the ird public API (https://api.ird.ro).
Create a client with `make-client`, then call functions in the resource
namespaces (`ird.domains`, `ird.vps`, `ird.jobs`, …). Every one of those
returns a core.async channel delivering exactly one value: the parsed
response map, or an ExceptionInfo (see ird.async and ird.errors). Request
bodies and responses use plain maps whose keys match the documented
snake_case field names (e.g. {:auto_renew true}).
Authentication is a single Bearer credential: either an API key created in
the account panel (`ird_...`) or an account JWT from the device login flow
(ird.auth/device-login) — pass whichever as :api-key. A client without
:api-key can call the anonymous endpoints (domain check/example, device
login)."
(:require [clojure.string :as str]
[cljs.core.async :as async]
[ird.config :as config]
[ird.errors :as err]
[ird.gate :as gate]
[ird.transport :as transport]))
(defn make-client
"Create an ird API client: a plain map. Synchronous, makes no requests.
Options:
:api-key (optional) API key (`ird_...`) or account JWT, sent as a
Bearer token. Omit for anonymous access to the few endpoints
that allow it.
:api-url (optional) base URL override; defaults to https://api.ird.ro.
:headers (optional) extra headers merged over the computed ones on
every request (e.g. {\"User-Agent\" \"my-app\"}).
:timeout-ms (optional) per-request timeout, applied with
AbortSignal.timeout; a timed-out request is delivered as an
:ird/transport-error.
:http-fn (optional) transport override for tests, proxies and
instrumentation: a fn of the request map (see
ird.transport/build-request) returning a channel of
{:status <int> :body <string>} or an Error. Defaults to
ird.transport/fetch-http over the global fetch.
:sleep-fn (optional) fn of ms returning a channel that closes after
ms, used by the polling helpers; defaults to
cljs.core.async/timeout.
:verbose (optional) when truthy, logs method and URL of each request
to stderr.
Domain-gate options (:denied-domains / :allowed-domains) need a network
round trip and are therefore only accepted by `make-gated-client`; passing
them here throws an :ird/config-error."
[{:keys [api-key api-url headers timeout-ms http-fn sleep-fn verbose] :as opts}]
(when (or (contains? opts :denied-domains) (contains? opts :allowed-domains))
(throw (err/config-error
"Domain gate options need a request to attach: use make-gated-client (returns a channel)"
(select-keys opts [:denied-domains :allowed-domains]))))
{:api-key api-key
:api-url (-> (or (not-empty (some-> api-url str/trim)) config/api-base-url)
(str/replace #"/+$" ""))
:headers (or headers {})
:timeout-ms timeout-ms
:http-fn (or http-fn transport/fetch-http)
:sleep-fn (or sleep-fn async/timeout)
:verbose (boolean verbose)})
(defn make-gated-client
"Create a client with a client-side domain gate. Takes every `make-client`
option plus:
:denied-domains domain names/patterns (e.g. \"example.ro\",
\"*.example.ro\") that this client must never mutate,
even though the account could.
:allowed-domains when set, ONLY these domain names/patterns may be
mutated; everything else is denied (an empty collection
denies every domain mutation). Deny wins over allow.
Requires :api-key: the account's registrations are fetched up front to map
registration ids to domain names, so every domain-touching mutation is
checked client-side before any request is made; a violation is delivered
as an :ird/domain-denied error. See ird.gate.
Returns a channel delivering the gated client, or an error (an
:ird/config-error without :api-key, or whatever the registration fetch
failed with)."
[{:keys [denied-domains allowed-domains] :as opts}]
(gate/attach (make-client (dissoc opts :denied-domains :allowed-domains))
{:denied-domains denied-domains
:allowed-domains allowed-domains}))