ts / src / client.ts
372 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * The client is a frozen plain value: create one with `createClient` and
 * pass it as the first argument to the functions in the resource modules
 * (`domains`, `vps`, `jobs`, ...).
 *
 * 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 — pass whichever as `apiKey`. A client without `apiKey` can call the
 * anonymous endpoints (domain check/example, device login).
 */
import { API_BASE_URL } from './config.js';
import { attachGate, type Gate, type GateRules } from './gate.js';

export type SleepFn = (ms: number) => Promise<void>;

export type ClientOptions = Readonly<{
  /** API key (`ird_...`) or account JWT, sent as a Bearer token. */
  apiKey?: string;
  /** Base URL override; defaults to https://api.ird.ro. */
  apiUrl?: string;
  /** Extra headers merged over the computed ones on every request. */
  headers?: Readonly<Record<string, string>>;
  /** Per-request timeout, applied with AbortSignal.timeout. */
  timeoutMs?: number;
  /** Transport override (tests, proxies, instrumentation); defaults to global fetch. */
  fetch?: typeof fetch;
  /** Delay used by the polling helpers; defaults to setTimeout. */
  sleep?: SleepFn;
}>;

export type Client = Readonly<{
  apiKey?: string;
  apiUrl: string;
  headers: Readonly<Record<string, string>>;
  timeoutMs?: number;
  fetch: typeof fetch;
  sleep: SleepFn;
  /** Present on clients created with `createGatedClient`. */
  gate?: Gate;
}>;

export const defaultSleep: SleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/** Create an ird API client. Synchronous; makes no requests. */
export const createClient = (options: ClientOptions = {}): Client =>
  Object.freeze({
    apiKey: options.apiKey,
    apiUrl: (options.apiUrl?.trim() || API_BASE_URL).replace(/\/+$/, ''),
    headers: Object.freeze({ ...options.headers }),
    timeoutMs: options.timeoutMs,
    fetch: options.fetch ?? globalThis.fetch,
    sleep: options.sleep ?? defaultSleep,
  });

/**
 * Create a client with a domain gate: `deniedDomains` are names/patterns
 * (`"example.ro"`, `"*.example.ro"`) this client must never mutate, and
 * `allowedDomains`, when set, are the ONLY names it may mutate (an empty
 * list denies every domain mutation; deny wins over allow).
 *
 * Requires `apiKey`: 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. Violations throw a
 * `domainDenied` error. See `gate.ts`.
 */
export const createGatedClient = async (options: ClientOptions & GateRules): Promise<Client> =>
  attachGate(createClient(options), options);

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