@cubee_ee/sdk 0.2.3 → 0.2.5

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.
@@ -98,6 +98,91 @@ export declare const CUBIC_POOL_IDL: {
98
98
  name: string;
99
99
  type: string;
100
100
  })[];
101
+ } | {
102
+ name: string;
103
+ docs: string[];
104
+ discriminator: number[];
105
+ accounts: ({
106
+ name: string;
107
+ docs: string[];
108
+ writable: boolean;
109
+ signer?: undefined;
110
+ } | {
111
+ name: string;
112
+ docs: string[];
113
+ writable?: undefined;
114
+ signer?: undefined;
115
+ } | {
116
+ name: string;
117
+ docs: string[];
118
+ signer: boolean;
119
+ writable?: undefined;
120
+ } | {
121
+ name: string;
122
+ docs: string[];
123
+ writable: boolean;
124
+ signer: boolean;
125
+ } | {
126
+ name: string;
127
+ docs?: undefined;
128
+ writable?: undefined;
129
+ signer?: undefined;
130
+ })[];
131
+ args: {
132
+ name: string;
133
+ type: string;
134
+ }[];
135
+ } | {
136
+ name: string;
137
+ docs: string[];
138
+ discriminator: number[];
139
+ accounts: ({
140
+ name: string;
141
+ writable: boolean;
142
+ docs?: undefined;
143
+ signer?: undefined;
144
+ } | {
145
+ name: string;
146
+ docs: string[];
147
+ writable?: undefined;
148
+ signer?: undefined;
149
+ } | {
150
+ name: string;
151
+ docs: string[];
152
+ writable: boolean;
153
+ signer: boolean;
154
+ } | {
155
+ name: string;
156
+ writable?: undefined;
157
+ docs?: undefined;
158
+ signer?: undefined;
159
+ })[];
160
+ args: never[];
161
+ } | {
162
+ name: string;
163
+ docs: string[];
164
+ discriminator: number[];
165
+ accounts: ({
166
+ name: string;
167
+ docs: string[];
168
+ writable: boolean;
169
+ signer?: undefined;
170
+ } | {
171
+ name: string;
172
+ signer: boolean;
173
+ docs?: undefined;
174
+ writable?: undefined;
175
+ })[];
176
+ args: {
177
+ name: string;
178
+ type: {
179
+ vec: {
180
+ defined: {
181
+ name: string;
182
+ };
183
+ };
184
+ };
185
+ }[];
101
186
  } | {
102
187
  name: string;
103
188
  docs: string[];
@@ -184,6 +269,37 @@ export declare const CUBIC_POOL_IDL: {
184
269
  };
185
270
  })[];
186
271
  };
272
+ } | {
273
+ name: string;
274
+ type: {
275
+ kind: string;
276
+ fields: ({
277
+ name: string;
278
+ docs: string[];
279
+ type: string;
280
+ } | {
281
+ name: string;
282
+ type: string;
283
+ docs?: undefined;
284
+ } | {
285
+ name: string;
286
+ docs: string[];
287
+ type: {
288
+ array: (number | {
289
+ defined: {
290
+ name: string;
291
+ };
292
+ })[];
293
+ };
294
+ } | {
295
+ name: string;
296
+ docs: string[];
297
+ type: {
298
+ array: (string | number)[];
299
+ };
300
+ })[];
301
+ };
302
+ docs?: undefined;
187
303
  } | {
188
304
  name: string;
189
305
  docs: string[];
@@ -205,7 +321,11 @@ export declare const CUBIC_POOL_IDL: {
205
321
  kind: string;
206
322
  fields: {
207
323
  name: string;
208
- type: string;
324
+ type: {
325
+ defined: {
326
+ name: string;
327
+ };
328
+ };
209
329
  }[];
210
330
  };
211
331
  docs?: undefined;
@@ -652,6 +772,91 @@ export declare const IDLS: {
652
772
  name: string;
653
773
  type: string;
654
774
  })[];
