vue-api-kit 1.4.1 → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MelvishNiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -237,6 +237,7 @@ async function handleSubmit() {
237
237
  - ✅ **File Upload**: Support for multipart/form-data in mutations
238
238
  - ✅ **Path Parameters**: Automatic path parameter replacement
239
239
  - ✅ **Debouncing**: Built-in request debouncing
240
+ - ✅ **CSRF Protection**: Automatic CSRF token refresh on 403/419 errors
240
241
  - ✅ **Global Error Handling**: Centralized error management
241
242
  - ✅ **Request Interceptors**: Modify requests before sending
242
243
  - ✅ **Fully Typed**: Complete type inference for params, data, and response
@@ -252,6 +253,9 @@ const api = createApiClient({
252
253
  },
253
254
  withCredentials: true,
254
255
 
256
+ // CSRF Token Protection
257
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Auto-refresh CSRF token on 403/419 errors
258
+
255
259
  // Global handlers
256
260
  onBeforeRequest: async (config) => {
257
261
  // Modify request before sending
@@ -266,7 +270,7 @@ const api = createApiClient({
266
270
  },
267
271
 
268
272
  onFinishRequest: async () => {
269
- // Called when request finishes
273
+ // Called when request finishes (success or error)
270
274
  console.log('Request finished');
271
275
  },
272
276
 
@@ -392,10 +396,10 @@ import { postQueries, postMutations } from './post-api';
392
396
  // Approach 1: Merge queries and mutations separately
393
397
  export const api = createApiClient({
394
398
  baseURL: 'https://api.example.com',
395
-
399
+
396
400
  // Merge all queries from different modules
397
401
  queries: mergeQueries(userQueries, postQueries),
398
-
402
+
399
403
  // Merge all mutations from different modules
400
404
  mutations: mergeMutations(userMutations, postMutations)
401
405
  });
@@ -448,6 +452,81 @@ async function handleUpload(file: File) {
448
452
  }
449
453
  ```
450
454
 
455
+ ## 🔒 CSRF Token Protection
456
+
457
+ The client includes built-in CSRF token protection, perfect for Laravel Sanctum or similar CSRF-based authentication systems.
458
+
459
+ ### How it works
460
+
461
+ When you set `csrfRefreshEndpoint`, the client will:
462
+ 1. Automatically detect CSRF errors (403 or 419 status codes)
463
+ 2. Call the CSRF refresh endpoint to get a new token
464
+ 3. Retry the original request with the fresh token
465
+ 4. Prevent infinite loops and race conditions
466
+
467
+ ### Configuration
468
+
469
+ ```typescript
470
+ const api = createApiClient({
471
+ baseURL: 'https://api.example.com',
472
+ withCredentials: true, // Required for CSRF cookies
473
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel Sanctum endpoint
474
+
475
+ queries: { /* ... */ },
476
+ mutations: { /* ... */ }
477
+ });
478
+ ```
479
+
480
+ ### Use Case: Laravel Sanctum
481
+
482
+ ```typescript
483
+ // api.ts
484
+ import { createApiClient } from 'vue-api-kit';
485
+ import { z } from 'zod';
486
+
487
+ export const api = createApiClient({
488
+ baseURL: 'https://api.example.com',
489
+ withCredentials: true, // Send cookies with requests
490
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel's CSRF endpoint
491
+
492
+ mutations: {
493
+ login: {
494
+ method: 'POST',
495
+ path: '/login',
496
+ data: z.object({
497
+ email: z.string().email(),
498
+ password: z.string()
499
+ }),
500
+ response: z.object({
501
+ user: z.object({
502
+ id: z.number(),
503
+ name: z.string(),
504
+ email: z.string()
505
+ })
506
+ })
507
+ },
508
+ createPost: {
509
+ method: 'POST',
510
+ path: '/posts',
511
+ data: z.object({
512
+ title: z.string(),
513
+ content: z.string()
514
+ })
515
+ }
516
+ }
517
+ });
518
+ ```
519
+
520
+ ### Benefits
521
+
522
+ - ✅ **Automatic Recovery**: No manual token refresh needed
523
+ - ✅ **Seamless UX**: Users don't experience authentication errors
524
+ - ✅ **Race Condition Safe**: Multiple simultaneous requests share the same refresh
525
+ - ✅ **Infinite Loop Prevention**: Won't retry the CSRF endpoint itself
526
+ - ✅ **Laravel Sanctum Compatible**: Works perfectly with Laravel's SPA authentication
527
+
528
+
529
+
451
530
  ## 📝 License
452
531
 
453
532
  MIT
@@ -72,6 +72,8 @@ export interface ApiMutation<TData extends ZodType<any> | undefined = ZodType<an
72
72
  * const options: ApiClientOptions = {
73
73
  * baseURL: "https://api.example.com",
74
74
  * headers: { Authorization: "Bearer token" },
75
+ * withCredentials: true,
76
+ * csrfRefreshEndpoint: "/auth/refresh-csrf",
75
77
  * queries: { getUsers: { path: "/users" } },
76
78
  * mutations: { createUser: { method: "POST", path: "/users" } },
77
79
  * onErrorRequest: (error) => console.error(error.message)
@@ -81,11 +83,12 @@ export interface ApiClientOptions<Q extends Record<string, ApiQuery> = Record<st
81
83
  baseURL: string;
82
84
  headers?: Record<string, string>;
83
85
  withCredentials?: boolean;
86
+ csrfRefreshEndpoint?: string;
84
87
  queries?: Q;
85
88
  mutations?: M;
86
- onBeforeRequest?: (config: InternalAxiosRequestConfig<any>) => Promise<any> | any;
87
- onStartRequest?: () => Promise<void>;
88
- onFinishRequest?: () => Promise<void>;
89
+ onBeforeRequest?: (config: InternalAxiosRequestConfig<any>) => Promise<any> | void | any;
90
+ onStartRequest?: () => Promise<void> | void | any;
91
+ onFinishRequest?: () => Promise<void> | void | any;
89
92
  onErrorRequest?: (error: {
90
93
  message: string;
91
94
  status?: number;
@@ -177,7 +180,7 @@ export interface MutationResult<TResult, TData = any, TParams = any> {
177
180
  isLoading: Ref<boolean>;
178
181
  isDone: Ref<boolean>;
179
182
  uploadProgress: Ref<number>;
180
- mutate: ({ data, params }: {
183
+ mutate: (rgs?: {
181
184
  data?: TData;
182
185
  params?: TParams;
183
186
  }) => Promise<void>;
package/dist/index.js CHANGED
@@ -1,196 +1,218 @@
1
- import { ZodError as P } from "zod";
2
- import * as k from "zod";
3
- import L, { AxiosError as w } from "axios";
4
- import { AxiosError as G } from "axios";
5
- import { nextTick as S, ref as m, onMounted as x, watch as N, onBeforeUnmount as T } from "vue";
1
+ import { ZodError as L } from "zod";
2
+ import * as $ from "zod";
3
+ import k, { AxiosError as S } from "axios";
4
+ import { AxiosError as K } from "axios";
5
+ import { nextTick as x, ref as m, onMounted as F, watch as N, onBeforeUnmount as T } from "vue";
6
6
  import { debounce as Z } from "lodash-es";
7
- function I(r) {
8
- const g = L.create({
9
- baseURL: r.baseURL,
7
+ function z(s) {
8
+ const h = k.create({
9
+ baseURL: s.baseURL,
10
10
  headers: {
11
11
  "Content-Type": "application/json",
12
12
  Accept: "application/json",
13
- ...r.headers
13
+ ...s.headers
14
14
  },
15
- withCredentials: r.withCredentials ?? !1
15
+ withCredentials: s.withCredentials ?? !1
16
16
  });
17
- r.onBeforeRequest && g.interceptors.request.use(
17
+ let b = !1, q = null;
18
+ s.onBeforeRequest && h.interceptors.request.use(
18
19
  async (e) => {
19
20
  try {
20
- return await r.onBeforeRequest(e) || e;
21
- } catch (a) {
22
- return Promise.reject(a);
21
+ return await s.onBeforeRequest(e) || e;
22
+ } catch (r) {
23
+ return Promise.reject(r);
23
24
  }
24
25
  },
25
26
  (e) => Promise.reject(e)
26
- ), r.onStartRequest && g.interceptors.request.use(
27
+ ), s.onStartRequest && h.interceptors.request.use(
27
28
  async (e) => {
28
29
  try {
29
- return await r.onStartRequest(), e;
30
- } catch (a) {
31
- return Promise.reject(a);
30
+ return await s.onStartRequest(), e;
31
+ } catch (r) {
32
+ return Promise.reject(r);
32
33
  }
33
34
  },
34
35
  (e) => Promise.reject(e)
35
- ), r.onFinishRequest && g.interceptors.response.use(
36
- (e) => (r.onFinishRequest(), e),
37
- (e) => Promise.reject(e)
38
- ), g.interceptors.request.use((e) => {
36
+ ), s.onFinishRequest && h.interceptors.response.use(
37
+ (e) => (s.onFinishRequest(), e),
38
+ (e) => (s.onFinishRequest(), Promise.reject(e))
39
+ ), h.interceptors.request.use((e) => {
39
40
  if (!e.url) return e;
40
- const a = (o) => {
41
- if (o)
42
- for (const [t, v] of Object.entries(o)) {
41
+ const r = (a) => {
42
+ if (a)
43
+ for (const [t, v] of Object.entries(a)) {
43
44
  const l = `{${t}}`;
44
45
  e.url.includes(l) && (e.url = e.url.replace(
45
46
  l,
46
47
  encodeURIComponent(String(v))
47
- ), delete o[t]);
48
+ ), delete a[t]);
48
49
  }
49
50
  };
50
- return e.method !== "get" && e.data?.params && a(e.data.params), a(e.params), e;
51
- }), g.interceptors.response.use(
51
+ return e.method !== "get" && e.data?.params && r(e.data.params), r(e.params), e;
52
+ }), s.csrfRefreshEndpoint && h.interceptors.response.use(
53
+ (e) => e,
54
+ async (e) => {
55
+ const r = e.config;
56
+ if (r.url === s.csrfRefreshEndpoint)
57
+ return Promise.reject(e);
58
+ if (e.response && (e.response.status === 403 || e.response.status === 419) && !r._retry) {
59
+ r._retry = !0;
60
+ try {
61
+ return b && q ? await q : (b = !0, q = h.get(s.csrfRefreshEndpoint, {
62
+ // Mark this request to prevent retry
63
+ _skipRetry: !0
64
+ }).then(() => {
65
+ b = !1, q = null;
66
+ }), await q), h(r);
67
+ } catch (a) {
68
+ return b = !1, q = null, Promise.reject(a);
69
+ }
70
+ }
71
+ return Promise.reject(e);
72
+ }
73
+ ), h.interceptors.response.use(
52
74
  (e) => e,
53
- (e) => (S(() => {
75
+ (e) => (x(() => {
54
76
  e.code;
55
77
  }), Promise.reject(e))
56
78
  );
57
- const j = r.queries ?? {}, C = {};
58
- for (const e in j) {
59
- const a = j[e];
60
- a && (C[e] = (o) => {
79
+ const C = s.queries ?? {}, M = {};
80
+ for (const e in C) {
81
+ const r = C[e];
82
+ r && (M[e] = (a) => {
61
83
  let t;
62
- o && typeof o == "object" && ("loadOnMount" in o || "debounce" in o || "onResult" in o || "onError" in o || "data" in o ? t = o : t = { params: o });
63
- const v = m(), l = m(), E = m(), R = m(!1), y = m(!1), A = m(!0);
64
- let q = new AbortController();
84
+ a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ? t = a : t = { params: a });
85
+ const v = m(), l = m(), R = m(), g = m(!1), y = m(!1), P = m(!0);
86
+ let j = new AbortController();
65
87
  const u = () => {
66
- q?.abort(), q = new AbortController();
67
- }, i = async () => {
68
- R.value && u(), R.value = !0, l.value = void 0;
88
+ j?.abort(), j = new AbortController();
89
+ }, c = async () => {
90
+ g.value && u(), g.value = !0, l.value = void 0;
69
91
  try {
70
- a.params && t?.params && a.params.parse(t.params);
71
- let s = t?.data;
72
- a.data && s && a.data.parse(s);
92
+ r.params && t?.params && r.params.parse(t.params);
93
+ let o = t?.data;
94
+ r.data && o && r.data.parse(o);
73
95
  const f = {
74
- method: a.method ?? "GET",
75
- url: a.path,
96
+ method: r.method ?? "GET",
97
+ url: r.path,
76
98
  params: t?.params,
77
- signal: q.signal
99
+ signal: j.signal
78
100
  };
79
- a.method === "POST" && s && (f.data = s);
80
- const n = await g.request(f), c = a.response ? a.response.parse(n.data) : n.data;
81
- v.value = c, t?.onResult?.(c);
82
- } catch (s) {
83
- if (s instanceof w) {
84
- if (s.code !== "ERR_CANCELED") {
85
- const f = s.response?.data?.message || s.message || "An error occurred", n = s.response?.status, c = s.code, p = s.response?.data;
86
- l.value = f, t?.onError?.(s), r.onErrorRequest?.({ message: f, status: n, code: c, data: p });
101
+ r.method === "POST" && o && (f.data = o);
102
+ const n = await h.request(f), i = r.response ? r.response.parse(n.data) : n.data;
103
+ v.value = i, t?.onResult?.(i);
104
+ } catch (o) {
105
+ if (o instanceof S) {
106
+ if (o.code !== "ERR_CANCELED") {
107
+ const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code, p = o.response?.data;
108
+ l.value = f, t?.onError?.(o), s.onErrorRequest?.({ message: f, status: n, code: i, data: p });
87
109
  }
88
- } else if (s instanceof P) {
89
- E.value = s.issues || [];
90
- const n = `Validation error: ${E.value.map(
91
- (c) => `${c.path.join(".")}: ${c.message}`
110
+ } else if (o instanceof L) {
111
+ R.value = o.issues || [];
112
+ const n = `Validation error: ${R.value.map(
113
+ (i) => `${i.path.join(".")}: ${i.message}`
92
114
  ).join(", ")}`;
93
- l.value = n, t?.onError?.(s), t?.onZodError?.(E.value), r.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), r.onZodError && r.onZodError(E.value);
115
+ l.value = n, t?.onError?.(o), t?.onZodError?.(R.value), s.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), s.onZodError && s.onZodError(R.value);
94
116
  } else {
95
- const f = s.message || "An error occurred";
96
- l.value = f, t?.onError?.(f), r.onErrorRequest?.({ message: f });
117
+ const f = o.message || "An error occurred";
118
+ l.value = f, t?.onError?.(f), s.onErrorRequest?.({ message: f });
97
119
  }
98
120
  } finally {
99
- R.value = !1, y.value = !0;
121
+ g.value = !1, y.value = !0;
100
122
  }
101
- }, h = t?.debounce ? Z(i, t.debounce) : i;
123
+ }, E = t?.debounce ? Z(c, t.debounce) : c;
102
124
  let d = null;
