@pear-protocol/core-sdk 1.3.0 → 1.5.0

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.
@@ -362,6 +362,7 @@ declare class Core {
362
362
  side: "BUY" | "SELL";
363
363
  reduceOnly: boolean;
364
364
  isExternal: boolean;
365
+ synthetic: boolean;
365
366
  quantity: string;
366
367
  price: string;
367
368
  usd: string;
@@ -389,6 +390,7 @@ declare class Core {
389
390
  side: "BUY" | "SELL";
390
391
  reduceOnly: boolean;
391
392
  isExternal: boolean;
393
+ synthetic: boolean;
392
394
  quantity: string;
393
395
  price: string;
394
396
  usd: string;
@@ -532,6 +534,7 @@ declare class Core {
532
534
  side: "BUY" | "SELL";
533
535
  reduceOnly: boolean;
534
536
  isExternal: boolean;
537
+ synthetic: boolean;
535
538
  quantity: string;
536
539
  price: string;
537
540
  usd: string;
@@ -664,6 +667,7 @@ declare class Core {
664
667
  side: "BUY" | "SELL";
665
668
  reduceOnly: boolean;
666
669
  isExternal: boolean;
670
+ synthetic: boolean;
667
671
  quantity: string;
668
672
  price: string;
669
673
  usd: string;
@@ -829,6 +833,7 @@ declare class Core {
829
833
  side: "BUY" | "SELL";
830
834
  reduceOnly: boolean;
831
835
  isExternal: boolean;
836
+ synthetic: boolean;
832
837
  quantity: string;
833
838
  price: string;
834
839
  usd: string;
@@ -963,6 +968,7 @@ declare class Core {
963
968
  side: "BUY" | "SELL";
964
969
  reduceOnly: boolean;
965
970
  isExternal: boolean;
971
+ synthetic: boolean;
966
972
  quantity: string;
967
973
  price: string;
968
974
  usd: string;
@@ -1095,6 +1101,7 @@ declare class Core {
1095
1101
  side: "BUY" | "SELL";
1096
1102
  reduceOnly: boolean;
1097
1103
  isExternal: boolean;
1104
+ synthetic: boolean;
1098
1105
  quantity: string;
1099
1106
  price: string;
1100
1107
  usd: string;
@@ -1260,6 +1267,7 @@ declare class Core {
1260
1267
  side: "BUY" | "SELL";
1261
1268
  reduceOnly: boolean;
1262
1269
  isExternal: boolean;
1270
+ synthetic: boolean;
1263
1271
  quantity: string;
1264
1272
  price: string;
1265
1273
  usd: string;
@@ -1394,6 +1402,7 @@ declare class Core {
1394
1402
  side: "BUY" | "SELL";
1395
1403
  reduceOnly: boolean;
1396
1404
  isExternal: boolean;
1405
+ synthetic: boolean;
1397
1406
  quantity: string;
1398
1407
  price: string;
1399
1408
  usd: string;
@@ -1526,6 +1535,7 @@ declare class Core {
1526
1535
  side: "BUY" | "SELL";
1527
1536
  reduceOnly: boolean;
1528
1537
  isExternal: boolean;
1538
+ synthetic: boolean;
1529
1539
  quantity: string;
1530
1540
  price: string;
1531
1541
  usd: string;
@@ -1691,6 +1701,7 @@ declare class Core {
1691
1701
  side: "BUY" | "SELL";
1692
1702
  reduceOnly: boolean;
1693
1703
  isExternal: boolean;
1704
+ synthetic: boolean;
1694
1705
  quantity: string;
1695
1706
  price: string;
1696
1707
  usd: string;
@@ -1862,7 +1873,9 @@ declare class Core {
1862
1873
  tradeAccountId: string;
1863
1874
  connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
1864
1875
  status: "OPEN" | "CLOSED";
1876
+ origin: "PARTIAL" | "PEAR" | "EXTERNAL";
1865
1877
  exposure: Record<string, `-${string}` | `+${string}`>;
1878
+ totalFundingPayment: Record<string, `-${string}` | `+${string}`>;
1866
1879
  initialExposureUSD: Record<string, `-${string}` | `+${string}`>;
1867
1880
  createdAt: string;
1868
1881
  updatedAt: string;
@@ -1874,8 +1887,9 @@ declare class Core {
1874
1887
  positions: {
1875
1888
  id: string;
1876
1889
  createdAt: string;
1877
- exposure: Record<string, string>;
1890
+ exposure: Record<string, `-${string}` | `+${string}`>;
1878
1891
  status: "OPEN";
1892
+ origin: "PARTIAL" | "PEAR" | "EXTERNAL";
1879
1893
  connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
1880
1894
  }[];
1881
1895
  }>;
@@ -2058,14 +2072,131 @@ declare class Core {
2058
2072
  }>;
2059
2073
  };
2060
2074
  rebalance: {
2061
- manually: (positionId: string, body: HttpTypes.RebalanceManually, headers?: ClientHeaders) => Promise<{
2075
+ manual: (positionId: string, body: HttpTypes.RebalanceManual, headers?: ClientHeaders) => Promise<{
2062
2076
  plan: {
2063
2077
  outcome: "full_rebalance";
2064
2078
  } | {
2065
- outcome: "partial_rebalance" | "no_rebalance";
2079
+ outcome: "partial_rebalance";
2080
+ messages: [string, ...string[]];
2081
+ } | {
2082
+ outcome: "no_rebalance";
2083
+ messages: [string, ...string[]];
2084
+ } | {
2085
+ outcome: "failed";
2066
2086
  messages: [string, ...string[]];
2067
2087
  };
2068
2088
  }>;
2089
+ auto: {
2090
+ list: (query?: HttpTypes.AutoRebalanceListParamsInput, headers?: ClientHeaders) => Promise<{
2091
+ limit: number;
2092
+ nextCursor: string | undefined;
2093
+ items: {
2094
+ id: string;
2095
+ positionId: string;
2096
+ status: "ACTIVE" | "PAUSED";
2097
+ weightSource: "INITIAL_WEIGHT" | "LIVE_BETA";
2098
+ minimumDriftPct: string;
2099
+ createdAt: string;
2100
+ updatedAt: string;
2101
+ timeframe?: "1d" | "1h" | "4h" | undefined;
2102
+ }[];
2103
+ }>;
2104
+ create: (body: HttpTypes.CreateAutoRebalanceConfig, headers?: ClientHeaders) => Promise<{
2105
+ config: {
2106
+ id: string;
2107
+ positionId: string;
2108
+ status: "ACTIVE" | "PAUSED";
2109
+ weightSource: "INITIAL_WEIGHT" | "LIVE_BETA";
2110
+ minimumDriftPct: string;
2111
+ createdAt: string;
2112
+ updatedAt: string;
2113
+ timeframe?: "1d" | "1h" | "4h" | undefined;
2114
+ };
2115
+ }>;
2116
+ update: (id: string, body: HttpTypes.UpdateAutoRebalanceConfig, headers?: ClientHeaders) => Promise<{
2117
+ config: {
2118
+ id: string;
2119
+ positionId: string;
2120
+ status: "ACTIVE" | "PAUSED";
2121
+ weightSource: "INITIAL_WEIGHT" | "LIVE_BETA";
2122
+ minimumDriftPct: string;
2123
+ createdAt: string;
2124
+ updatedAt: string;
2125
+ timeframe?: "1d" | "1h" | "4h" | undefined;
2126
+ };
2127
+ }>;
2128
+ cancel: (id: string, headers?: ClientHeaders) => Promise<void>;
2129
+ };
2130
+ listEvents: (positionId: string, query?: HttpTypes.RebalanceEventListParamsInput, headers?: ClientHeaders) => Promise<{
2131
+ limit: number;
2132
+ nextCursor: string | undefined;
2133
+ items: {
2134
+ id: string;
2135
+ autoRebalanceConfigId: string;
2136
+ positionId: string;
2137
+ weightSource: "INITIAL_WEIGHT" | "LIVE_BETA";
2138
+ driftPct: string;
2139
+ currentWeights: Record<string, number>;
2140
+ targetWeights: Record<string, number>;
2141
+ status: "FAILED" | "SUCCESS";
2142
+ createdAt: string;
2143
+ error?: string | undefined;
2144
+ }[];
2145
+ }>;
2146
+ };
2147
+ savedBaskets: {
2148
+ list: (headers?: ClientHeaders) => Promise<{
2149
+ baskets: {
2150
+ id: string;
2151
+ name: string;
2152
+ legs: [{
2153
+ symbol: string;
2154
+ side: "BUY" | "SELL";
2155
+ weight: number;
2156
+ }, ...{
2157
+ symbol: string;
2158
+ side: "BUY" | "SELL";
2159
+ weight: number;
2160
+ }[]];
2161
+ createdAt: string;
2162
+ updatedAt: string;
2163
+ }[];
2164
+ }>;
2165
+ create: (body: HttpTypes.CreateSavedBasketPayload, headers?: ClientHeaders) => Promise<{
2166
+ basket: {
2167
+ id: string;
2168
+ name: string;
2169
+ legs: [{
2170
+ symbol: string;
2171
+ side: "BUY" | "SELL";
2172
+ weight: number;
2173
+ }, ...{
2174
+ symbol: string;
2175
+ side: "BUY" | "SELL";
2176
+ weight: number;
2177
+ }[]];
2178
+ createdAt: string;
2179
+ updatedAt: string;
2180
+ };
2181
+ }>;
2182
+ update: (id: string, body: HttpTypes.UpdateSavedBasketPayload, headers?: ClientHeaders) => Promise<{
2183
+ basket: {
2184
+ id: string;
2185
+ name: string;
2186
+ legs: [{
2187
+ symbol: string;
2188
+ side: "BUY" | "SELL";
2189
+ weight: number;
2190
+ }, ...{
2191
+ symbol: string;
2192
+ side: "BUY" | "SELL";
2193
+ weight: number;
2194
+ }[]];
2195
+ createdAt: string;
2196
+ updatedAt: string;
2197
+ };
2198
+ }>;
2199
+ delete: (id: string, headers?: ClientHeaders) => Promise<void>;
2069
2200
  };
2070
2201
  schedules: {
2071
2202
  list: (query: HttpTypes.ScheduleListParamsInput, headers?: ClientHeaders) => Promise<{
@@ -2105,6 +2236,7 @@ declare class Core {
2105
2236
  side: "BUY" | "SELL";
2106
2237
  reduceOnly: boolean;
2107
2238
  isExternal: boolean;
2239
+ synthetic: boolean;
2108
2240
  quantity: string;
2109
2241
  price: string;
2110
2242
  usd: string;
@@ -2150,6 +2282,7 @@ declare class Core {
2150
2282
  side: "BUY" | "SELL";
2151
2283
  reduceOnly: boolean;
2152
2284
  isExternal: boolean;
2285
+ synthetic: boolean;
2153
2286
  quantity: string;
2154
2287
  price: string;
2155
2288
  usd: string;
@@ -2200,6 +2333,7 @@ declare class Core {
2200
2333
  side: "BUY" | "SELL";
2201
2334
  reduceOnly: boolean;
2202
2335
  isExternal: boolean;
2336
+ synthetic: boolean;
2203
2337
  quantity: string;
2204
2338
  price: string;
2205
2339
  usd: string;
@@ -2245,6 +2379,7 @@ declare class Core {
2245
2379
  side: "BUY" | "SELL";
2246
2380
  reduceOnly: boolean;
2247
2381
  isExternal: boolean;
2382
+ synthetic: boolean;
2248
2383
  quantity: string;
2249
2384
  price: string;
2250
2385
  usd: string;
@@ -2268,15 +2403,24 @@ declare class Core {
2268
2403
  }>;
2269
2404
  };
2270
2405
  sync: {
2271
- since: (body: HttpTypes.SyncSinceRequest, headers?: ClientHeaders) => Promise<{
2272
- since: string;
2273
- sinceMs: number;
2274
- safeTs?: number | undefined;
2275
- confirmedExchangeFillIds?: string[] | undefined;
2276
- }>;
2277
- confirm: (body: HttpTypes.ConfirmFillsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2278
- reconcile: (body: HttpTypes.ReconcileFillsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2279
- create: (body: HttpTypes.CreateFillsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2406
+ fills: {
2407
+ since: (body: HttpTypes.SyncFillsSinceRequest, headers?: ClientHeaders) => Promise<{
2408
+ sinceMs: number;
2409
+ neverSynced: boolean;
2410
+ confirmedExchangeFillIds?: string[] | undefined;
2411
+ }>;
2412
+ reconcile: (body: HttpTypes.ReconcileFillsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2413
+ confirm: (body: HttpTypes.ConfirmFillsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2414
+ create: (body: HttpTypes.CreateApplicablesRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2415
+ createBulk: (body: HttpTypes.CreateApplicablesRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2416
+ };
2417
+ funding: {
2418
+ since: (body: HttpTypes.SyncFundingSinceRequest, headers?: ClientHeaders) => Promise<{
2419
+ sinceMs: number;
2420
+ }>;
2421
+ create: (body: HttpTypes.CreateFundingPaymentsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2422
+ createBulk: (body: HttpTypes.CreateFundingPaymentsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
2423
+ };
2280
2424
  };
2281
2425
  trade: {
2282
2426
  open: (body: HttpTypes.CreateTradeOpen, headers?: ClientHeaders) => Promise<{
@@ -3106,6 +3250,7 @@ declare class Core {
3106
3250
  side: "BUY" | "SELL";
3107
3251
  reduceOnly: boolean;
3108
3252
  isExternal: boolean;
3253
+ synthetic: boolean;
3109
3254
  quantity: string;
3110
3255
  price: string;
3111
3256
  usd: string;
@@ -3238,6 +3383,7 @@ declare class Core {
3238
3383
  side: "BUY" | "SELL";
3239
3384
  reduceOnly: boolean;
3240
3385
  isExternal: boolean;
3386
+ synthetic: boolean;
3241
3387
  quantity: string;
3242
3388
  price: string;
3243
3389
  usd: string;
@@ -3403,6 +3549,7 @@ declare class Core {
3403
3549
  side: "BUY" | "SELL";
3404
3550
  reduceOnly: boolean;
3405
3551
  isExternal: boolean;
3552
+ synthetic: boolean;
3406
3553
  quantity: string;
3407
3554
  price: string;
3408
3555
  usd: string;
@@ -3523,6 +3670,7 @@ declare class Core {
3523
3670
  side: "BUY" | "SELL";
3524
3671
  reduceOnly: boolean;
3525
3672
  isExternal: boolean;
3673
+ synthetic: boolean;
3526
3674
  quantity: string;
3527
3675
  price: string;
3528
3676
  usd: string;
@@ -3655,6 +3803,7 @@ declare class Core {
3655
3803
  side: "BUY" | "SELL";
3656
3804
  reduceOnly: boolean;
3657
3805
  isExternal: boolean;
3806
+ synthetic: boolean;
3658
3807
  quantity: string;
3659
3808
  price: string;
3660
3809
  usd: string;
@@ -3820,6 +3969,7 @@ declare class Core {
3820
3969
  side: "BUY" | "SELL";
3821
3970
  reduceOnly: boolean;
3822
3971
  isExternal: boolean;
3972
+ synthetic: boolean;
3823
3973
  quantity: string;
3824
3974
  price: string;
3825
3975
  usd: string;
@@ -3940,6 +4090,7 @@ declare class Core {
3940
4090
  side: "BUY" | "SELL";
3941
4091
  reduceOnly: boolean;
3942
4092
  isExternal: boolean;
4093
+ synthetic: boolean;
3943
4094
  quantity: string;
3944
4095
  price: string;
3945
4096
  usd: string;
@@ -4072,6 +4223,7 @@ declare class Core {
4072
4223
  side: "BUY" | "SELL";
4073
4224
  reduceOnly: boolean;
4074
4225
  isExternal: boolean;
4226
+ synthetic: boolean;
4075
4227
  quantity: string;
4076
4228
  price: string;
4077
4229
  usd: string;
@@ -4237,6 +4389,7 @@ declare class Core {
4237
4389
  side: "BUY" | "SELL";
4238
4390
  reduceOnly: boolean;
4239
4391
  isExternal: boolean;
4392
+ synthetic: boolean;
4240
4393
  quantity: string;
4241
4394
  price: string;
4242
4395
  usd: string;
@@ -1,3 +1,5 @@
1
+ import { gzipJson } from '../utils';
2
+
1
3
  class Core {
2
4
  client;
3
5
  clientId;
@@ -245,12 +247,76 @@ class Core {
245
247
  }
246
248
  };
247
249
  rebalance = {
248
- manually: async (positionId, body, headers) => {
249
- return await this.client.fetch(`/rebalance/manually/${positionId}`, {
250
+ manual: async (positionId, body, headers) => {
251
+ return await this.client.fetch(`/rebalance/manual/${positionId}`, {
250
252
  method: "POST",
251
253
  headers,
252
254
  body: this.withClientId(body)
253
255
  });
256
+ },
257
+ auto: {
258
+ list: async (query, headers) => {
259
+ return await this.client.fetch(`/rebalance/auto`, {
260
+ method: "GET",
261
+ headers,
262
+ query
263
+ });
264
+ },
265
+ create: async (body, headers) => {
266
+ return await this.client.fetch(`/rebalance/auto`, {
267
+ method: "POST",
268
+ headers,
269
+ body
270
+ });
271
+ },
272
+ update: async (id, body, headers) => {
273
+ return await this.client.fetch(`/rebalance/auto/${id}`, {
274
+ method: "PATCH",
275
+ headers,
276
+ body
277
+ });
278
+ },
279
+ cancel: async (id, headers) => {
280
+ return await this.client.fetch(`/rebalance/auto/${id}/cancel`, {
281
+ method: "POST",
282
+ headers
283
+ });
284
+ }
285
+ },
286
+ listEvents: async (positionId, query, headers) => {
287
+ return await this.client.fetch(`/rebalance/${positionId}/events`, {
288
+ method: "GET",
289
+ headers,
290
+ query
291
+ });
292
+ }
293
+ };
294
+ savedBaskets = {
295
+ list: async (headers) => {
296
+ return await this.client.fetch(`/saved-baskets`, {
297
+ method: "GET",
298
+ headers
299
+ });
300
+ },
301
+ create: async (body, headers) => {
302
+ return await this.client.fetch(`/saved-baskets`, {
303
+ method: "POST",
304
+ headers,
305
+ body
306
+ });
307
+ },
308
+ update: async (id, body, headers) => {
309
+ return await this.client.fetch(`/saved-baskets/${id}`, {
310
+ method: "PATCH",
311
+ headers,
312
+ body
313
+ });
314
+ },
315
+ delete: async (id, headers) => {
316
+ return await this.client.fetch(`/saved-baskets/${id}`, {
317
+ method: "DELETE",
318
+ headers
319
+ });
254
320
  }
255
321
  };
256
322
  schedules = {
@@ -282,33 +348,71 @@ class Core {
282
348
  }
283
349
  };
284
350
  sync = {
285
- since: async (body, headers) => {
286
- return await this.client.fetch(`/sync/since`, {
287
- method: "POST",
288
- headers,
289
- body
290
- });
291
- },
292
- confirm: async (body, headers) => {
293
- return await this.client.fetch(`/sync/confirm`, {
294
- method: "POST",
295
- headers,
296
- body
297
- });
298
- },
299
- reconcile: async (body, headers) => {
300
- return await this.client.fetch(`/sync/reconcile`, {
301
- method: "POST",
302
- headers,
303
- body
304
- });
351
+ fills: {
352
+ since: async (body, headers) => {
353
+ return await this.client.fetch(`/sync/fills/since`, {
354
+ method: "POST",
355
+ headers,
356
+ body
357
+ });
358
+ },
359
+ reconcile: async (body, headers) => {
360
+ return await this.client.fetch(`/sync/fills/reconcile`, {
361
+ method: "POST",
362
+ headers,
363
+ body
364
+ });
365
+ },
366
+ confirm: async (body, headers) => {
367
+ return await this.client.fetch(`/sync/fills/confirm`, {
368
+ method: "POST",
369
+ headers,
370
+ body
371
+ });
372
+ },
373
+ create: async (body, headers) => {
374
+ return await this.client.fetch(`/sync/fills/create`, {
375
+ method: "POST",
376
+ headers,
377
+ body
378
+ });
379
+ },
380
+ createBulk: async (body, headers) => {
381
+ const file = await gzipJson(body);
382
+ const formData = new FormData();
383
+ formData.append("file", file, "payload.json.gz");
384
+ return await this.client.fetch(`/sync/fills/create/bulk`, {
385
+ method: "POST",
386
+ headers: { ...headers, "content-type": null },
387
+ body: formData
388
+ });
389
+ }
305
390
  },
306
- create: async (body, headers) => {
307
- return await this.client.fetch(`/sync/create`, {
308
- method: "POST",
309
- headers,
310
- body
311
- });
391
+ funding: {
392
+ since: async (body, headers) => {
393
+ return await this.client.fetch(`/sync/funding/since`, {
394
+ method: "POST",
395
+ headers,
396
+ body
397
+ });
398
+ },
399
+ create: async (body, headers) => {
400
+ return await this.client.fetch(`/sync/funding/create`, {
401
+ method: "POST",
402
+ headers,
403
+ body
404
+ });
405
+ },
406
+ createBulk: async (body, headers) => {
407
+ const file = await gzipJson(body);
408
+ const formData = new FormData();
409
+ formData.append("file", file, "payload.json.gz");
410
+ return await this.client.fetch(`/sync/funding/create/bulk`, {
411
+ method: "POST",
412
+ headers: { ...headers, "content-type": null },
413
+ body: formData
414
+ });
415
+ }
312
416
  }
313
417
  };
314
418
  trade = {
package/dist/index.js CHANGED
@@ -7,6 +7,28 @@ import { HttpClient } from './http';
7
7
  import { FetchError } from './utils';
8
8
  export * from '@pear-protocol/utils';
9
9
 
10
+ const SESSION_REFRESH_PATH = "/auth/session/refresh";
11
+ const SESSION_REFRESH_TIMEOUT_MS = 15e3;
12
+ const isSessionRefreshRequest = (request) => {
13
+ try {
14
+ return new URL(request.url).pathname.includes(SESSION_REFRESH_PATH);
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+ const withTimeout = async (promise, ms, label) => {
20
+ let timer;
21
+ try {
22
+ return await Promise.race([
23
+ promise,
24
+ new Promise((_, reject) => {
25
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
26
+ })
27
+ ]);
28
+ } finally {
29
+ clearTimeout(timer);
30
+ }
31
+ };
10
32
  class PearSDK {
11
33
  admin;
12
34
  auth;
@@ -31,7 +53,7 @@ class PearSDK {
31
53
  const refreshSession = async () => {
32
54
  if (!refreshInFlight) {
33
55
  logger.info("[session-refresh] Refreshing session...");
34
- refreshInFlight = this.auth.refreshSession().then(() => logger.info("[session-refresh] Session refreshed successfully")).catch((err) => {
56
+ refreshInFlight = withTimeout(this.auth.refreshSession(), SESSION_REFRESH_TIMEOUT_MS, "Session refresh").then(() => logger.info("[session-refresh] Session refreshed successfully")).catch((err) => {
35
57
  logger.error("[session-refresh] Refresh failed:", String(err));
36
58
  throw err;
37
59
  }).finally(() => {
@@ -42,18 +64,25 @@ class PearSDK {
42
64
  }
43
65
  return refreshInFlight;
44
66
  };
67
+ const expire = (reason) => {
68
+ logger.error(`[session-refresh] ${reason}`);
69
+ config.onSessionExpired?.();
70
+ throw new FetchError("Session expired. Please log in again.", "Unauthorized", 401);
71
+ };
72
+ const alreadyRetried = /* @__PURE__ */ new WeakSet();
45
73
  const sessionRefreshMiddleware = async (response, retry, request) => {
46
74
  if (response.status !== 401) return response;
75
+ if (isSessionRefreshRequest(request)) return response;
47
76
  logger.warn(`[session-refresh] 401 received for ${request.method} ${request.url}`);
77
+ if (alreadyRetried.has(request)) return expire("Still 401 after refresh, giving up");
48
78
  try {
49
79
  await refreshSession();
50
- logger.info(`[session-refresh] Retrying ${request.method} ${request.url}`);
51
- return retry();
52
80
  } catch {
53
- logger.error("[session-refresh] Session expired, cannot recover");
54
- config.onSessionExpired?.();
55
- throw new FetchError("Session expired. Please log in again.", "Unauthorized", 401);
81
+ return expire("Session expired, cannot recover");
56
82
  }
83
+ alreadyRetried.add(request);
84
+ logger.info(`[session-refresh] Retrying ${request.method} ${request.url}`);
85
+ return retry();
57
86
  };
58
87
  client.useResponse(sessionRefreshMiddleware);
59
88
  }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,88 @@
1
+ import { describe, afterEach, vi, it, expect } from 'vitest';
2
+ import PearSDK from './index';
3
+
4
+ const json = (status, body = {}) => {
5
+ return new Response(JSON.stringify(body), {
6
+ status,
7
+ headers: { "content-type": "application/json" }
8
+ });
9
+ };
10
+ const silentLogger = {
11
+ error: () => {
12
+ },
13
+ warn: () => {
14
+ },
15
+ info: () => {
16
+ },
17
+ debug: () => {
18
+ }
19
+ };
20
+ const setup = (routes) => {
21
+ const calls = [];
22
+ const fetchMock = vi.fn(async (input) => {
23
+ const key = `${input.method} ${new URL(input.url).pathname}`;
24
+ calls.push(key);
25
+ const queue = routes[key];
26
+ if (!queue || queue.length === 0) {
27
+ throw new Error(`unexpected request: ${key}`);
28
+ }
29
+ const responder = queue.length > 1 ? queue.shift() : queue[0];
30
+ return responder();
31
+ });
32
+ vi.stubGlobal("fetch", fetchMock);
33
+ const onSessionExpired = vi.fn();
34
+ const sdk = new PearSDK({
35
+ url: "https://gateway.test",
36
+ auth: { type: "session" },
37
+ onSessionExpired,
38
+ logger: silentLogger
39
+ });
40
+ return { sdk, onSessionExpired, calls };
41
+ };
42
+ describe("session-refresh middleware", () => {
43
+ afterEach(() => {
44
+ vi.unstubAllGlobals();
45
+ vi.useRealTimers();
46
+ vi.restoreAllMocks();
47
+ });
48
+ it("rejects (never hangs) when the refresh endpoint itself 401s on a dead cookie", async () => {
49
+ const { sdk, onSessionExpired, calls } = setup({
50
+ "GET /positions": [() => json(401, { message: "unauthorized" })],
51
+ "POST /auth/session/refresh": [() => json(401, { message: "refresh failed" })]
52
+ });
53
+ await expect(sdk.core.positions.list({})).rejects.toMatchObject({ status: 401 });
54
+ expect(onSessionExpired).toHaveBeenCalledTimes(1);
55
+ expect(calls).toEqual(["GET /positions", "POST /auth/session/refresh"]);
56
+ });
57
+ it("gives up after a single retry when the request still 401s after a successful refresh", async () => {
58
+ const { sdk, onSessionExpired, calls } = setup({
59
+ "GET /positions": [() => json(401)],
60
+ "POST /auth/session/refresh": [() => json(200)]
61
+ });
62
+ await expect(sdk.core.positions.list({})).rejects.toMatchObject({ status: 401 });
63
+ expect(calls.filter((call) => call === "GET /positions")).toHaveLength(2);
64
+ expect(calls.filter((call) => call === "POST /auth/session/refresh")).toHaveLength(1);
65
+ expect(onSessionExpired).toHaveBeenCalledTimes(1);
66
+ });
67
+ it("transparently refreshes and retries the original request on a recoverable 401", async () => {
68
+ const { sdk, onSessionExpired, calls } = setup({
69
+ "GET /positions": [() => json(401), () => json(200, { positions: ["ok"] })],
70
+ "POST /auth/session/refresh": [() => json(200)]
71
+ });
72
+ await expect(sdk.core.positions.list({})).resolves.toMatchObject({ positions: ["ok"] });
73
+ expect(onSessionExpired).not.toHaveBeenCalled();
74
+ expect(calls).toEqual(["GET /positions", "POST /auth/session/refresh", "GET /positions"]);
75
+ });
76
+ it("caps a stalled refresh at the timeout instead of wedging the queue", async () => {
77
+ vi.useFakeTimers();
78
+ const { sdk, onSessionExpired } = setup({
79
+ "GET /positions": [() => json(401)],
80
+ "POST /auth/session/refresh": [() => new Promise(() => {
81
+ })]
82
+ });
83
+ const settled = expect(sdk.core.positions.list({})).rejects.toMatchObject({ status: 401 });
84
+ await vi.advanceTimersByTimeAsync(15e3);
85
+ await settled;
86
+ expect(onSessionExpired).toHaveBeenCalledTimes(1);
87
+ });
88
+ });
package/dist/types.d.ts CHANGED
@@ -111,6 +111,6 @@ type FetchArgs = Omit<RequestInit, 'headers' | 'body'> & {
111
111
  };
112
112
  type RequestMiddleware = (request: Request, next: () => Promise<Response>) => Promise<Response>;
113
113
  type ResponseMiddleware = (response: Response, retry: () => Promise<Response>, request: Request) => Promise<Response>;
114
- type ClientIdBody = HttpTypes.RebalanceManually | HttpTypes.CreateSchedule | HttpTypes.CreateTradeOpen | HttpTypes.CreateTradeAdjust | HttpTypes.CreateTradeClose | HttpTypes.CreateTradeCloseAll | HttpTypes.CreateTriggerOpen | HttpTypes.CreateTriggerClose;
114
+ type ClientIdBody = HttpTypes.RebalanceManual | HttpTypes.CreateSchedule | HttpTypes.CreateTradeOpen | HttpTypes.CreateTradeAdjust | HttpTypes.CreateTradeClose | HttpTypes.CreateTradeCloseAll | HttpTypes.CreateTriggerOpen | HttpTypes.CreateTriggerClose;
115
115
 
116
116
  export type { AuthMode, ClientHeaders, ClientIdBody, FetchArgs, FetchInput, FetchParams, Logger, RequestMiddleware, ResponseMiddleware, SDKConfig };
package/dist/utils.d.ts CHANGED
@@ -9,5 +9,6 @@ declare class FetchError extends Error {
9
9
  statusText: string | undefined;
10
10
  constructor(message: string, statusText?: string, status?: number);
11
11
  }
12
+ declare function gzipJson(value: unknown): Promise<Blob>;
12
13
 
13
- export { FetchError, getBaseUrl, normalizeRequest, normalizeResponseData };
14
+ export { FetchError, getBaseUrl, gzipJson, normalizeRequest, normalizeResponseData };
package/dist/utils.js CHANGED
@@ -44,5 +44,9 @@ class FetchError extends Error {
44
44
  this.status = status;
45
45
  }
46
46
  }
47
+ async function gzipJson(value) {
48
+ const stream = new Blob([JSON.stringify(value)]).stream().pipeThrough(new CompressionStream("gzip"));
49
+ return await new Response(stream).blob();
50
+ }
47
51
 
48
- export { FetchError, getBaseUrl, normalizeRequest, normalizeResponseData };
52
+ export { FetchError, getBaseUrl, gzipJson, normalizeRequest, normalizeResponseData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/core-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Pear Protocol Core SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -22,17 +22,20 @@
22
22
  "scripts": {
23
23
  "build": "tsup",
24
24
  "clean": "rm -rf dist .turbo",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
25
27
  "typecheck": "tsc --noEmit"
26
28
  },
27
29
  "dependencies": {
28
- "@pear-protocol/types": "^1.4.0",
29
- "@pear-protocol/utils": "^0.0.23",
30
+ "@pear-protocol/types": "^1.6.0",
31
+ "@pear-protocol/utils": "^0.1.0",
30
32
  "qs": "^6.14.0"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@backend/typescript-config": "workspace:*",
34
36
  "@types/qs": "^6.14.0",
35
37
  "tsup": "8.5.1",
36
- "typescript": "5.9.3"
38
+ "typescript": "5.9.3",
39
+ "vitest": "4.1.2"
37
40
  }
38
41
  }