vue-api-kit 1.5.2 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +24 -7
  2. package/dist/index.js +93 -90
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -458,18 +458,23 @@ The client includes built-in CSRF token protection, perfect for Laravel Sanctum
458
458
 
459
459
  ### How it works
460
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
461
+ **Automatic XSRF Token Handling:**
462
+ 1. When `withCredentials: true`, the client automatically reads the `XSRF-TOKEN` cookie
463
+ 2. Decodes and sends it as `X-XSRF-TOKEN` header with every request
464
+ 3. This satisfies Laravel Sanctum's CSRF protection requirements
465
+
466
+ **Automatic CSRF Refresh:**
467
+ 1. Detects CSRF errors (403 or 419 status codes)
468
+ 2. Calls the CSRF refresh endpoint to get a new token
469
+ 3. Retries the original request automatically with the fresh token
470
+ 4. Prevents infinite loops and race conditions
466
471
 
467
472
  ### Configuration
468
473
 
469
474
  ```typescript
470
475
  const api = createApiClient({
471
476
  baseURL: 'https://api.example.com',
472
- withCredentials: true, // Required for CSRF cookies
477
+ withCredentials: true, // Required! Enables cookies AND auto XSRF header
473
478
  csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel Sanctum endpoint
474
479
 
475
480
  queries: { /* ... */ },
@@ -486,7 +491,7 @@ import { z } from 'zod';
486
491
 
487
492
  export const api = createApiClient({
488
493
  baseURL: 'https://api.example.com',
489
- withCredentials: true, // Send cookies with requests
494
+ withCredentials: true, // Enables cookies AND automatic XSRF-TOKEN header
490
495
  csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel's CSRF endpoint
491
496
 
492
497
  mutations: {
@@ -519,12 +524,24 @@ export const api = createApiClient({
519
524
 
520
525
  ### Benefits
521
526
 
527
+ - ✅ **Automatic XSRF Header**: Cookie automatically sent as `X-XSRF-TOKEN` header
522
528
  - ✅ **Automatic Recovery**: No manual token refresh needed
523
529
  - ✅ **Seamless UX**: Users don't experience authentication errors
524
530
  - ✅ **Race Condition Safe**: Multiple simultaneous requests share the same refresh
525
531
  - ✅ **Infinite Loop Prevention**: Won't retry the CSRF endpoint itself
526
532
  - ✅ **Laravel Sanctum Compatible**: Works perfectly with Laravel's SPA authentication
527
533
 
534
+ ### Important Notes
535
+
536
+ 1. **withCredentials is required**: Set `withCredentials: true` to enable both cookies and automatic XSRF token handling
537
+ 2. **Cookie Domain**: Ensure your API sets cookies with the correct domain (e.g., `.localhost` for local development)
538
+ 3. **CORS Configuration**: Your Laravel backend must allow credentials:
539
+ ```php
540
+ // config/cors.php
541
+ 'supports_credentials' => true,
542
+ 'allowed_origins' => ['http://localhost:5173'],
543
+ ```
544
+
528
545
 
529
546
 
530
547
  ## 📝 License
package/dist/index.js CHANGED
@@ -1,161 +1,164 @@
1
- import { ZodError as L } from "zod";
2
- import * as U from "zod";
3
- import x, { AxiosError as S } from "axios";
4
- import { AxiosError as K } from "axios";
5
- import { nextTick as F, ref as m, onMounted as N, watch as T, onBeforeUnmount as Z } from "vue";
6
- import { debounce as $ } from "lodash-es";
7
- function z(s) {
8
- const h = x.create({
9
- baseURL: s.baseURL,
1
+ import { ZodError as S } from "zod";
2
+ import * as $ from "zod";
3
+ import L, { AxiosError as F } from "axios";
4
+ import { AxiosError as G } from "axios";
5
+ import { nextTick as N, ref as m, onMounted as T, watch as k, onBeforeUnmount as x } from "vue";
6
+ import { debounce as U } from "lodash-es";
7
+ function z(t) {
8
+ const p = L.create({
9
+ baseURL: t.baseURL,
10
10
  headers: {
11
11
  "Content-Type": "application/json",
12
12
  Accept: "application/json",
13
- ...s.headers
13
+ ...t.headers
14
14
  },
15
- withCredentials: s.withCredentials ?? !1
15
+ withCredentials: t.withCredentials ?? !1
16
16
  });
17
- let b = !1, q = null;
18
- s.onBeforeRequest && h.interceptors.request.use(
17
+ let C = !1, q = null;
18
+ t.onBeforeRequest && p.interceptors.request.use(
19
19
  async (e) => {
20
20
  try {
21
- return await s.onBeforeRequest(e) || e;
21
+ return await t.onBeforeRequest(e) || e;
22
22
  } catch (r) {
23
23
  return Promise.reject(r);
24
24
  }
25
25
  },
26
26
  (e) => Promise.reject(e)
27
- ), s.onStartRequest && h.interceptors.request.use(
27
+ ), t.onStartRequest && p.interceptors.request.use(
28
28
  async (e) => {
29
29
  try {
30
- return await s.onStartRequest(), e;
30
+ return await t.onStartRequest(), e;
31
31
  } catch (r) {
32
32
  return Promise.reject(r);
33
33
  }
34
34
  },
35
35
  (e) => Promise.reject(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) => {
36
+ ), t.onFinishRequest && p.interceptors.response.use(
37
+ (e) => (t.onFinishRequest(), e),
38
+ (e) => (t.onFinishRequest(), Promise.reject(e))
39
+ ), p.interceptors.request.use((e) => {
40
40
  if (!e.url) return e;
41
41
  const r = (a) => {
42
42
  if (a)
43
- for (const [t, v] of Object.entries(a)) {
44
- const l = `{${t}}`;
43
+ for (const [s, R] of Object.entries(a)) {
44
+ const l = `{${s}}`;
45
45
  e.url.includes(l) && (e.url = e.url.replace(
46
46
  l,
47
- encodeURIComponent(String(v))
48
- ), delete a[t]);
47
+ encodeURIComponent(String(R))
48
+ ), delete a[s]);
49
49
  }
50
50
  };
51
51
  return e.method !== "get" && e.data?.params && r(e.data.params), r(e.params), e;
52
- }), s.csrfRefreshEndpoint && h.interceptors.response.use(
52
+ }), t.withCredentials && p.interceptors.request.use((e) => {
53
+ const r = document.cookie.split("; ").find((a) => a.startsWith("XSRF-TOKEN="))?.split("=")[1];
54
+ return r && (e.headers["X-XSRF-TOKEN"] = decodeURIComponent(r)), e;
55
+ }), t.csrfRefreshEndpoint && p.interceptors.response.use(
53
56
  (e) => e,
54
57
  async (e) => {
55
58
  const r = e.config;
56
- if (r?.url === s.csrfRefreshEndpoint)
59
+ if (r?.url === t.csrfRefreshEndpoint)
57
60
  return Promise.reject(e);
58
61
  if (e.response && (e.response.status === 403 || e.response.status === 419) && !r?._retry) {
59
62
  r._retry = !0;
60
63
  try {
61
- return b && q ? await q : (b = !0, q = h.get(s.csrfRefreshEndpoint).then(() => {
62
- b = !1, q = null;
63
- }), await q), h.request(r);
64
+ return C && q ? await q : (C = !0, q = p.get(t.csrfRefreshEndpoint).then(() => {
65
+ C = !1, q = null;
66
+ }), await q), p.request(r);
64
67
  } catch (a) {
65
- return b = !1, q = null, Promise.reject(a);
68
+ return C = !1, q = null, Promise.reject(a);
66
69
  }
67
70
  }
68
71
  return Promise.reject(e);
69
72
  }
70
- ), h.interceptors.response.use(
73
+ ), p.interceptors.response.use(
71
74
  (e) => e,
72
- (e) => (F(() => {
75
+ (e) => (N(() => {
73
76
  e.code;
74
77
  }), Promise.reject(e))
75
78
  );
76
- const C = s.queries ?? {}, M = {};
77
- for (const e in C) {
78
- const r = C[e];
79
+ const P = t.queries ?? {}, M = {};
80
+ for (const e in P) {
81
+ const r = P[e];
79
82
  r && (M[e] = (a) => {
80
- let t;
81
- a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ? t = a : t = { params: a });
82
- const v = m(), l = m(), E = m(), g = m(!1), y = m(!1), P = m(!0);
83
+ let s;
84
+ a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ? s = a : s = { params: a });
85
+ const R = m(), l = m(), v = m(), g = m(!1), y = m(!1), A = m(!0);
83
86
  let j = new AbortController();
84
87
  const u = () => {
85
88
  j?.abort(), j = new AbortController();
86
89
  }, c = async () => {
87
90
  g.value && u(), g.value = !0, l.value = void 0;
88
91
  try {
89
- r.params && t?.params && r.params.parse(t.params);
90
- let o = t?.data;
92
+ r.params && s?.params && r.params.parse(s.params);
93
+ let o = s?.data;
91
94
  r.data && o && r.data.parse(o);
92
95
  const f = {
93
96
  method: r.method ?? "GET",
94
97
  url: r.path,
95
- params: t?.params,
98
+ params: s?.params,
96
99
  signal: j.signal
97
100
  };
98
101
  r.method === "POST" && o && (f.data = o);
99
- const n = await h.request(f), i = r.response ? r.response.parse(n.data) : n.data;
100
- v.value = i, t?.onResult?.(i);
102
+ const n = await p.request(f), i = r.response ? r.response.parse(n.data) : n.data;
103
+ R.value = i, s?.onResult?.(i);
101
104
  } catch (o) {
102
- if (o instanceof S) {
105
+ if (o instanceof F) {
103
106
  if (o.code !== "ERR_CANCELED") {
104
- const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code, p = o.response?.data;
105
- l.value = f, t?.onError?.(o), s.onErrorRequest?.({ message: f, status: n, code: i, data: p });
107
+ const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code, h = o.response?.data;
108
+ l.value = f, s?.onError?.(o), t.onErrorRequest?.({ message: f, status: n, code: i, data: h });
106
109
  }
107
- } else if (o instanceof L) {
108
- E.value = o.issues || [];
109
- const n = `Validation error: ${E.value.map(
110
+ } else if (o instanceof S) {
111
+ v.value = o.issues || [];
112
+ const n = `Validation error: ${v.value.map(
110
113
  (i) => `${i.path.join(".")}: ${i.message}`
111
114
  ).join(", ")}`;
112
- l.value = n, t?.onError?.(o), t?.onZodError?.(E.value), s.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), s.onZodError && s.onZodError(E.value);
115
+ l.value = n, s?.onError?.(o), s?.onZodError?.(v.value), t.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), t.onZodError && t.onZodError(v.value);
113
116
  } else {
114
117
  const f = o.message || "An error occurred";
115
- l.value = f, t?.onError?.(f), s.onErrorRequest?.({ message: f });
118
+ l.value = f, s?.onError?.(f), t.onErrorRequest?.({ message: f });
116
119
  }
