Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

client.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

client.tsBlame166 lines · 1 contributor
5c83cccClaude1import * as SecureStore from 'expo-secure-store';
2import { useSettingsStore } from '../store/settingsStore';
3
4export const PAT_STORE_KEY = 'gluecron_pat';
5
6/** Retrieves the saved PAT from SecureStore. Returns null if none saved. */
7export async function getSavedToken(): Promise<string | null> {
8 return SecureStore.getItemAsync(PAT_STORE_KEY);
9}
10
11/** Saves a PAT to SecureStore. */
12export async function saveToken(token: string): Promise<void> {
13 await SecureStore.setItemAsync(PAT_STORE_KEY, token);
14}
15
16/** Removes the saved PAT from SecureStore. */
17export async function removeToken(): Promise<void> {
18 await SecureStore.deleteItemAsync(PAT_STORE_KEY);
19}
20
21/** Build the full URL for a given API path. */
22function buildUrl(path: string): string {
23 const host = useSettingsStore.getState().host;
24 const normalizedPath = path.startsWith('/') ? path : `/${path}`;
25 return `${host}${normalizedPath}`;
26}
27
28/** Build common headers, injecting the Bearer token if one is saved. */
29async function buildHeaders(extra?: Record<string, string>): Promise<Record<string, string>> {
30 const token = await getSavedToken();
31 const headers: Record<string, string> = {
32 'Content-Type': 'application/json',
33 Accept: 'application/json',
34 ...extra,
35 };
36 if (token) {
37 headers['Authorization'] = `Bearer ${token}`;
38 }
39 return headers;
40}
41
42export class ApiError extends Error {
43 constructor(
44 public readonly status: number,
45 message: string,
46 public readonly body?: unknown,
47 ) {
48 super(message);
49 this.name = 'ApiError';
50 }
51}
52
53/** Performs a GET (or custom method) request and parses the JSON response. */
54export async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
55 const headers = await buildHeaders(init?.headers as Record<string, string> | undefined);
56 const response = await fetch(buildUrl(path), {
57 ...init,
58 headers,
59 });
60
61 if (!response.ok) {
62 let body: unknown;
63 try {
64 body = await response.json();
65 } catch {
66 body = await response.text();
67 }
68 const message =
69 typeof body === 'object' && body !== null && 'error' in body
70 ? String((body as Record<string, unknown>).error)
71 : `HTTP ${response.status}`;
72 throw new ApiError(response.status, message, body);
73 }
74
75 return response.json() as Promise<T>;
76}
77
78/** Performs a POST request with a JSON body. */
79export async function postJSON<T>(path: string, body: unknown): Promise<T> {
80 const headers = await buildHeaders();
81 const response = await fetch(buildUrl(path), {
82 method: 'POST',
83 headers,
84 body: JSON.stringify(body),
85 });
86
87 if (!response.ok) {
88 let errorBody: unknown;
89 try {
90 errorBody = await response.json();
91 } catch {
92 errorBody = await response.text();
93 }
94 const message =
95 typeof errorBody === 'object' &&
96 errorBody !== null &&
97 'error' in errorBody
98 ? String((errorBody as Record<string, unknown>).error)
99 : `HTTP ${response.status}`;
100 throw new ApiError(response.status, message, errorBody);
101 }
102
103 return response.json() as Promise<T>;
104}
105
106/** Performs a PATCH request with a JSON body. */
107export async function patchJSON<T>(path: string, body: unknown): Promise<T> {
108 const headers = await buildHeaders();
109 const response = await fetch(buildUrl(path), {
110 method: 'PATCH',
111 headers,
112 body: JSON.stringify(body),
113 });
114
115 if (!response.ok) {
116 let errorBody: unknown;
117 try {
118 errorBody = await response.json();
119 } catch {
120 errorBody = await response.text();
121 }
122 const message =
123 typeof errorBody === 'object' &&
124 errorBody !== null &&
125 'error' in errorBody
126 ? String((errorBody as Record<string, unknown>).error)
127 : `HTTP ${response.status}`;
128 throw new ApiError(response.status, message, errorBody);
129 }
130
131 return response.json() as Promise<T>;
132}
133
134/** Performs a DELETE request. */
135export async function deleteRequest(path: string): Promise<void> {
136 const headers = await buildHeaders();
137 const response = await fetch(buildUrl(path), {
138 method: 'DELETE',
139 headers,
140 });
141
142 if (!response.ok) {
143 throw new ApiError(response.status, `HTTP ${response.status}`);
144 }
145}
146
147/** Executes a GraphQL query against /api/graphql. */
148export async function graphql<T>(
149 query: string,
150 variables?: Record<string, unknown>,
151): Promise<T> {
152 const result = await postJSON<{ data?: T; errors?: Array<{ message: string }> }>(
153 '/api/graphql',
154 { query, variables },
155 );
156
157 if (result.errors && result.errors.length > 0) {
158 throw new Error(result.errors.map((e) => e.message).join('; '));
159 }
160
161 if (result.data === undefined) {
162 throw new Error('GraphQL response missing data field');
163 }
164
165 return result.data;
166}