103
- return (t?.params || t?.data) && (x(() => {
125
+ return (t?.params || t?.data) && (F(() => {
104
126
  d && d(), d = N(
105
127
  () => JSON.stringify({ params: t.params, data: t.data }),
106
128
  () => {
107
- h();
129
+ E();
108
130
  },
109
131
  { immediate: !1 }
110
132
  );
111
133
  }), T(() => {
112
- d && d(), q?.abort();
113
- })), (t?.loadOnMount === void 0 || t.loadOnMount) && !y.value && (A.value ? (A.value = !1, i()) : h()), { result: v, errorMessage: l, zodErrors: E, isLoading: R, isDone: y, refetch: i };
134
+ d && d(), j?.abort();
135
+ })), (t?.loadOnMount === void 0 || t.loadOnMount) && !y.value && (P.value ? (P.value = !1, c()) : E()), { result: v, errorMessage: l, zodErrors: R, isLoading: g, isDone: y, refetch: c };
114
136
  });
115
137
  }
116
- const M = r.mutations ?? {}, D = {};
117
- for (const e in M) {
118
- const a = M[e];
119
- a && (D[e] = (o) => {
120
- const t = m(), v = m(), l = m(), E = m(!1), R = m(!1), y = m(0);
121
- return { result: t, errorMessage: v, zodErrors: l, isLoading: E, isDone: R, uploadProgress: y, mutate: async (q) => {
122
- if (!E.value) {
123
- E.value = !0, v.value = void 0, y.value = 0;
138
+ const D = s.mutations ?? {}, w = {};
139
+ for (const e in D) {
140
+ const r = D[e];
141
+ r && (w[e] = (a) => {
142
+ const t = m(), v = m(), l = m(), R = m(!1), g = m(!1), y = m(0);
143
+ return { result: t, errorMessage: v, zodErrors: l, isLoading: R, isDone: g, uploadProgress: y, mutate: async (j) => {
144
+ if (!R.value) {
145
+ R.value = !0, v.value = void 0, y.value = 0;
124
146
  try {
125
- const { data: u, params: i } = q;
126
- let h = u ?? {}, d = {};
127
- if (a.isMultipart) {
147
+ const { data: u = {}, params: c } = j ?? {};
148
+ let E = u ?? {}, d = {};
149
+ if (r.isMultipart) {
128
150
  const n = new FormData();
129
- for (const [c, p] of Object.entries(u))
130
- p instanceof File || p instanceof Blob ? n.append(c, p) : Array.isArray(p) ? p.forEach((b) => {
131
- b instanceof File || b instanceof Blob ? n.append(c, b) : n.append(c, JSON.stringify(b));
132
- }) : typeof p == "object" && p !== null ? n.append(c, JSON.stringify(p)) : n.append(c, String(p));
133
- h = n, d["Content-Type"] = "multipart/form-data";
134
- } else a.data && a.data.parse(u);
135
- a.params && i && a.params.parse(i);
136
- const s = await g.request({
137
- method: a.method,
138
- url: a.path,
139
- data: h,
140
- params: i,
151
+ for (const [i, p] of Object.entries(u))
152
+ p instanceof File || p instanceof Blob ? n.append(i, p) : Array.isArray(p) ? p.forEach((A) => {
153
+ A instanceof File || A instanceof Blob ? n.append(i, A) : n.append(i, JSON.stringify(A));
154
+ }) : typeof p == "object" && p !== null ? n.append(i, JSON.stringify(p)) : n.append(i, String(p));
155
+ E = n, d["Content-Type"] = "multipart/form-data";
156
+ } else r.data && r.data.parse(u);
157
+ r.params && c && r.params.parse(c);
158
+ const o = await h.request({
159
+ method: r.method,
160
+ url: r.path,
161
+ data: E,
162
+ params: c,
141
163
  headers: d,
142
164
  onUploadProgress: (n) => {
143
165
  if (n.total) {
144
- const c = Math.round(n.loaded * 100 / n.total);
145
- y.value = c, o?.onUploadProgress?.(c);
166
+ const i = Math.round(n.loaded * 100 / n.total);
167
+ y.value = i, a?.onUploadProgress?.(i);
146
168
  }
147
169
  }
148
- }), f = a.response ? a.response.parse(s.data) : s.data;
149
- t.value = f, o?.onResult?.(f);
170
+ }), f = r.response ? r.response.parse(o.data) : o.data;
171
+ t.value = f, a?.onResult?.(f);
150
172
  } catch (u) {
151
- if (u instanceof w) {
152
- const i = u.response?.data?.message || u.message || "An error occurred", h = u.response?.status, d = u.code;
153
- v.value = i, o?.onError?.(u), r.onErrorRequest?.({ message: i, status: h, code: d });
154
- } else if (u instanceof P) {
173
+ if (u instanceof S) {
174
+ const c = u.response?.data?.message || u.message || "An error occurred", E = u.response?.status, d = u.code;
175
+ v.value = c, a?.onError?.(u), s.onErrorRequest?.({ message: c, status: E, code: d });
176
+ } else if (u instanceof L) {
155
177
  l.value = u.issues || [];
156
- const h = `Validation error: ${l.value.map(
178
+ const E = `Validation error: ${l.value.map(
157
179
  (d) => `${d.path.join(".")}: ${d.message}`
158
180
  ).join(", ")}`;
159
- v.value = h, o?.onError?.(u), o?.onZodError?.(l.value), r.onErrorRequest?.({ message: h, code: "VALIDATION_ERROR" }), r.onZodError && r.onZodError(l.value);
181
+ v.value = E, a?.onError?.(u), a?.onZodError?.(l.value), s.onErrorRequest?.({ message: E, code: "VALIDATION_ERROR" }), s.onZodError && s.onZodError(l.value);
160
182
  } else {
161
- const i = u.message || "An error occurred";
162
- v.value = i, o?.onError?.(u), r.onErrorRequest?.({ message: i });
183
+ const c = u.message || "An error occurred";
184
+ v.value = c, a?.onError?.(u), s.onErrorRequest?.({ message: c });
163
185
  }
164
186
  } finally {
165
- E.value = !1, R.value = !0;
187
+ R.value = !1, g.value = !0;
166
188
  }
167
189
  }
168
190
  } };
169
191
  });
170
192
  }
171
193
  return {
172
- query: C,
173
- mutation: D
194
+ query: M,
195
+ mutation: w
174
196
  };
175
197
  }
176
- function z(r) {
177
- return r;
198
+ function V(s) {
199
+ return s;
178
200
  }
179
- function V(r) {
180
- return r;
201
+ function J(s) {
202
+ return s;
181
203
  }
182
- function _(...r) {
183
- return Object.assign({}, ...r);
204
+ function Q(...s) {
205
+ return Object.assign({}, ...s);
184
206
  }
185
- function J(...r) {
186
- return Object.assign({}, ...r);
207
+ function W(...s) {
208
+ return Object.assign({}, ...s);
187
209
  }
188
210
  export {
189
- G as AxiosError,
190
- I as createApiClient,
191
- V as defineMutation,
192
- z as defineQuery,
193
- J as mergeMutations,
194
- _ as mergeQueries,
195
- k as z
211
+ K as AxiosError,
212
+ z as createApiClient,
213
+ J as defineMutation,
214
+ V as defineQuery,
215
+ W as mergeMutations,
216
+ Q as mergeQueries,
217
+ $ as z
196
218
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vue-api-kit",
3
3
  "type": "module",
4
- "version": "1.4.1",
4
+ "version": "1.5.0",
5
5
  "description": "A powerful and flexible API client for Vue 3 applications, built with TypeScript and Zod for type-safe API interactions.",
6
6
  "keywords": [
7
7
  "vue3",