117
120
  } finally {
118
121
  g.value = !1, y.value = !0;
119
122
  }
120
- }, R = t?.debounce ? $(c, t.debounce) : c;
123
+ }, E = s?.debounce ? U(c, s.debounce) : c;
121
124
  let d = null;
122
- return (t?.params || t?.data) && (N(() => {
123
- d && d(), d = T(
124
- () => JSON.stringify({ params: t.params, data: t.data }),
125
+ return (s?.params || s?.data) && (T(() => {
126
+ d && d(), d = k(
127
+ () => JSON.stringify({ params: s.params, data: s.data }),
125
128
  () => {
126
- R();
129
+ E();
127
130
  },
128
131
  { immediate: !1 }
129
132
  );
130
- }), Z(() => {
133
+ }), x(() => {
131
134
  d && d(), j?.abort();
132
- })), (t?.loadOnMount === void 0 || t.loadOnMount) && !y.value && (P.value ? (P.value = !1, c()) : R()), { result: v, errorMessage: l, zodErrors: E, isLoading: g, isDone: y, refetch: c };
135
+ })), (s?.loadOnMount === void 0 || s.loadOnMount) && !y.value && (A.value ? (A.value = !1, c()) : E()), { result: R, errorMessage: l, zodErrors: v, isLoading: g, isDone: y, refetch: c };
133
136
  });
