@pear-protocol/core-sdk 1.9.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.
- package/dist/auth/index.d.ts +5 -1
- package/dist/index.js +13 -35
- package/dist/types.d.ts +10 -0
- package/package.json +3 -6
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.js +0 -88
package/dist/auth/index.d.ts
CHANGED
|
@@ -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<
|
|
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;
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/index.test.d.ts
DELETED
package/dist/index.test.js
DELETED
|
@@ -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
|
-
});
|