vue-api-kit 1.4.2 → 1.5.1

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/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,6 +83,7 @@ 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
89
  onBeforeRequest?: (config: InternalAxiosRequestConfig<any>) => Promise<any> | void | any;
@@ -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,214 @@
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";
6
- import { debounce as Z } from "lodash-es";
7
- function I(r) {
8
- const g = L.create({
1
+ import { ZodError as L } from "zod";
2
+ import * as T 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 Z } from "vue";
6
+ import { debounce as _ } from "lodash-es";
7
+ function z(r) {
8
+ const h = k.create({
9
9
  baseURL: r.baseURL,
10
- headers: {
11
- "Content-Type": "application/json",
12
- Accept: "application/json",
13
- ...r.headers
14
- },
10
+ ...r.headers && { headers: r.headers },
15
11
  withCredentials: r.withCredentials ?? !1
16
12
  });
17
- r.onBeforeRequest && g.interceptors.request.use(
13
+ let P = !1, q = null;
14
+ r.onBeforeRequest && h.interceptors.request.use(
18
15
  async (e) => {
19
16
  try {
20
17
  return await r.onBeforeRequest(e) || e;
21
- } catch (a) {
22
- return Promise.reject(a);
18
+ } catch (s) {
19
+ return Promise.reject(s);
23
20
  }
24
21
  },
25
22
  (e) => Promise.reject(e)
26
- ), r.onStartRequest && g.interceptors.request.use(
23
+ ), r.onStartRequest && h.interceptors.request.use(
27
24
  async (e) => {
28
25
  try {
29
26
  return await r.onStartRequest(), e;
30
- } catch (a) {
31
- return Promise.reject(a);
27
+ } catch (s) {
28
+ return Promise.reject(s);
32
29
  }
33
30
  },
34
31
  (e) => Promise.reject(e)
35
- ), r.onFinishRequest && g.interceptors.response.use(
32
+ ), r.onFinishRequest && h.interceptors.response.use(
36
33
  (e) => (r.onFinishRequest(), e),
37
- (e) => Promise.reject(e)
38
- ), g.interceptors.request.use((e) => {
34
+ (e) => (r.onFinishRequest(), Promise.reject(e))
35
+ ), h.interceptors.request.use((e) => {
39
36
  if (!e.url) return e;
40
- const a = (o) => {
41
- if (o)
42
- for (const [t, v] of Object.entries(o)) {
37
+ const s = (a) => {
38
+ if (a)
39
+ for (const [t, v] of Object.entries(a)) {
43
40
  const l = `{${t}}`;
44
41
  e.url.includes(l) && (e.url = e.url.replace(
45
42
  l,
46
43
  encodeURIComponent(String(v))
47
- ), delete o[t]);
44
+ ), delete a[t]);
48
45
  }
49
46
  };
50
- return e.method !== "get" && e.data?.params && a(e.data.params), a(e.params), e;
51
- }), g.interceptors.response.use(
47
+ return e.method !== "get" && e.data?.params && s(e.data.params), s(e.params), e;
48
+ }), r.csrfRefreshEndpoint && h.interceptors.response.use(
52
49
  (e) => e,
53
- (e) => (S(() => {
50
+ async (e) => {
51
+ const s = e.config;
52
+ if (s.url === r.csrfRefreshEndpoint)
53
+ return Promise.reject(e);
54
+ if (e.response && (e.response.status === 403 || e.response.status === 419) && !s._retry) {
55
+ s._retry = !0;
56
+ try {
57
+ return P && q ? await q : (P = !0, q = h.get(r.csrfRefreshEndpoint, {
58
+ // Mark this request to prevent retry
59
+ _skipRetry: !0
60
+ }).then(() => {
61
+ P = !1, q = null;
62
+ }), await q), h(s);
63
+ } catch (a) {
64
+ return P = !1, q = null, Promise.reject(a);
65
+ }
66
+ }
67
+ return Promise.reject(e);
68
+ }
69
+ ), h.interceptors.response.use(
70
+ (e) => e,
71
+ (e) => (x(() => {
54
72
  e.code;
55
73
  }), Promise.reject(e))
56
74
  );
57
- const j = r.queries ?? {}, C = {};
58
- for (const e in j) {
59
- const a = j[e];
60
- a && (C[e] = (o) => {
75
+ const C = r.queries ?? {}, M = {};
76
+ for (const e in C) {
77
+ const s = C[e];
78
+ s && (M[e] = (a) => {
61
79
  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();
80
+ a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ? t = a : t = { params: a });
81
+ const v = m(), l = m(), R = m(), g = m(!1), y = m(!1), A = m(!0);
82
+ let b = new AbortController();
65
83
  const u = () => {
66
- q?.abort(), q = new AbortController();
67
- }, i = async () => {
68
- R.value && u(), R.value = !0, l.value = void 0;
84
+ b?.abort(), b = new AbortController();
85
+ }, c = async () => {
86
+ g.value && u(), g.value = !0, l.value = void 0;
69
87
  try {
70
- a.params && t?.params && a.params.parse(t.params);
71
- let s = t?.data;
72
- a.data && s && a.data.parse(s);
88
+ s.params && t?.params && s.params.parse(t.params);
89
+ let o = t?.data;
90
+ s.data && o && s.data.parse(o);
73
91
  const f = {
74
- method: a.method ?? "GET",
75
- url: a.path,
92
+ method: s.method ?? "GET",
93
+ url: s.path,
76
94
  params: t?.params,
77
- signal: q.signal
95
+ signal: b.signal
78
96
  };
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 });
97
+ s.method === "POST" && o && (f.data = o);
98
+ const n = await h.request(f), i = s.response ? s.response.parse(n.data) : n.data;
99
+ v.value = i, t?.onResult?.(i);
100
+ } catch (o) {
101
+ if (o instanceof S) {
102
+ if (o.code !== "ERR_CANCELED") {
103
+ const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code, p = o.response?.data;
104
+ l.value = f, t?.onError?.(o), r.onErrorRequest?.({ message: f, status: n, code: i, data: p });
87
105
  }
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}`
106
+ } else if (o instanceof L) {
107
+ R.value = o.issues || [];
108
+ const n = `Validation error: ${R.value.map(
109
+ (i) => `${i.path.join(".")}: ${i.message}`
92
110
  ).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);
111
+ l.value = n, t?.onError?.(o), t?.onZodError?.(R.value), r.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), r.onZodError && r.onZodError(R.value);
94
112
  } else {
95
- const f = s.message || "An error occurred";
113
+ const f = o.message || "An error occurred";
96
114
  l.value = f, t?.onError?.(f), r.onErrorRequest?.({ message: f });
97
115
  }
98
116
  } finally {
99
- R.value = !1, y.value = !0;
117
+ g.value = !1, y.value = !0;
100
118
  }
101
- }, h = t?.debounce ? Z(i, t.debounce) : i;
119
+ }, E = t?.debounce ? _(c, t.debounce) : c;
102
120
  let d = null;
103
- return (t?.params || t?.data) && (x(() => {
121
+ return (t?.params || t?.data) && (F(() => {
104
122
  d && d(), d = N(
105
123
  () => JSON.stringify({ params: t.params, data: t.data }),
106
124
  () => {
107
- h();
125
+ E();
108
126
  },
109
127
  { immediate: !1 }
110
128
  );
111
- }), 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 };
129
+ }), Z(() => {
130
+ d && d(), b?.abort();
131
+ })), (t?.loadOnMount === void 0 || t.loadOnMount) && !y.value && (A.value ? (A.value = !1, c()) : E()), { result: v, errorMessage: l, zodErrors: R, isLoading: g, isDone: y, refetch: c };
114
132
  });
115
133
  }
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;
134
+ const D = r.mutations ?? {}, w = {};
135
+ for (const e in D) {
136
+ const s = D[e];
137
+ s && (w[e] = (a) => {
138
+ const t = m(), v = m(), l = m(), R = m(!1), g = m(!1), y = m(0);
139
+ return { result: t, errorMessage: v, zodErrors: l, isLoading: R, isDone: g, uploadProgress: y, mutate: async (b) => {
140
+ if (!R.value) {
141
+ R.value = !0, v.value = void 0, y.value = 0;
124
142
  try {
125
- const { data: u, params: i } = q;
126
- let h = u ?? {}, d = {};
127
- if (a.isMultipart) {
143
+ const { data: u = {}, params: c } = b ?? {};
144
+ let E = u ?? {}, d = {};
145
+ if (s.isMultipart) {
128
146
  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,
147
+ for (const [i, p] of Object.entries(u))
148
+ p instanceof File || p instanceof Blob ? n.append(i, p) : Array.isArray(p) ? p.forEach((j) => {
149
+ j instanceof File || j instanceof Blob ? n.append(i, j) : n.append(i, JSON.stringify(j));
150
+ }) : typeof p == "object" && p !== null ? n.append(i, JSON.stringify(p)) : n.append(i, String(p));
151
+ E = n, d["Content-Type"] = "multipart/form-data";
152
+ } else s.data && s.data.parse(u);
153
+ s.params && c && s.params.parse(c);
154
+ const o = await h.request({
155
+ method: s.method,
156
+ url: s.path,
157
+ data: E,
158
+ params: c,
141
159
  headers: d,
142
160
  onUploadProgress: (n) => {
143
161
  if (n.total) {
144
- const c = Math.round(n.loaded * 100 / n.total);
145
- y.value = c, o?.onUploadProgress?.(c);
162
+ const i = Math.round(n.loaded * 100 / n.total);
163
+ y.value = i, a?.onUploadProgress?.(i);
146
164
  }
147
165
  }
148
- }), f = a.response ? a.response.parse(s.data) : s.data;
149
- t.value = f, o?.onResult?.(f);
166
+ }), f = s.response ? s.response.parse(o.data) : o.data;
167
+ t.value = f, a?.onResult?.(f);
150
168
  } 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) {
169
+ if (u instanceof S) {
170
+ const c = u.response?.data?.message || u.message || "An error occurred", E = u.response?.status, d = u.code;
171
+ v.value = c, a?.onError?.(u), r.onErrorRequest?.({ message: c, status: E, code: d });
172
+ } else if (u instanceof L) {
155
173
  l.value = u.issues || [];
156
- const h = `Validation error: ${l.value.map(
174
+ const E = `Validation error: ${l.value.map(
157
175
  (d) => `${d.path.join(".")}: ${d.message}`
158
176
  ).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);
177
+ v.value = E, a?.onError?.(u), a?.onZodError?.(l.value), r.onErrorRequest?.({ message: E, code: "VALIDATION_ERROR" }), r.onZodError && r.onZodError(l.value);
160
178
  } else {
161
- const i = u.message || "An error occurred";
162
- v.value = i, o?.onError?.(u), r.onErrorRequest?.({ message: i });
179
+ const c = u.message || "An error occurred";
180
+ v.value = c, a?.onError?.(u), r.onErrorRequest?.({ message: c });
163
181
  }
164
182
  } finally {
165
- E.value = !1, R.value = !0;
183
+ R.value = !1, g.value = !0;
166
184
  }
167
185
  }
168
186
  } };
169
187
  });
170
188
  }
171
189
  return {
172
- query: C,
173
- mutation: D
190
+ query: M,
191
+ mutation: w
174
192
  };
175
193
  }
176
- function z(r) {
194
+ function V(r) {
177
195
  return r;
178
196
  }
179
- function V(r) {
197
+ function J(r) {
180
198
  return r;
181
199
  }
182
- function _(...r) {
200
+ function Q(...r) {
183
201
  return Object.assign({}, ...r);
184
202
  }
185
- function J(...r) {
203
+ function W(...r) {
186
204
  return Object.assign({}, ...r);
187
205
  }
188
206
  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
207
+ K as AxiosError,
208
+ z as createApiClient,
209
+ J as defineMutation,
210
+ V as defineQuery,
211
+ W as mergeMutations,
212
+ Q as mergeQueries,
213
+ T as z
196
214
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vue-api-kit",
3
3
  "type": "module",
4
- "version": "1.4.2",
4
+ "version": "1.5.1",
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",