swadhinbiswas ✨ feat(cli): harden auth, TLS, retries, and repo parsing · 1/22/2026
416b898
137 lines · 3.3 KB
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
 * CLI API Client
 * Make authenticated requests to OpenCodeHub
 */

import fs from "fs";
import { getConfig } from "./config.js";

let tlsConfigured = false;

export function applyTlsConfig() {
  if (tlsConfigured) return;

  const config = getConfig();

  if (config.caFile) {
    if (!fs.existsSync(config.caFile)) {
      throw new Error(`CA file not found: ${config.caFile}`);
    }
    process.env.NODE_EXTRA_CA_CERTS = config.caFile;
  }

  if (config.insecure) {
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
  }

  tlsConfigured = true;
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function shouldRetry(status: number) {
  return status === 429 || status === 502 || status === 503 || status === 504;
}

export async function apiCall<T = any>(
  serverUrl: string,
  endpoint: string,
  method: "GET" | "POST" | "PATCH" | "DELETE" = "GET",
  body?: any,
): Promise<T> {
  applyTlsConfig();

  const config = getConfig();
  const url = serverUrl || config.serverUrl;

  if (!url) {
    throw new Error(
      "Server URL not configured. Run 'och config set serverUrl <url>' or 'och auth login --url <url>'.",
    );
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };

  if (config.token) {
    headers["Authorization"] = `Bearer ${config.token}`;
  }

  const maxRetries = method === "GET" ? 2 : 0;
  let attempt = 0;

  while (true) {
    try {
      const response = await fetch(`${url}${endpoint}`, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
      });

      const contentType = response.headers.get("content-type") || "";
      const data = contentType.includes("application/json")
        ? await response.json()
        : await response.text();

      if (!response.ok) {
        if (attempt < maxRetries && shouldRetry(response.status)) {
          const retryAfter = response.headers.get("retry-after");
          const retryAfterMs =
            retryAfter && !Number.isNaN(Number(retryAfter))
              ? Number(retryAfter) * 1000
              : undefined;
          const delay = retryAfterMs ?? 300 * Math.pow(2, attempt);
          attempt += 1;
          await sleep(delay);
          continue;
        }

        const message =
          typeof data === "string"
            ? data
            : data?.error || `API error: ${response.status}`;
        throw new Error(message);
      }

      return data as T;
    } catch (error) {
      if (attempt < maxRetries) {
        attempt += 1;
        await sleep(300 * Math.pow(2, attempt - 1));
        continue;
      }

      throw error;
    }
  }
}

export async function getWithAuth<T = any>(endpoint: string): Promise<T> {
  const config = getConfig();
  return apiCall<T>(config.serverUrl, endpoint, "GET");
}

export async function postWithAuth<T = any>(
  endpoint: string,
  body: any,
): Promise<T> {
  const config = getConfig();
  return apiCall<T>(config.serverUrl, endpoint, "POST", body);
}

export async function patchWithAuth<T = any>(
  endpoint: string,
  body: any,
): Promise<T> {
  const config = getConfig();
  return apiCall<T>(config.serverUrl, endpoint, "PATCH", body);
}

export async function deleteWithAuth<T = any>(endpoint: string): Promise<T> {
  const config = getConfig();
  return apiCall<T>(config.serverUrl, endpoint, "DELETE");
}