@pear-protocol/core-sdk 1.8.0 → 1.10.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.
@@ -37,7 +37,11 @@ declare class Auth {
37
37
  scope: "read" | "read_write";
38
38
  authMethod: "api_key";
39
39
  }>;
40
- refreshSession(headers?: ClientHeaders): Promise<void>;
40
+ refreshSession(headers?: ClientHeaders): Promise<{
41
+ accessToken: string;
42
+ tokenType: "Bearer";
43
+ expiresIn: number;
44
+ }>;
41
45
  refreshToken(body: HttpTypes.RefreshTokenRequest, headers?: ClientHeaders): Promise<{
42
46
  accessToken: string;
43
47
  refreshToken: string;
@@ -2046,6 +2046,7 @@ declare class Core {
2046
2046
  status: "OPEN" | "CLOSED";
2047
2047
  origin: "PARTIAL" | "PEAR" | "EXTERNAL";
2048
2048
  exposure: Record<string, `-${string}` | `+${string}`>;
2049
+ unstableHistoricalData: boolean;
2049
2050
  totalFundingPayment: Record<string, `-${string}` | `+${string}`>;
2050
2051
  initialExposureUSD: Record<string, `-${string}` | `+${string}`>;
2051
2052
  entryPrices: Record<string, string>;
@@ -2270,6 +2271,7 @@ declare class Core {
2270
2271
  minimumDriftPct: string;
2271
2272
  createdAt: string;
2272
2273
  updatedAt: string;
2274
+ pausedReason?: "USER" | "POSITION_CHANGED" | "REPEATED_FAILURES" | undefined;
2273
2275
  timeframe?: "1d" | "1h" | "4h" | undefined;
2274
2276
  }[];
2275
2277
  nextCursor?: string | undefined;
@@ -2283,6 +2285,7 @@ declare class Core {
2283
2285
  minimumDriftPct: string;
2284
2286
  createdAt: string;
2285
2287
  updatedAt: string;
2288
+ pausedReason?: "USER" | "POSITION_CHANGED" | "REPEATED_FAILURES" | undefined;
2286
2289
  timeframe?: "1d" | "1h" | "4h" | undefined;
2287
2290
  };
2288
2291
  }>;
@@ -2295,6 +2298,20 @@ declare class Core {
2295
2298
  minimumDriftPct: string;
2296
2299
  createdAt: string;
2297
2300
  updatedAt: string;
2301
+ pausedReason?: "USER" | "POSITION_CHANGED" | "REPEATED_FAILURES" | undefined;
2302
+ timeframe?: "1d" | "1h" | "4h" | undefined;
2303
+ };
2304
+ }>;
2305
+ replace: (id: string, body: HttpTypes.ReplaceAutoRebalanceConfig, headers?: ClientHeaders) => Promise<{
2306
+ config: {
2307
+ id: string;
2308
+ positionId: string;
2309
+ status: "ACTIVE" | "PAUSED";
2310
+ weightSource: "INITIAL_WEIGHT" | "LIVE_BETA";
2311
+ minimumDriftPct: string;
2312
+ createdAt: string;
2313
+ updatedAt: string;
2314
+ pausedReason?: "USER" | "POSITION_CHANGED" | "REPEATED_FAILURES" | undefined;
2298
2315
  timeframe?: "1d" | "1h" | "4h" | undefined;
2299
2316
  };
2300
2317
  }>;
@@ -282,6 +282,13 @@ class Core {
282
282
  body
283
283
  });
284
284
  },