134
137
  }
135
- const D = s.mutations ?? {}, w = {};
138
+ const D = t.mutations ?? {}, w = {};
136
139
  for (const e in D) {
137
140
  const r = D[e];
138
141
  r && (w[e] = (a) => {
139
- const t = m(), v = m(), l = m(), E = m(!1), g = m(!1), y = m(0);
140
- return { result: t, errorMessage: v, zodErrors: l, isLoading: E, isDone: g, uploadProgress: y, mutate: async (j) => {
141
- if (!E.value) {
142
- E.value = !0, v.value = void 0, y.value = 0;
142
+ const s = m(), R = m(), l = m(), v = m(!1), g = m(!1), y = m(0);
143
+ return { result: s, errorMessage: R, zodErrors: l, isLoading: v, isDone: g, uploadProgress: y, mutate: async (j) => {
144
+ if (!v.value) {
145
+ v.value = !0, R.value = void 0, y.value = 0;
143
146
  try {
144
147
  const { data: u = {}, params: c } = j ?? {};
145
- let R = u ?? {}, d = {};
148
+ let E = u ?? {}, d = {};
146
149
  if (r.isMultipart) {
147
150
  const n = new FormData();
148
- for (const [i, p] of Object.entries(u))
149
- p instanceof File || p instanceof Blob ? n.append(i, p) : Array.isArray(p) ? p.forEach((A) => {
150
- A instanceof File || A instanceof Blob ? n.append(i, A) : n.append(i, JSON.stringify(A));
151
- }) : typeof p == "object" && p !== null ? n.append(i, JSON.stringify(p)) : n.append(i, String(p));
152
- R = n, d["Content-Type"] = "multipart/form-data";
151
+ for (const [i, h] of Object.entries(u))
152
+ h instanceof File || h instanceof Blob ? n.append(i, h) : Array.isArray(h) ? h.forEach((b) => {
153
+ b instanceof File || b instanceof Blob ? n.append(i, b) : n.append(i, JSON.stringify(b));
154
+ }) : typeof h == "object" && h !== null ? n.append(i, JSON.stringify(h)) : n.append(i, String(h));
155
+ E = n, d["Content-Type"] = "multipart/form-data";
153
156
  } else r.data && r.data.parse(u);
154
157
  r.params && c && r.params.parse(c);
155
- const o = await h.request({
158
+ const o = await p.request({
156
159
  method: r.method,
157
160
  url: r.path,
158
- data: R,
161
+ data: E,
159
162
  params: c,
160
163
  headers: d,
161
164
  onUploadProgress: (n) => {
@@ -165,23 +168,23 @@ function z(s) {
165
168
  }
166
169
  }
167
170
  }), f = r.response ? r.response.parse(o.data) : o.data;
168
- t.value = f, a?.onResult?.(f);
171
+ s.value = f, a?.onResult?.(f);
169
172
  } catch (u) {
170
- if (u instanceof S) {
171
- const c = u.response?.data?.message || u.message || "An error occurred", R = u.response?.status, d = u.code;
172
- v.value = c, a?.onError?.(u), s.onErrorRequest?.({ message: c, status: R, code: d });
173
- } else if (u instanceof L) {
173
+ if (u instanceof F) {
174
+ const c = u.response?.data?.message || u.message || "An error occurred", E = u.response?.status, d = u.code;
175
+ R.value = c, a?.onError?.(u), t.onErrorRequest?.({ message: c, status: E, code: d });
176
+ } else if (u instanceof S) {
174
177
  l.value = u.issues || [];
175
- const R = `Validation error: ${l.value.map(
178
+ const E = `Validation error: ${l.value.map(
176
179
  (d) => `${d.path.join(".")}: ${d.message}`
177
180
  ).join(", ")}`;
178
- v.value = R, a?.onError?.(u), a?.onZodError?.(l.value), s.onErrorRequest?.({ message: R, code: "VALIDATION_ERROR" }), s.onZodError && s.onZodError(l.value);
181
+ R.value = E, a?.onError?.(u), a?.onZodError?.(l.value), t.onErrorRequest?.({ message: E, code: "VALIDATION_ERROR" }), t.onZodError && t.onZodError(l.value);
179
182
  } else {
180
183
  const c = u.message || "An error occurred";
181
- v.value = c, a?.onError?.(u), s.onErrorRequest?.({ message: c });
184
+ R.value = c, a?.onError?.(u), t.onErrorRequest?.({ message: c });
182
185
  }
183
186
  } finally {
184
- E.value = !1, g.value = !0;
187
+ v.value = !1, g.value = !0;
185
188
  }
186
189
  }
187
190
  } };
@@ -192,24 +195,24 @@ function z(s) {
192
195
  mutation: w
193
196
  };
194
197
  }
195
- function V(s) {
196
- return s;
198
+ function V(t) {
199
+ return t;
197
200
  }
198
- function J(s) {
199
- return s;
201
+ function J(t) {
202
+ return t;
200
203
  }
201
- function Q(...s) {
202
- return Object.assign({}, ...s);
204
+ function Q(...t) {
205
+ return Object.assign({}, ...t);
203
206
  }
204
- function W(...s) {
205
- return Object.assign({}, ...s);
207
+ function W(...t) {
208
+ return Object.assign({}, ...t);
206
209
  }
207
210
  export {
208
- K as AxiosError,
211
+ G as AxiosError,
209
212
  z as createApiClient,
210
213
  J as defineMutation,
211
214
  V as defineQuery,
212
215
  W as mergeMutations,
213
216
  Q as mergeQueries,
214
- U as z
217
+ $ as z
215
218
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vue-api-kit",
3
3
  "type": "module",
4
- "version": "1.5.2",
4
+ "version": "1.6.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",