775
+ } | {
776
+ name: string;
777
+ docs: string[];
778
+ discriminator: number[];
779
+ accounts: ({
780
+ name: string;
781
+ docs: string[];
782
+ writable: boolean;
783
+ signer?: undefined;
784
+ } | {
785
+ name: string;
786
+ docs: string[];
787
+ writable?: undefined;
788
+ signer?: undefined;
789
+ } | {
790
+ name: string;
791
+ docs: string[];
792
+ signer: boolean;
793
+ writable?: undefined;
794
+ } | {
795
+ name: string;
796
+ docs: string[];
797
+ writable: boolean;
798
+ signer: boolean;
799
+ } | {
800
+ name: string;
801
+ docs?: undefined;
802
+ writable?: undefined;
803
+ signer?: undefined;
804
+ })[];
805
+ args: {
806
+ name: string;
807
+ type: string;
808
+ }[];
809
+ } | {
810
+ name: string;
811
+ docs: string[];
812
+ discriminator: number[];
813
+ accounts: ({
814
+ name: string;
815
+ writable: boolean;
816
+ docs?: undefined;
817
+ signer?: undefined;
818
+ } | {
819
+ name: string;
820
+ docs: string[];
821
+ writable?: undefined;
822
+ signer?: undefined;
823
+ } | {
824
+ name: string;
825
+ docs: string[];
826
+ writable: boolean;
827
+ signer: boolean;
828
+ } | {
829
+ name: string;
830
+ writable?: undefined;
831
+ docs?: undefined;
832
+ signer?: undefined;
833
+ })[];
834
+ args: never[];
835
+ } | {
836
+ name: string;
837
+ docs: string[];
838
+ discriminator: number[];
839
+ accounts: ({
840
+ name: string;
841
+ docs: string[];
842
+ writable: boolean;
843
+ signer?: undefined;
844
+ } | {
845
+ name: string;
846
+ signer: boolean;
847
+ docs?: undefined;
848
+ writable?: undefined;
849
+ })[];
850
+ args: {
851
+ name: string;
852
+ type: {
853
+ vec: {
854
+ defined: {
855
+ name: string;
856
+ };
857
+ };
858
+ };
859
+ }[];
655
860
  } | {
656
861
  name: string;
657
862
  docs: string[];
@@ -738,6 +943,37 @@ export declare const IDLS: {
738
943
  };
739
944
  })[];
740
945
  };
