File size: 2,620 Bytes
302854a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a42bc3b
302854a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a42bc3b
302854a
 
 
 
 
 
 
 
 
 
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
// @homepilot/api-client — base HTTP client for the HomePilot backend and
// OllaBridge Cloud.
//
// Platform-pure: the auth token and the fetch implementation are *injected*
// (ports), so the same client runs unchanged on web (browser fetch +
// localStorage), desktop (Electron), and mobile (RN fetch + SecureStore). This
// is the ports-&-adapters boundary that keeps the shared packages free of any
// platform API.

export type FetchImpl = typeof fetch;

export interface TokenProvider {
  getToken(): string | null | Promise<string | null>;
}

export interface ClientOptions {
  baseUrl: string;
  tokenProvider?: TokenProvider;
  fetchImpl?: FetchImpl;
  /**
   * Maps a token to the auth header(s) to send. Defaults to
   * `Authorization: Bearer <token>`. HomePilot's backend uses
   * `{ "X-API-Key": token }`; OllaBridge Cloud uses the Bearer default.
   */
  authHeader?: (token: string) => Record<string, string>;
}

export interface ApiClient {
  readonly baseUrl: string;
  get<T>(path: string): Promise<T>;
  post<T>(path: string, body?: unknown): Promise<T>;
  put<T>(path: string, body?: unknown): Promise<T>;
  del<T>(path: string): Promise<T>;
}

export class ApiError extends Error {
  constructor(public readonly status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

export function createClient(opts: ClientOptions): ApiClient {
  const doFetch: FetchImpl = opts.fetchImpl ?? fetch;
  const baseUrl = opts.baseUrl.replace(/\/+$/, "");

  async function headers(): Promise<Record<string, string>> {
    const h: Record<string, string> = { "Content-Type": "application/json" };
    const token = await opts.tokenProvider?.getToken();
    if (token) {
      const auth = opts.authHeader
        ? opts.authHeader(token)
        : { Authorization: `Bearer ${token}` };
      Object.assign(h, auth);
    }
    return h;
  }

  async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
    const res = await doFetch(`${baseUrl}${path}`, {
      method,
      headers: await headers(),
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (!res.ok) throw new ApiError(res.status, await safeText(res));
    return (await res.json()) as T;
  }

  return {
    baseUrl,
    get: (path) => request("GET", path),
    post: (path, body) => request("POST", path, body),
    put: (path, body) => request("PUT", path, body),
    del: (path) => request("DELETE", path),
  };
}

async function safeText(res: Response): Promise<string> {
  try {
    return await res.text();
  } catch {
    return res.statusText;
  }
}