Skip to content
File

Blob: ui/client/api.ts

typescript118 lines
1export interface TunnelSyncResult {
2 hostname: string;
3 target: string;
4 published: boolean;
5 publishedEndpoints: number;
6 error?: string;
7}
8 
9export interface SyncResult {
10 applied: boolean;
11 saved: boolean;
12 error?: string;
13 attemptedAt?: string;
14 tunnels: TunnelSyncResult[];
15}
16 
17export interface ClientStatus {
18 apex: string;
19 connectedNodes: {
20 id?: number;
21 address?: string;
22 unknown?: boolean;
23 rendezvous?: boolean;
24 }[];
25 synchronization: SyncResult;
26 pending: boolean;
27 retryAt?: string;
28}
29 
30export interface RegisteredTunnel {
31 hostname: string;
32 target: string;
33 headerHost: string;
34}
35 
36export interface DomainRecord {
37 message: string;
38 record: string;
39 type: string;
40 content: string;
41}
42 
43export class APIError extends Error {
44 constructor(message: string, readonly outcome?: Partial<SyncResult>) {
45 super(message);
46 this.name = "APIError";
47 }
48}
49 
50export function errorMessage(error: unknown): string {
51 return error instanceof Error ? error.message : "Request failed.";
52}
53 
54export function createAPI() {
55 const requests = new Set<AbortController>();
56 
57 async function request<T = void>(path: string, options: RequestInit = {}): Promise<T> {
58 const controller = new AbortController();
59 requests.add(controller);
60 const timer = setTimeout(() => controller.abort(), 60_000);
61 try {
62 const response = await fetch(path, { ...options, signal: controller.signal });
63 const body = await response.text();
64 let data: unknown = null;
65 try {
66 data = body ? JSON.parse(body) : null;
67 } catch {
68 // Some API successes are plain text.
69 }
70 if (!response.ok) {
71 const outcome = data && typeof data === "object" && !Array.isArray(data)
72 ? data as Partial<SyncResult>
73 : undefined;
74 throw new APIError(
75 outcome?.error || body.trim() || `Request failed (${response.status}).`,
76 outcome,
77 );
78 }
79 return data as T;
80 } catch (error) {
81 if (error instanceof Error && error.name === "AbortError") {
82 throw new Error(
83 options.method === "POST"
84 ? "Timed out; result unknown. Refresh before retrying."
85 : "Timed out. Check the client, then refresh.",
86 );
87 }
88 throw error;
89 } finally {
90 clearTimeout(timer);
91 requests.delete(controller);
92 }
93 }
94 
95 function abort() {
96 for (const controller of requests) controller.abort();
97 }
98 
99 return { request, abort };
100}
101 
102export interface DomainToken {
103 id: string;
104 hostname: string;
105 expiresAt?: string;
106 incomplete: boolean;
107}
108 
109export interface MintedToken {
110 grant: DomainToken;
111 token: string;
112}
113 
114export interface RevokeOutcome {
115 revoked: boolean;
116 indexError?: string;
117}