@wheeparam/library 0.0.4 → 0.0.6

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.
@@ -1,166 +1,305 @@
1
- // src/client/axios.ts
2
- import axios from "axios";
3
- function createLocalStorageTokenAccess(keys) {
4
- const accessTokenKey = keys?.accessTokenKey ?? "accessToken";
5
- const refreshTokenKey = keys?.refreshTokenKey ?? "refreshToken";
1
+ // src/client/createAxiosClient.ts
2
+ import axios, {
3
+ AxiosHeaders
4
+ } from "axios";
5
+
6
+ // src/client/error.ts
7
+ var defaultShouldRefresh = (error) => {
8
+ return error.response?.status === 401;
9
+ };
10
+ function defaultParseRefreshResponse(data) {
11
+ const source = typeof data === "object" && data !== null ? data : {};
12
+ const accessToken = typeof source.result?.accessToken === "string" && source.result.accessToken || typeof source.accessToken === "string" && source.accessToken || typeof source.result?.token === "string" && source.result.token || typeof source.token === "string" && source.token || "";
13
+ const refreshToken = typeof source.result?.refreshToken === "string" && source.result.refreshToken || typeof source.refreshToken === "string" && source.refreshToken || void 0;
14
+ if (!accessToken) {
15
+ throw new Error("Refresh response does not contain accessToken");
16
+ }
6
17
  return {
7
- get() {
8
- return {
9
- accessToken: window.localStorage.getItem(accessTokenKey),
10
- refreshToken: window.localStorage.getItem(refreshTokenKey)
11
- };
18
+ accessToken,
19
+ refreshToken
20
+ };
21
+ }
22
+ function getDefaultErrorMessage(error) {
23
+ const axiosError = error;
24
+ const status = axiosError.response?.status;
25
+ const serverMessage = axiosError.response?.data?.error;
26
+ if (serverMessage && serverMessage.trim().length > 0) {
27
+ return serverMessage;
28
+ }
29
+ switch (status) {
30
+ case 0:
31
+ return "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.";
32
+ case 400:
33
+ return "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
34
+ case 401:
35
+ return "\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
36
+ case 403:
37
+ return "\uC811\uADFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
38
+ case 404:
39
+ return "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.";
40
+ case 500:
41
+ return "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
42
+ default:
43
+ return "";
44
+ }
45
+ }
46
+
47
+ // src/client/refreshManager.ts
48
+ var RefreshManager = class {
49
+ constructor() {
50
+ this.isRefreshing = false;
51
+ this.subscribers = [];
52
+ }
53
+ /**
54
+ * 현재 refresh 진행 중인지 반환합니다.
55
+ */
56
+ get refreshing() {
57
+ return this.isRefreshing;
58
+ }
59
+ /**
60
+ * refresh 시작 상태로 전환합니다.
61
+ */
62
+ begin() {
63
+ this.isRefreshing = true;
64
+ }
65
+ /**
66
+ * refresh 종료 상태로 전환합니다.
67
+ */
68
+ end() {
69
+ this.isRefreshing = false;
70
+ }
71
+ /**
72
+ * refresh 완료를 기다리는 subscriber를 등록합니다.
73
+ *
74
+ * @param callback refresh 완료 후 호출될 함수
75
+ */
76
+ subscribe(callback) {
77
+ this.subscribers.push(callback);
78
+ }
79
+ /**
80
+ * 대기 중인 subscriber들에게 결과를 알리고 큐를 초기화합니다.
81
+ *
82
+ * @param token 새 access token
83
+ */
84
+ notify(token) {
85
+ this.subscribers.forEach((callback) => callback(token));
86
+ this.subscribers = [];
87
+ }
88
+ };
89
+ async function requestRefresh(params) {
90
+ const {
91
+ refreshUrl,
92
+ withCredentials,
93
+ refreshStrategy,
94
+ refreshToken,
95
+ parseRefreshResponse
96
+ } = params;
97
+ const requestInit = {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json"
12
101
  },
13
- set(data) {
14
- if (data.accessToken !== void 0) {
15
- window.localStorage.setItem(accessTokenKey, data.accessToken ?? "");
102
+ credentials: withCredentials ? "include" : "same-origin"
103
+ };
104
+ if (refreshStrategy === "body") {
105
+ if (!refreshToken) {
106
+ throw new Error("Refresh token not found");
107
+ }
108
+ requestInit.body = JSON.stringify({ refreshToken });
109
+ }
110
+ const response = await fetch(refreshUrl, requestInit);
111
+ if (!response.ok) {
112
+ throw new Error("Token refresh failed");
113
+ }
114
+ const json = await response.json();
115
+ return parseRefreshResponse(json);
116
+ }
117
+
118
+ // src/client/tokenStorage.ts
119
+ function createAccessTokenAccess(mode, key = "accessToken") {
120
+ let memoryToken = null;
121
+ if (mode === "localStorage") {
122
+ return {
123
+ get: () => window.localStorage.getItem(key),
124
+ set: (token) => {
125
+ if (!token) {
126
+ window.localStorage.removeItem(key);
127
+ } else {
128
+ window.localStorage.setItem(key, token);
129
+ }
130
+ },
131
+ clear: () => {
132
+ window.localStorage.removeItem(key);
16
133
  }
17
- if (data.refreshToken !== void 0) {
18
- window.localStorage.setItem(refreshTokenKey, data.refreshToken ?? "");
134
+ };
135
+ }
136
+ if (mode === "memory") {
137
+ return {
138
+ get: () => memoryToken,
139
+ set: (token) => {
140
+ memoryToken = token;
141
+ },
142
+ clear: () => {
143
+ memoryToken = null;
19
144
  }
145
+ };
146
+ }
147
+ return {
148
+ get: () => null,
149
+ set: () => {
20
150
  },
21
- clear() {
22
- window.localStorage.removeItem(accessTokenKey);
23
- window.localStorage.removeItem(refreshTokenKey);
151
+ clear: () => {
24
152
  }
25
153
  };
26
154
  }
27
- function createCookieModeTokenAccess(keys) {
28
- const refreshTokenKey = keys?.refreshTokenKey ?? "refreshToken";
155
+ function createRefreshTokenAccess(key = "refreshToken") {
29
156
  return {
30
- get() {
31
- return {
32
- accessToken: null,
33
- // 쿠키 기반이면 JS에서 accessToken을 직접 읽지 않는 전제를 둔다.
34
- refreshToken: window.localStorage.getItem(refreshTokenKey)
35
- };
36
- },
37
- set(data) {
38
- if (data.refreshToken !== void 0) {
39
- window.localStorage.setItem(refreshTokenKey, data.refreshToken ?? "");
157
+ get: () => window.localStorage.getItem(key),
158
+ set: (token) => {
159
+ if (!token) {
160
+ window.localStorage.removeItem(key);
161
+ } else {
162
+ window.localStorage.setItem(key, token);
40
163
  }
41
164
  },
42
- clear() {
43
- window.localStorage.removeItem(refreshTokenKey);
165
+ clear: () => {
166
+ window.localStorage.removeItem(key);
44
167
  }
45
168
  };
46
169
  }
170
+
171
+ // src/client/createAxiosClient.ts
172
+ function setAuthorizationHeader(config, accessToken) {
173
+ const headers = new AxiosHeaders();
174
+ if (config.headers) {
175
+ const current = config.headers instanceof AxiosHeaders ? config.headers.toJSON() : config.headers;
176
+ for (const [key, value] of Object.entries(current)) {
177
+ if (value !== void 0) {
178
+ headers.set(key, String(value));
179
+ }
180
+ }
181
+ }
182
+ headers.set("Authorization", `Bearer ${accessToken}`);
183
+ config.headers = headers;
184
+ return config;
185
+ }
47
186
  function createAxiosClient(options) {
48
187
  const {
49
188
  baseURL,
50
189
  timeout = 1e4,
51
190
  withCredentials = true,
52
191
  refreshUrl,
53
- storage = "localStorage",
192
+ accessTokenStorage = "localStorage",
193
+ refreshStrategy = "body",
54
194
  storageKeys,
55
195
  onAuthStateChange,
56
196
  onError,
57
- getErrorMessage
197
+ getErrorMessage,
198
+ shouldRefresh = defaultShouldRefresh,
199
+ parseRefreshResponse = defaultParseRefreshResponse
58
200
  } = options;
59
- const instance = axios.create({ baseURL, timeout, withCredentials });
60
- const tokenAccess = storage === "localStorage" ? createLocalStorageTokenAccess(storageKeys) : createCookieModeTokenAccess({ refreshTokenKey: storageKeys?.refreshTokenKey });
61
- let isRefreshing = false;
62
- let subscribers = [];
63
- const notifySubscribers = (token) => {
64
- subscribers.forEach((cb) => cb(token));
65
- subscribers = [];
66
- };
201
+ const accessTokenKey = storageKeys?.accessTokenKey ?? "accessToken";
202
+ const refreshTokenKey = storageKeys?.refreshTokenKey ?? "refreshToken";
203
+ const instance = axios.create({
204
+ baseURL,
205
+ timeout,
206
+ withCredentials
207
+ });
208
+ const accessTokenAccess = createAccessTokenAccess(accessTokenStorage, accessTokenKey);
209
+ const refreshTokenAccess = createRefreshTokenAccess(refreshTokenKey);
210
+ const refreshManager = new RefreshManager();
67
211
  const clearAuth = () => {
68
- tokenAccess.clear();
212
+ accessTokenAccess.clear();
213
+ if (refreshStrategy === "body") {
214
+ refreshTokenAccess.clear();
215
+ }
69
216
  onAuthStateChange?.("unauthenticated");
70
217
  };
71
- const requestRefresh = async (refreshToken) => {
72
- const res = await fetch(refreshUrl, {
73
- method: "POST",
74
- headers: { "Content-Type": "application/json" },
75
- body: JSON.stringify({ refreshToken }),
76
- credentials: withCredentials ? "include" : "same-origin"
77
- });
78
- if (!res.ok) {
79
- throw new Error("Token refresh failed");
218
+ const getToken = () => {
219
+ return {
220
+ accessToken: accessTokenAccess.get(),
221
+ refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null
222
+ };
223
+ };
224
+ const setToken = (data) => {
225
+ if (data.accessToken !== void 0) {
226
+ accessTokenAccess.set(data.accessToken ?? null);
227
+ }
228
+ if (refreshStrategy === "body" && data.refreshToken !== void 0) {
229
+ refreshTokenAccess.set(data.refreshToken ?? null);
80
230
  }
81
- return await res.json();
82
231
  };
83
232
  instance.interceptors.request.use(
84
233
  (config) => {
85
- const { accessToken } = tokenAccess.get();
234
+ const accessToken = accessTokenAccess.get();
86
235
  if (accessToken) {
87
- config.headers = config.headers ?? {};
88
- config.headers.Authorization = `Bearer ${accessToken}`;
236
+ setAuthorizationHeader(config, accessToken);
89
237
  }
90
238
  return config;
91
239
  },
92
- (e) => Promise.reject(e)
240
+ (error) => Promise.reject(error)
93
241
  );
94
242
  instance.interceptors.response.use(
95
- (res) => res,
243
+ (response) => response,
96
244
  async (error) => {
97
- const response = error.response;
98
- const status = response?.status;
99
245
  const originalRequest = error.config ?? {};
100
- if (status !== 401) {
101
- let message = getErrorMessage?.(error) ?? response?.data?.error ?? "";
102
- if (!message && typeof status === "number") {
103
- switch (status) {
104
- case 0:
105
- message = "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694";
106
- break;
107
- case 400:
108
- message = "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
109
- break;
110
- case 404:
111
- message = "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4";
112
- break;
113
- case 500:
114
- message = "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
115
- break;
116
- }
246
+ if (!shouldRefresh(error)) {
247
+ const message = getErrorMessage?.(error) ?? getDefaultErrorMessage(error);
248
+ if (message) {
249
+ onError?.(message, error);
117
250
  }
118
- if (message) onError?.(message, error);
119
251
  return Promise.reject(error);
120
252
  }
121
- if (originalRequest._retry) {
253
+ const requestUrl = originalRequest.url ?? "";
254
+ if (typeof requestUrl === "string" && requestUrl.includes(refreshUrl)) {
122
255
  clearAuth();
123
256
  return Promise.reject(error);
124
257
  }
125
- originalRequest._retry = true;
126
- const { refreshToken } = tokenAccess.get();
127
- if (!refreshToken) {
258
+ if (originalRequest._retry) {
128
259
  clearAuth();
129
260
  return Promise.reject(error);
130
261
  }
131
- if (isRefreshing) {
262
+ originalRequest._retry = true;
263
+ if (refreshManager.refreshing) {
132
264
  return new Promise((resolve, reject) => {
133
- subscribers.push((newToken) => {
134
- if (!newToken) return reject(error);
135
- originalRequest.headers = originalRequest.headers ?? {};
136
- originalRequest.headers.Authorization = `Bearer ${newToken}`;
137
- resolve(instance(originalRequest));
265
+ refreshManager.subscribe((newAccessToken) => {
266
+ if (!newAccessToken) {
267
+ reject(error);
268
+ return;
269
+ }
270
+ const retryRequest = setAuthorizationHeader(originalRequest, newAccessToken);
271
+ resolve(instance(retryRequest));
138
272
  });
139
273
  });
140
274
  }
141
- isRefreshing = true;
275
+ refreshManager.begin();
142
276
  try {
143
- const newTokens = await requestRefresh(refreshToken);
144
- tokenAccess.set(newTokens);
277
+ const newTokens = await requestRefresh({
278
+ refreshUrl,
279
+ withCredentials,
280
+ refreshStrategy,
281
+ refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null,
282
+ parseRefreshResponse
283
+ });
284
+ setToken(newTokens);
145
285
  onAuthStateChange?.("authenticated");
146
- isRefreshing = false;
147
- notifySubscribers(newTokens.accessToken);
148
- originalRequest.headers = originalRequest.headers ?? {};
149
- originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
150
- return instance(originalRequest);
151
- } catch (e) {
152
- isRefreshing = false;
153
- notifySubscribers(null);
286
+ refreshManager.end();
287
+ refreshManager.notify(newTokens.accessToken);
288
+ const retryRequest = setAuthorizationHeader(originalRequest, newTokens.accessToken);
289
+ return instance(retryRequest);
290
+ } catch (refreshError) {
291
+ refreshManager.end();
292
+ refreshManager.notify(null);
154
293
  clearAuth();
155
- return Promise.reject(error);
294
+ return Promise.reject(refreshError);
156
295
  }
157
296
  }
158
297
  );
159
298
  return {
160
299
  axios: instance,
161
- setToken: (data) => tokenAccess.set(data),
162
- getToken: () => tokenAccess.get(),
163
- clearToken: () => clearAuth()
300
+ setToken,
301
+ getToken,
302
+ clearToken: clearAuth
164
303
  };
165
304
  }
166
305
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wheeparam/library",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",