946
+ } | {
947
+ name: string;
948
+ type: {
949
+ kind: string;
950
+ fields: ({
951
+ name: string;
952
+ docs: string[];
953
+ type: string;
954
+ } | {
955
+ name: string;
956
+ type: string;
957
+ docs?: undefined;
958
+ } | {
959
+ name: string;
960
+ docs: string[];
961
+ type: {
962
+ array: (number | {
963
+ defined: {
964
+ name: string;
965
+ };
966
+ })[];
967
+ };
968
+ } | {
969
+ name: string;
970
+ docs: string[];
971
+ type: {
972
+ array: (string | number)[];
973
+ };
974
+ })[];
975
+ };
976
+ docs?: undefined;
741
977
  } | {
742
978
  name: string;
743
979
  docs: string[];
@@ -759,7 +995,11 @@ export declare const IDLS: {
759
995
  kind: string;
760
996
  fields: {
761
997
  name: string;
762
- type: string;
998
+ type: {
999
+ defined: {
1000
+ name: string;
1001
+ };
1002
+ };
763
1003
  }[];
764
1004
  };
765
1005
  docs?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/idl/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAe,CAAC;AAC3C,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAmB,CAAC;AACnD,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA0B,CAAC;AAElE,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIP,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/idl/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAe,CAAC;AAC3C,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAmB,CAAC;AACnD,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA0B,CAAC;AAElE,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIP,CAAC"}
@@ -17,7 +17,7 @@ export interface SdkError {
17
17
  /** Original error for debugging; never render this to end users. */
18
18
  cause?: unknown;
19
19
  }
20
- export type SdkErrorCode = "rpc_unavailable" | "rpc_timeout" | "rpc_rate_limited" | "account_not_found" | "invalid_input" | "math_overflow" | "parse_failure" | "backend_unavailable" | "backend_invalid_response" | "insufficient_funds" | "pool_disabled" | "swaps_disabled" | "unsupported_pool_state" | "slippage_exceeded" | "simulation_failed" | "tx_build_failed" | "alt_fetch_failed" | "unknown";
20
+ export type SdkErrorCode = "rpc_unavailable" | "rpc_timeout" | "rpc_rate_limited" | "account_not_found" | "invalid_input" | "math_overflow" | "parse_failure" | "backend_unavailable" | "backend_invalid_response" | "insufficient_funds" | "pool_disabled" | "swaps_disabled" | "unsupported_pool_state" | "slippage_exceeded" | "simulation_failed" | "tx_build_failed" | "alt_fetch_failed" | "auth_failed" | "unknown";
21
21
  export declare const ok: <T>(data: T) => SdkResult<T>;
22
22
  export declare const err: (code: SdkErrorCode, humanMessage: string, cause?: unknown) => SdkResult<never>;
23
23
  //# sourceMappingURL=result.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/types/result.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACrB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,QAAQ;IACvB,oCAAoC;IACpC,IAAI,EAAE,YAAY,CAAC;IACnB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,aAAa,GACb,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,eAAe,GACf,qBAAqB,GACrB,0BAA0B,GAC1B,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,wBAAwB,GACxB,mBAAmB,GACnB,mBAAmB,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,SAAS,CAAC;AAEd,eAAO,MAAM,EAAE,GAAI,CAAC,EAAE,MAAM,CAAC,KAAG,SAAS,CAAC,CAAC,CAAyB,CAAC;AACrE,eAAO,MAAM,GAAG,GACd,MAAM,YAAY,EAClB,cAAc,MAAM,EACpB,QAAQ,OAAO,KACd,SAAS,CAAC,KAAK,CAGhB,CAAC"}
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/types/result.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACrB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,QAAQ;IACvB,oCAAoC;IACpC,IAAI,EAAE,YAAY,CAAC;IACnB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,aAAa,GACb,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,eAAe,GACf,eAAe,GACf,qBAAqB,GACrB,0BAA0B,GAC1B,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,wBAAwB,GACxB,mBAAmB,GACnB,mBAAmB,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,aAAa,GACb,SAAS,CAAC;AAEd,eAAO,MAAM,EAAE,GAAI,CAAC,EAAE,MAAM,CAAC,KAAG,SAAS,CAAC,CAAC,CAAyB,CAAC;AACrE,eAAO,MAAM,GAAG,GACd,MAAM,YAAY,EAClB,cAAc,MAAM,EACpB,QAAQ,OAAO,KACd,SAAS,CAAC,KAAK,CAGhB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/types/result.ts"],"names":[],"mappings":";;;AAqCO,MAAM,EAAE,GAAG,CAAI,IAAO,EAAgB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAAxD,QAAA,EAAE,MAAsD;AAC9D,MAAM,GAAG,GAAG,CACjB,IAAkB,EAClB,YAAoB,EACpB,KAAe,EACG,EAAE,CAAC,CAAC;IACtB,EAAE,EAAE,KAAK;IACT,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE;CACrC,CAAC,CAAC;AAPU,QAAA,GAAG,OAOb"}
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/types/result.ts"],"names":[],"mappings":";;;AAsCO,MAAM,EAAE,GAAG,CAAI,IAAO,EAAgB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAAxD,QAAA,EAAE,MAAsD;AAC9D,MAAM,GAAG,GAAG,CACjB,IAAkB,EAClB,YAAoB,EACpB,KAAe,EACG,EAAE,CAAC,CAAC;IACtB,EAAE,EAAE,KAAK;IACT,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE;CACrC,CAAC,CAAC;AAPU,QAAA,GAAG,OAOb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubee_ee/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -6,6 +6,16 @@ export interface CubeBackendClientParams {
6
6
  apiEndpoint: string;
7
7
  apiKey?: string;
8
8
  defaultHeaders?: Record<string, string>;
9
+ /**
10
+ * Called when tokens are refreshed automatically after a 401.
11
+ * The frontend should persist the new tokens (e.g. to localStorage).
12
+ */
13
+ onTokenRefreshed?: (tokens: AuthTokens) => void;
14
+ /**
15
+ * Called when both access and refresh tokens are expired/invalid.
16
+ * The frontend should trigger a full re-authentication (SIWS sign-in).
17
+ */
18
+ onAuthExpired?: () => void;
9
19
  }
10
20
 
11
21
  export type StatsKind =
@@ -131,14 +141,36 @@ export interface LeaderboardEpochResponse {
131
141
  epochs: EpochHistoryEntry[];
132
142
  }
133
143
 
144
+ // ── Auth types ──
145
+
146
+ export interface NonceResponse {
147
+ nonce: string;
148
+ message: string;
149
+ }
150
+
151
+ export interface AuthTokens {
152
+ accessToken: string;
153
+ refreshToken: string;
154
+ wallet: string;
155
+ expiresIn: string;
156
+ }
157
+
134
158
  /**
135
159
  * REST wrapper around the Cube backend. Every method is a SdkResult; no
136
160
  * exceptions escape. If a request fails, the result carries a
137
161
  * human-readable error plus the original cause.
162
+ *
163
+ * Auto-refresh: when a request gets 401, the client automatically tries
164
+ * to refresh tokens via POST /api/auth/refresh. If successful, the
165
+ * original request is retried once with the new access token.
138
166
  */
139
167
  export class CubeBackendClient {
140
168
  private readonly endpoint: string;
141
169
  private readonly headers: Record<string, string>;
170
+ private refreshToken: string | null = null;
171
+ private refreshInFlight: Promise<boolean> | null = null;
172
+ private readonly onTokenRefreshed?: (tokens: AuthTokens) => void;
173
+ private readonly onAuthExpired?: () => void;
142
174
 
143
175
  constructor(params: CubeBackendClientParams) {
144
176
  this.endpoint = params.apiEndpoint.replace(/\/$/, "");
@@ -147,6 +179,8 @@ export class CubeBackendClient {
147
179
  ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
148
180
  ...(params.defaultHeaders ?? {}),
149
181
  };
182
+ this.onTokenRefreshed = params.onTokenRefreshed;
183
+ this.onAuthExpired = params.onAuthExpired;
150
184
  }
151
185
 
152
186
  listPools(): Promise<SdkResult<PoolSummary[]>> {
@@ -280,16 +314,90 @@ export class CubeBackendClient {
280
314
  return this.get<StatsSeries>(`/api/stats/${kind}?${qs.toString()}`);
281
315
  }
282
316
 
317
+ // ── Auth ──
318
+
319
+ /** Request a SIWS nonce + pre-built message for the given wallet. */
320
+ getNonce(wallet: string): Promise<SdkResult<NonceResponse>> {
321
+ return this.get<NonceResponse>(
322
+ `/api/auth/nonce?wallet=${encodeURIComponent(wallet)}`,
323
+ );
324
+ }
325
+
326
+ /** Submit signed SIWS message to receive access + refresh tokens. */
327
+ verifySignature(
328
+ message: string,
329
+ signature: string,
330
+ ): Promise<SdkResult<AuthTokens>> {
331
+ return this.post<AuthTokens>("/api/auth/verify", {
332
+ message,
333
+ signature,
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Set both tokens. Call this after verifySignature() and on app init
339
+ * (restoring tokens from storage).
340
+ */
341
+ setTokens(accessToken: string, refreshToken: string): void {
342
+ this.headers["Authorization"] = `Bearer ${accessToken}`;
343
+ this.refreshToken = refreshToken;
344
+ }
345
+
346
+ /** Clear both tokens (logout). */
347
+ clearTokens(): void {
348
+ delete this.headers["Authorization"];
349
+ this.refreshToken = null;
350
+ }
351
+
352
+ /** @deprecated Use setTokens() instead. */
353
+ setAccessToken(token: string): void {
354
+ this.headers["Authorization"] = `Bearer ${token}`;
355
+ }
356
+
357
+ /** @deprecated Use clearTokens() instead. */
358
+ clearAccessToken(): void {
359
+ delete this.headers["Authorization"];
360
+ this.refreshToken = null;
361
+ }
362
+
283
363
  /** Generic GET with retry. Callers that need it for other endpoints. */
284
364
  get<T>(path: string): Promise<SdkResult<T>> {
285
- return this.request<T>("GET", path);
365
+ return this.requestWithRefresh<T>("GET", path);
286
366
  }
287
367
 
288
368
  post<T>(path: string, body: unknown): Promise<SdkResult<T>> {
289
- return this.request<T>("POST", path, body);
369
+ return this.requestWithRefresh<T>("POST", path, body);
370
+ }
371
+
372
+ // ── Private: HTTP layer with auto-refresh ──
373
+
374
+ /**
375
+ * Core request method with auto-refresh on 401.
376
+ * If a request gets 401 and we have a refresh token:
377
+ * 1. Call POST /api/auth/refresh (deduplicated if concurrent)
378
+ * 2. On success: update tokens, notify via callback, retry original request
379
+ * 3. On failure: notify via onAuthExpired callback, return original error
380
+ */
381
+ private async requestWithRefresh<T>(
382
+ method: "GET" | "POST",
383
+ path: string,
384
+ body?: unknown,
385
+ ): Promise<SdkResult<T>> {
386
+ const result = await this.rawRequest<T>(method, path, body);
387
+
388
+ // Don't auto-refresh for auth endpoints themselves
389
+ const isAuthPath = path.startsWith("/api/auth/");
390
+ if (!isAuthPath && !result.ok && this.is401(result) && this.refreshToken) {
391
+ const refreshed = await this.tryRefresh();
392
+ if (refreshed) {
393
+ return this.rawRequest<T>(method, path, body);
394
+ }
395
+ }
396
+
397
+ return result;
290
398
  }
291
399
 
292
- private async request<T>(
400
+ private async rawRequest<T>(
293
401
  method: "GET" | "POST",
294
402
  path: string,
295
403
  body?: unknown
@@ -303,7 +411,11 @@ export class CubeBackendClient {
303
411
  const raw = await safeCall(async () => {
304
412
  const res = await fetch(url, fetchOpts);
305
413
  if (!res.ok) {
306
- throw new Error(`${method} ${path} → HTTP ${res.status} ${res.statusText}`);
414
+ const error = new Error(
415
+ `${method} ${path} → HTTP ${res.status} ${res.statusText}`,
416
+ );
417
+ (error as any).status = res.status;
418
+ throw error;
307
419
  }
308
420
  return (await res.json()) as T;
309
421
  });
@@ -311,6 +423,46 @@ export class CubeBackendClient {
311
423
  return ok(raw.data);
312
424
  }
313
425
 
426
+ /**
427
+ * Attempt to refresh tokens. Returns true if successful.
428
+ * Deduplicates concurrent refresh attempts.
429
+ */
430
+ private async tryRefresh(): Promise<boolean> {
431
+ // Deduplicate: if a refresh is already in flight, wait for it
432
+ if (this.refreshInFlight) {
433
+ return this.refreshInFlight;
434
+ }
435
+
436
+ this.refreshInFlight = this.doRefresh();
437
+ try {
438
+ return await this.refreshInFlight;
439
+ } finally {
440
+ this.refreshInFlight = null;
441
+ }
442
+ }
443
+
444
+ private async doRefresh(): Promise<boolean> {
445
+ const res = await this.rawRequest<AuthTokens>("POST", "/api/auth/refresh", {
446
+ refreshToken: this.refreshToken,
447
+ });
448
+
449
+ if (res.ok) {
450
+ this.setTokens(res.data.accessToken, res.data.refreshToken);
451
+ this.onTokenRefreshed?.(res.data);
452
+ return true;
453
+ }
454
+
455
+ // Refresh failed — both tokens are dead
456
+ this.clearTokens();
457
+ this.onAuthExpired?.();
458
+ return false;
459
+ }
460
+
461
+ private is401(result: SdkResult<unknown>): boolean {
462
+ if (result.ok) return false;
463
+ return result.error.humanMessage.includes("HTTP 401");
464
+ }
465
+
314
466
  /**
315
467
  * Fetch a response envelope of the form `{ data: T, ... }` and unwrap
316
468
  * the `.data` field. The existing Cube backend wraps most endpoints