ts

ts/

ird-ts

TypeScript 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 a small typed function, with request and response types generated from the OpenAPI spec and verified one-to-one against it by the test suite. Zero runtime dependencies: Node 18+ fetch, ESM and CommonJS.

Install

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

{
  "dependencies": {
    "ird-ts": "git+https://ird.cod-sursa.ro/ts.git#semver:^0.2.0"
  }
}

or pinned to an exact commit:

git ls-remote https://ird.cod-sursa.ro/ts.git
npm install "git+https://ird.cod-sursa.ro/ts.git#<sha>"

The compiled dist/ is committed, so installing needs no build step and no dev dependencies.

The landing page at https://ird.cod-sursa.ro/ts/ 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 { createClient, domains, credits } from 'ird-ts';

// API key from your account panel (or an account JWT — same field)
const client = createClient({ apiKey: process.env.IRD_API_KEY });

const { total } = await credits.balance(client);
// total: "1234.50"  (money is always a decimal string)

const { status } = await domains.check(client, 'example.ro');
// status: "registered"

// a few endpoints work without a key:
await domains.check(createClient(), 'example.ro');

CommonJS works the same way:

const { createClient, domains } = require('ird-ts');

Authentication

One Bearer credential, two forms — pass either as apiKey:

import { auth, createClient } from 'ird-ts';

const tokens = await auth.deviceLogin(createClient());
// prints a verification URL, polls until you approve, prompts for the code
const client = createClient({ apiKey: tokens.access_token });

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

Errors

Every failure is a plain Error tagged with a kind; narrow with the type guards or on kind:

import { isApiError, isDomainDeniedError, registrations } from 'ird-ts';

try {
  await registrations.register(client, { domain: 'example.ro', years: 1 });
} catch (e) {
  if (isApiError(e)) console.log(e.status, e.code, e.message, e.response);
  else if (isDomainDeniedError(e)) console.log(e.domain, e.op);
  else throw e;
}
kind thrown when
api non-2xx response (code, status, response)
transport connection failure, or a 2xx body that is not JSON
config bad arguments or client options, before any request
domainDenied the domain gate blocked a mutation
timeout a polling helper ran past its deadline (last response attached)
jobFailed / uploadFailed jobs.run / ipfs.addFile reached a terminal failure

Domain gate (client-side safety net)

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

import { createGatedClient } from 'ird-ts';

// this client can never mutate prod.ro or anything under it,
// even though the account can:
const client = await createGatedClient({ apiKey, deniedDomains: ['prod.ro', '*.prod.ro'] });

// or allow-list: ONLY these may be mutated
const client = await createGatedClient({ apiKey, allowedDomains: ['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 a domainDenied error. Reads are never gated. Registration-id-based operations resolve through a domain cache built at client creation (which is why the gated constructor is async); a cache miss falls back to fetching the single registration and caches it — refreshGate re-syncs the whole cache. Ids that still cannot be resolved fail closed under an allow-list. An empty allowedDomains list denies every domain mutation; deny wins over allow.

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
mailbox 2* the offline mailbox itself — deposit/replay on the app's own node
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

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

Types

Types come from spec/openapi.json via openapi-typescript (committed as src/generated/api.ts, regenerate with npm run gen:types). Because the spec has no operation ids, every type is addressed by path and method through four helpers:

import type { Res, Body, Query, Params } from 'ird-ts';

type Registration = Res<'/domains/registrations/{id}', 'get'>['registration'];
type RegisterBody = Body<'/domains/registrations', 'post'>;
type SearchQuery = Query<'/domains/search', 'get'>;

http.request(client, 'get', '/any/spec/path', { params, query, body }) is the typed escape hatch for calling an operation directly; http.requestUrl does the same against an absolute URL.

Pagination

Cursor-paginated listings have an All companion that follows next_cursor and returns the collected array: registrations.listAll, registrations.listAllActions, credits.ledgerAll, domains.directoryAll, support.listAllThreads.

Offset-paginated listings have one too, walking limit/offset until a short page: ipfs.listAllPins, ipfs.listAllDomainPins. All take an optional query (filters plus a page limit, default 100).

Async patterns

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

import { jobs } from 'ird-ts';

await jobs.run(client, jobs.domainAvailabilityBulk(['a.ro', 'b.ro']));
await jobs.run(client, jobs.llmChat([{ role: 'user', content: 'Salut!' }], { max_tokens: 256 }));
await jobs.run(client, jobs.embedding('text to embed'));
await jobs.run(client, jobs.nlpDetectLanguage('ce limbă e asta?'));

Builders exist for all job types: domainAvailabilityBulk, embedding, llmChat, llmRawCompletion, nlpAnalyze, nlpDetectLanguage. Multimodal content-part helpers (jobs.textPart, jobs.imagePart, jobs.userMessage) are included forward-compatibly; the public API currently accepts string content only.

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

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

import { ipfs } from 'ird-ts';

const upload = await ipfs.addFile(client, 'site.tar', { name: 'site' });
// { cid: "bafy...", pin_id: "...", status: "completed", ... }

addFile accepts a file path, bytes, a Blob, or a ReadableStream.

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

const { domain } = await ipfs.attachDomain(client, { domain: 'cdn.example.ro', pin_ids: ['pin-1'] });
// point cdn.example.ro's A record at domain.dns_target (linked rows get it for free)

await ipfs.updateDomainPins(client, domain.id, { add: ['pin-2'], remove: ['pin-1'] });
await ipfs.listAllDomainPins(client, domain.id);

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

import { dns, domains } from 'ird-ts';

// take the copy while the domain still resolves at its current host
const snapshot = await domains.dnsSnapshot(client, 'example.com');
const { claim } = await dns.createClaim(client, { domain: 'example.com' });
await dns.seedClaim(client, claim.id, { snapshot_id: snapshot.id });
// set claim.nameservers at the registrar, then:
await dns.verifyClaim(client, claim.id);

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:

import { mailbox, p2p } from 'ird-ts';

await p2p.updateApp(client, 'app-1', { mailbox_enabled: true });

// one short-lived token, reused across calls (every mint is charged)
const creds = await mailbox.credentials(client, 'app-1', { room: dbAddress, ttl_sec: 3600 });
// { token, endpoint: "https://<node>/mailbox/v1", ttl: 3600, ... }

await mailbox.deposit(creds, dbAddress, ciphertext);
// { seq: "1874163641836634112", id: "51f2...", expires_at, deduped: false }

const { messages, cursor, gap } = await mailbox.replayAll(creds, dbAddress, { after: lastCursor });

Calls go to the app's own node, not api.ird.ro; credentials mints the token and hands you the endpoint in one value. Payloads are opaque to the node, which holds no keys — encrypt before depositing. mailbox.payloadBytes and payloadString 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

npm test         # unit + full-coverage table (offline)
npm run test:live  # live read-only tests; needs IRD_API_KEY (env or .env)

The coverage test invokes every one of the 161 wrapped operations against a recording fetch 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 live run adds a conformance sweep that closes the gap: it calls every read endpoint 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. The validator (spec-check.ts) is shipped and has its own offline tests pinning each class of drift it must catch.

Build

npm run build      # dist/esm + dist/cjs with declarations (committed)
npm run typecheck
npm run gen:types  # regenerate src/generated/api.ts from spec/openapi.json

Because consumers install from git, dist/ is committed and every tagged release points at a commit whose dist/ matches its src/ (npm run check:dist verifies that).

License

MIT

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