@bunbase-ae/js 2.9.0 → 2.9.1-next.225.44112ee

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/http.ts +52 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunbase-ae/js",
3
- "version": "2.9.0",
3
+ "version": "2.9.1-next.225.44112ee",
4
4
  "type": "module",
5
5
  "description": "TypeScript/JavaScript SDK for BunBase",
6
6
  "license": "UNLICENSED",
package/src/http.ts CHANGED
@@ -112,6 +112,34 @@ export class HttpClient {
112
112
  keepalive?: boolean;
113
113
  } = {},
114
114
  ): Promise<T> {
115
+ const res = await this.performWithRefresh(method, path, options);
116
+ return this.parse<T>(res);
117
+ }
118
+
119
+ // Returns the raw Response without parsing — for file downloads / blobs.
120
+ // Participates in the same 401 → refresh → retry flow as request<T>, and
121
+ // throws BunBaseError (with .status) on non-ok responses.
122
+ async requestRaw(method: string, path: string): Promise<Response> {
123
+ const res = await this.performWithRefresh(method, path, {});
124
+ if (!res.ok) throw await this.toError(res);
125
+ return res;
126
+ }
127
+
128
+ // Sends the request and, if the response is 401 and we hold a refresh token,
129
+ // refreshes once and retries. Returns the raw Response either way — the
130
+ // caller decides how to read/parse the body.
131
+ private async performWithRefresh(
132
+ method: string,
133
+ path: string,
134
+ options: {
135
+ body?: unknown;
136
+ formData?: FormData;
137
+ query?: Record<string, string>;
138
+ skipAuth?: boolean;
139
+ signal?: AbortSignal;
140
+ keepalive?: boolean;
141
+ },
142
+ ): Promise<Response> {
115
143
  const res = await this.send(method, path, options);
116
144
 
117
145
  // Static credentials (adminSecret, apiKey) don't use refresh tokens — skip the 401 retry flow.
@@ -128,26 +156,9 @@ export class HttpClient {
128
156
  // Refresh failed — tokens are already cleared in doRefresh.
129
157
  throw new BunBaseError("Session expired. Please log in again.", 401, null);
130
158
  }
131
- const retried = await this.send(method, path, options);
132
- return this.parse<T>(retried);
159
+ return this.send(method, path, options);
133
160
  }
134
161
 
135
- return this.parse<T>(res);
136
- }
137
-
138
- // Returns the raw Response without parsing — for file downloads / blobs.
139
- async requestRaw(method: string, path: string): Promise<Response> {
140
- const res = await this.send(method, path, {});
141
- if (!res.ok) {
142
- let message = res.statusText;
143
- try {
144
- const data = (await res.clone().json()) as { error?: string };
145
- if (data.error) message = data.error;
146
- } catch {
147
- // ignore
148
- }
149
- throw new Error(message);
150
- }
151
162
  return res;
152
163
  }
153
164
 
@@ -206,18 +217,34 @@ export class HttpClient {
206
217
  const data = isJson ? await res.json() : await res.text();
207
218
 
208
219
  if (!res.ok) {
209
- const body =
210
- typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
211
- const message =
212
- "error" in body ? String(body.error) : `Request failed with status ${res.status}`;
213
- const code = "code" in body ? String(body.code) : undefined;
214
- const field = "field" in body ? String(body.field) : undefined;
215
- throw new BunBaseError(message, res.status, data, code, field);
220
+ throw this.buildError(res.status, data);
216
221
  }
217
222
 
218
223
  return data as T;
219
224
  }
220
225
 
226
+ // Builds a BunBaseError from a non-ok Response. Used by requestRaw, where
227
+ // the body is never parsed by the caller, so we read it here.
228
+ private async toError(res: Response): Promise<BunBaseError> {
229
+ const contentType = res.headers.get("content-type") ?? "";
230
+ const isJson = contentType.includes("application/json");
231
+ let data: unknown = null;
232
+ try {
233
+ data = isJson ? await res.clone().json() : await res.clone().text();
234
+ } catch {
235
+ // Body was empty or unreadable — fall back to status-based message.
236
+ }
237
+ return this.buildError(res.status, data);
238
+ }
239
+
240
+ private buildError(status: number, data: unknown): BunBaseError {
241
+ const body = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
242
+ const message = "error" in body ? String(body.error) : `Request failed with status ${status}`;
243
+ const code = "code" in body ? String(body.code) : undefined;
244
+ const field = "field" in body ? String(body.field) : undefined;
245
+ return new BunBaseError(message, status, data, code, field);
246
+ }
247
+
221
248
  // Deduplicated token refresh — multiple concurrent callers share one in-flight request.
222
249
  private doRefresh(): Promise<void> {
223
250
  if (this.refreshPromise) return this.refreshPromise;