285
+ replace: async (id, body, headers) => {
286
+ return await this.client.fetch(`/rebalance/auto/${id}/replace`, {
287
+ method: "POST",
288
+ headers,
289
+ body
290
+ });
291
+ },
285
292
  cancel: async (id, headers) => {
286
293
  return await this.client.fetch(`/rebalance/auto/${id}/cancel`, {
287
294
  method: "POST",
package/dist/index.js CHANGED
@@ -9,28 +9,6 @@ import { HttpClient } from './http';
9
9
  import { FetchError } from './utils';
10
10
  export { FetchError } from './utils';
11
11
 
12
- const SESSION_REFRESH_PATH = "/auth/session/refresh";
13
- const SESSION_REFRESH_TIMEOUT_MS = 15e3;
14
- const isSessionRefreshRequest = (request) => {
15
- try {
16
- return new URL(request.url).pathname.includes(SESSION_REFRESH_PATH);
17
- } catch {
18
- return false;
19
- }
20
- };
21
- const withTimeout = async (promise, ms, label) => {
22
- let timer;
23
- try {
24
- return await Promise.race([
25
- promise,
26
- new Promise((_, reject) => {
27
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
28
- })
29
- ]);
30
- } finally {
31
- clearTimeout(timer);
32
- }
33
- };
34
12
  class PearSDK {
35
13
  admin;
36
14
  auth;
@@ -56,7 +34,14 @@ class PearSDK {
56
34
  const refreshSession = async () => {
57
35
  if (!refreshInFlight) {
58
36
  logger.info("[session-refresh] Refreshing session...");
59
- refreshInFlight = withTimeout(this.auth.refreshSession(), SESSION_REFRESH_TIMEOUT_MS, "Session refresh").then(() => logger.info("[session-refresh] Session refreshed successfully")).catch((err) => {
37
+ refreshInFlight = this.auth.refreshSession().then((session) => {
38
+ logger.info("[session-refresh] Session refreshed successfully");
39
+ try {
40
+ config.onSessionRefreshed?.(session);
41
+ } catch (err) {
42
+ logger.error("[session-refresh] onSessionRefreshed listener threw:", String(err));
43
+ }
44
+ }).catch((err) => {
60
45
  logger.error("[session-refresh] Refresh failed:", String(err));
61
46
  throw err;
62
47
  }).finally(() => {
@@ -67,25 +52,18 @@ class PearSDK {
67
52
  }
68
53
  return refreshInFlight;
69
54
  };
70
- const expire = (reason) => {
71
- logger.error(`[session-refresh] ${reason}`);
72
- config.onSessionExpired?.();
73
- throw new FetchError("Session expired. Please log in again.", "Unauthorized", 401);
74
- };
75
- const alreadyRetried = /* @__PURE__ */ new WeakSet();
76
55
  const sessionRefreshMiddleware = async (response, retry, request) => {
77
56
  if (response.status !== 401) return response;
78
- if (isSessionRefreshRequest(request)) return response;
79
57
  logger.warn(`[session-refresh] 401 received for ${request.method} ${request.url}`);
80
- if (alreadyRetried.has(request)) return expire("Still 401 after refresh, giving up");
81
58
  try {
82
59
  await refreshSession();
60
+ logger.info(`[session-refresh] Retrying ${request.method} ${request.url}`);
61
+ return retry();
83
62
  } catch {
84
- return expire("Session expired, cannot recover");
63
+ logger.error("[session-refresh] Session expired, cannot recover");
64
+ config.onSessionExpired?.();
65
+ throw new FetchError("Session expired. Please log in again.", "Unauthorized", 401);
85
66
  }
86
- alreadyRetried.add(request);
87
- logger.info(`[session-refresh] Retrying ${request.method} ${request.url}`);
88
- return retry();
89
67
  };
90
68
  client.useResponse(sessionRefreshMiddleware);
91
69
  }
package/dist/types.d.ts CHANGED
@@ -72,6 +72,16 @@ type SDKConfig = {
72
72
  * Typical use: clear UI state and redirect to login.
73
73
  */
74
74
  onSessionExpired?: () => void;
75
+ /**
76
+ * Called with the freshly minted token whenever the SDK rotates the session
77
+ * itself (session mode only). Session mode keeps the session in httpOnly
78
+ * cookies, so this is the only way a caller can observe the access token
79
+ * behind it — typical use is authenticating a bearer-only API alongside.
80
+ * Throwing from here is swallowed; it never fails the rotation.
81
+ *
82
+ * Example: (session) => localStorage.setItem('accessToken', session.accessToken)
83
+ */
84
+ onSessionRefreshed?: (session: HttpTypes.RefreshSessionResponse) => void;
75
85
  /**
76
86
  * Logger for debugging
77
87
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/core-sdk",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Pear Protocol Core SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -22,12 +22,10 @@
22
22
  "scripts": {
23
23
  "build": "tsup",
24
24
  "clean": "rm -rf dist .turbo",
25
- "test": "vitest run",
26
- "test:watch": "vitest",
27
25
  "typecheck": "tsc --noEmit"
28
26
  },
29
27
  "dependencies": {
30
- "@pear-protocol/types": "^1.11.0",
28
+ "@pear-protocol/types": "^1.13.0",
31
29
  "@pear-protocol/utils": "^0.2.0",
32
30
  "qs": "^6.14.0"
33
31
  },
@@ -35,7 +33,6 @@
35
33
  "@backend/typescript-config": "workspace:*",
36
34
  "@types/qs": "^6.14.0",
37
35
  "tsup": "8.5.1",
38
- "typescript": "5.9.3",
39
- "vitest": "4.1.2"
36
+ "typescript": "5.9.3"
40
37
  }
41
38
  }
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,88 +0,0 @@
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
- });