@astra-code/astra-ai 0.1.6 → 0.1.7

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.
@@ -7,7 +7,7 @@ import {
7
7
  } from "./config.js";
8
8
  import type {AgentEvent, AuthSession, ChatMessage} from "../types/events.js";
9
9
  import type {WorkspaceFile} from "./workspaceScanner.js";
10
- import {loadSession} from "./sessionStore.js";
10
+ import {loadSession, saveSession} from "./sessionStore.js";
11
11
 
12
12
  type JsonRecord = Record<string, unknown>;
13
13
 
@@ -22,14 +22,55 @@ export type SessionSummary = {
22
22
  export class BackendClient {
23
23
  private readonly baseUrl: string;
24
24
  private authToken: string | null;
25
+ private refreshToken: string | null;
26
+ private onTokenRefreshed: ((session: AuthSession) => void) | null = null;
25
27
 
26
28
  public constructor(baseUrl = getBackendUrl()) {
27
29
  this.baseUrl = baseUrl;
28
- this.authToken = loadSession()?.access_token?.trim() || null;
30
+ const stored = loadSession();
31
+ this.authToken = stored?.access_token?.trim() || null;
32
+ this.refreshToken = stored?.refresh_token?.trim() || null;
29
33
  }
30
34
 
31
35
  public setAuthSession(session: AuthSession | null): void {
32
36
  this.authToken = session?.access_token?.trim() || null;
37
+ this.refreshToken = session?.refresh_token?.trim() || null;
38
+ }
39
+
40
+ public setOnTokenRefreshed(cb: (session: AuthSession) => void): void {
41
+ this.onTokenRefreshed = cb;
42
+ }
43
+
44
+ private async tryRefreshToken(): Promise<boolean> {
45
+ if (!this.refreshToken) return false;
46
+ try {
47
+ const res = await fetch(`${this.baseUrl}/api/auth/refresh`, {
48
+ method: "POST",
49
+ headers: {"content-type": "application/json"},
50
+ body: JSON.stringify({refresh_token: this.refreshToken})
51
+ });
52
+ if (!res.ok) return false;
53
+ const data = (await res.json()) as Record<string, unknown>;
54
+ const newAccess = typeof data.access_token === "string" ? data.access_token.trim() : null;
55
+ if (!newAccess) return false;
56
+ const newRefresh = typeof data.refresh_token === "string" ? data.refresh_token.trim() : this.refreshToken;
57
+ this.authToken = newAccess;
58
+ this.refreshToken = newRefresh;
59
+ // Merge with stored session and persist
60
+ const stored = loadSession();
61
+ if (stored) {
62
+ const refreshed: AuthSession = {
63
+ ...stored,
64
+ access_token: newAccess,
65
+ refresh_token: newRefresh ?? undefined
66
+ };
67
+ saveSession(refreshed);
68
+ this.onTokenRefreshed?.(refreshed);
69
+ }
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
33
74
  }
34
75
 
35
76
  public async get(path: string, params?: Record<string, string>): Promise<JsonRecord> {
@@ -209,6 +250,7 @@ export class BackendClient {
209
250
  workspaceTree?: string[];
210
251
  workspaceFiles?: WorkspaceFile[];
211
252
  model?: string;
253
+ signal?: AbortSignal;
212
254
  }): AsyncGenerator<AgentEvent, void, void> {
213
255
  const model = payload.model ?? getDefaultModel();
214
256
  const body = {
@@ -230,20 +272,33 @@ export class BackendClient {
230
272
  user_id: payload.user.user_id,
231
273
  org_id: payload.user.org_id
232
274
  };
233
- return this.streamSse("/api/agent/chat/stream", body);
275
+ return this.streamSse("/api/agent/chat/stream", body, payload.signal);
234
276
  }
235
277
 
236
278
  private async request(url: string, method: string, payload?: JsonRecord): Promise<JsonRecord> {
237
- const headers: Record<string, string> = {"content-type": "application/json"};
238
- if (this.authToken) {
239
- headers.authorization = `Bearer ${this.authToken}`;
240
- }
241
- const response = await fetch(url, {
279
+ const makeHeaders = (): Record<string, string> => {
280
+ const h: Record<string, string> = {"content-type": "application/json"};
281
+ if (this.authToken) h.authorization = `Bearer ${this.authToken}`;
282
+ return h;
283
+ };
284
+
285
+ let response = await fetch(url, {
242
286
  method,
243
- headers,
287
+ headers: makeHeaders(),
244
288
  body: payload ? JSON.stringify(payload) : null
245
289
  });
246
290
 
291
+ if (response.status === 401) {
292
+ const refreshed = await this.tryRefreshToken();
293
+ if (refreshed) {
294
+ response = await fetch(url, {
295
+ method,
296
+ headers: makeHeaders(),
297
+ body: payload ? JSON.stringify(payload) : null
298
+ });
299
+ }
300
+ }
301
+
247
302
  if (!response.ok) {
248
303
  const detail = (await response.text()).trim();
249
304
  throw new Error(`Backend error ${response.status}: ${detail || response.statusText}`);
@@ -256,17 +311,33 @@ export class BackendClient {
256
311
  return JSON.parse(text) as JsonRecord;
257
312
  }
258
313
 
259
- private async *streamSse(path: string, payload: JsonRecord): AsyncGenerator<AgentEvent> {
260
- const headers: Record<string, string> = {"content-type": "application/json"};
261
- if (this.authToken) {
262
- headers.authorization = `Bearer ${this.authToken}`;
263
- }
264
- const response = await fetch(`${this.baseUrl}${path}`, {
314
+ private async *streamSse(path: string, payload: JsonRecord, signal?: AbortSignal): AsyncGenerator<AgentEvent> {
315
+ const makeHeaders = (): Record<string, string> => {
316
+ const h: Record<string, string> = {"content-type": "application/json"};
317
+ if (this.authToken) h.authorization = `Bearer ${this.authToken}`;
318
+ return h;
319
+ };
320
+ const sig = signal ?? null;
321
+
322
+ let response = await fetch(`${this.baseUrl}${path}`, {
265
323
  method: "POST",
266
- headers,
267
- body: JSON.stringify(payload)
324
+ headers: makeHeaders(),
325
+ body: JSON.stringify(payload),
326
+ signal: sig
268
327
  });
269
328
 
329
+ if (response.status === 401) {
330
+ const refreshed = await this.tryRefreshToken();
331
+ if (refreshed) {
332
+ response = await fetch(`${this.baseUrl}${path}`, {
333
+ method: "POST",
334
+ headers: makeHeaders(),
335
+ body: JSON.stringify(payload),
336
+ signal: sig
337
+ });
338
+ }
339
+ }
340
+
270
341
  if (!response.ok) {
271
342
  const detail = (await response.text()).trim();
272
343
  throw new Error(`Backend error ${response.status}: ${detail || response.statusText}`);
@@ -5,6 +5,7 @@ export type AuthSession = {
5
5
  role?: string;
6
6
  display_name?: string;
7
7
  access_token?: string;
8
+ refresh_token?: string;
8
9
  [key: string]: unknown;
9
10
  };
10
11