@kawaiininja/fetch 1.0.13 → 1.0.14
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/hooks/useCsrf.js +43 -22
- package/dist/hooks/useFetch.js +60 -15
- package/package.json +3 -2
package/dist/hooks/useCsrf.js
CHANGED
|
@@ -59,31 +59,52 @@ export function useCsrf() {
|
|
|
59
59
|
// Construct CSRF URL using the context's baseUrl
|
|
60
60
|
console.log("[useCsrf] Fetching new CSRF token from server...");
|
|
61
61
|
const csrfUrl = apiUrl("auth/csrf-token");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
// ⏱️ TIMEOUT HANDLING for CSRF
|
|
63
|
+
// If the server is unreachable (e.g. paused dev tunnel), this MUST fail fast
|
|
64
|
+
const CSRF_TIMEOUT_MS = 15000;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeoutId = setTimeout(() => controller.abort(), CSRF_TIMEOUT_MS);
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(csrfUrl, {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers: INTERNAL_HEADER,
|
|
71
|
+
credentials: "include",
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new Error(`CSRF Fetch failed with status: ${res.status}`);
|
|
77
|
+
}
|
|
78
|
+
const json = await res.json();
|
|
79
|
+
const token = json?.csrfToken;
|
|
80
|
+
if (!token)
|
|
81
|
+
throw new Error("Missing CSRF token in response");
|
|
82
|
+
csrfRef.current = token;
|
|
83
|
+
// Store in storage
|
|
84
|
+
if (typeof window !== "undefined") {
|
|
85
|
+
const _isNative = isNative();
|
|
86
|
+
if (_isNative) {
|
|
87
|
+
try {
|
|
88
|
+
await SecureStoragePlugin.set({ key: "csrf_token", value: token });
|
|
89
|
+
console.log("[useCsrf] Stored in SecureStorage.");
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.warn("[useCsrf] SecureStorage set failed, falling back to localStorage:", e);
|
|
93
|
+
}
|
|
82
94
|
}
|
|
95
|
+
localStorage.setItem("csrf_token", token);
|
|
96
|
+
}
|
|
97
|
+
return token;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
console.error("[useCsrf] CSRF Fetch Failed/Timed-out:", error);
|
|
102
|
+
let failReason = error.message;
|
|
103
|
+
if (error.name === "AbortError") {
|
|
104
|
+
failReason = `Connection Timed Out (${CSRF_TIMEOUT_MS}ms). Server unreachable.`;
|
|
83
105
|
}
|
|
84
|
-
|
|
106
|
+
throw new Error(`CSRF Handshake failed: ${failReason}`);
|
|
85
107
|
}
|
|
86
|
-
return token;
|
|
87
108
|
}, [apiUrl]);
|
|
88
109
|
return useMemo(() => ({ fetchCSRF, clearCsrf }), [fetchCSRF, clearCsrf]);
|
|
89
110
|
}
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -73,14 +73,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
73
73
|
};
|
|
74
74
|
// 🔒 SECURITY STRATEGY: NATIVE (MOBILE)
|
|
75
75
|
if (_isNative && isInternal) {
|
|
76
|
-
// Mobile relies on manual headers ("Active Courier")
|
|
77
|
-
// 🛡️ S-RANK UPGRADE: Use Session Memory Vault (In-Memory Singleton)
|
|
78
|
-
// console.log(`[useFetch] [Native] Strategy: Active Courier for ${url}`);
|
|
79
76
|
if (!nativeAuthVault.loaded) {
|
|
80
|
-
|
|
77
|
+
console.log("[useFetch] [Native] Vault not loaded, initializing...");
|
|
81
78
|
if (!nativeAuthVault.initPromise) {
|
|
82
79
|
nativeAuthVault.initPromise = (async () => {
|
|
83
80
|
try {
|
|
81
|
+
console.log("[useFetch] [Native] Reading SecureStorage...");
|
|
84
82
|
const { value: t } = await SecureStoragePlugin.get({
|
|
85
83
|
key: "token",
|
|
86
84
|
});
|
|
@@ -89,8 +87,8 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
89
87
|
});
|
|
90
88
|
nativeAuthVault.token = t || undefined;
|
|
91
89
|
nativeAuthVault.sessionId = s || undefined;
|
|
92
|
-
// Fallback to localStorage if SecureStorage is empty
|
|
93
90
|
if (!nativeAuthVault.token) {
|
|
91
|
+
console.log("[useFetch] [Native] SecureStorage empty, trying localStorage fallback");
|
|
94
92
|
nativeAuthVault.token =
|
|
95
93
|
localStorage.getItem("token") || undefined;
|
|
96
94
|
nativeAuthVault.sessionId =
|
|
@@ -113,6 +111,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
113
111
|
})();
|
|
114
112
|
}
|
|
115
113
|
await nativeAuthVault.initPromise;
|
|
114
|
+
console.log("[useFetch] [Native] Vault ready.");
|
|
116
115
|
}
|
|
117
116
|
const authToken = nativeAuthVault.token;
|
|
118
117
|
const sessionId = nativeAuthVault.sessionId;
|
|
@@ -128,15 +127,52 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
128
127
|
headersConfig["X-CSRF-Token"] = token;
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
// 🛡️ RETRY LOGIC (3 Attempts)
|
|
131
|
+
// We try 3 times. If all 3 fail, we throw.
|
|
132
|
+
let attempt = 0;
|
|
133
|
+
const maxAttempts = 3;
|
|
134
|
+
let lastError = null;
|
|
135
|
+
while (attempt < maxAttempts) {
|
|
136
|
+
attempt++;
|
|
137
|
+
const TIMEOUT_MS = 15000;
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
140
|
+
// Link to component lifecycle
|
|
141
|
+
const onUnmount = () => controller.abort();
|
|
142
|
+
abortRef.current.signal.addEventListener("abort", onUnmount);
|
|
143
|
+
try {
|
|
144
|
+
const response = await Promise.race([
|
|
145
|
+
fetch(url, {
|
|
146
|
+
...optionsRef.current,
|
|
147
|
+
...rest,
|
|
148
|
+
method,
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
credentials: "include",
|
|
151
|
+
headers: headersConfig,
|
|
152
|
+
body,
|
|
153
|
+
}),
|
|
154
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout of ${TIMEOUT_MS}ms exceeded`)), TIMEOUT_MS)),
|
|
155
|
+
]);
|
|
156
|
+
clearTimeout(timeoutId);
|
|
157
|
+
abortRef.current.signal.removeEventListener("abort", onUnmount);
|
|
158
|
+
return response; // Success!
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
clearTimeout(timeoutId);
|
|
162
|
+
abortRef.current.signal.removeEventListener("abort", onUnmount);
|
|
163
|
+
lastError = error;
|
|
164
|
+
// If aborted by user/unmount, DO NOT RETRY
|
|
165
|
+
if (error.name === "AbortError" || abortRef.current.signal.aborted) {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
console.warn(`[useFetch] Attempt ${attempt} failed: ${error.message}. Retrying...`);
|
|
169
|
+
// Wait 1s before retry
|
|
170
|
+
if (attempt < maxAttempts) {
|
|
171
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw lastError || new Error("Request failed after 3 attempts");
|
|
140
176
|
};
|
|
141
177
|
const parseResponse = async (res, parseAs) => {
|
|
142
178
|
const type = res.headers.get("content-type") || "";
|
|
@@ -156,8 +192,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
156
192
|
const request = useCallback(async (params = {}) => {
|
|
157
193
|
const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
|
|
158
194
|
let finalUrl = url.startsWith("http") ? url : apiUrl(url);
|
|
159
|
-
|
|
195
|
+
console.log(`[useFetch] Request initiated: ${method} ${finalUrl}`);
|
|
196
|
+
console.log(`[useFetch] Config: disableCsrf=${disableCsrf}, isNative=${isNative()}`);
|
|
197
|
+
if (!finalUrl) {
|
|
198
|
+
console.error("[useFetch] Aborted: Empty URL");
|
|
160
199
|
return;
|
|
200
|
+
}
|
|
161
201
|
safeSet(() => ({ loading: true, error: null }));
|
|
162
202
|
let lastStatus = null;
|
|
163
203
|
try {
|
|
@@ -192,11 +232,16 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
192
232
|
}
|
|
193
233
|
catch (err) {
|
|
194
234
|
if (err.name !== "AbortError") {
|
|
235
|
+
console.error("[useFetch] Request Failed:", err);
|
|
195
236
|
safeSet(() => ({ error: err.message, status: lastStatus }));
|
|
196
237
|
if (onError)
|
|
197
238
|
onError(err.message, lastStatus);
|
|
198
239
|
throw err;
|
|
199
240
|
}
|
|
241
|
+
else {
|
|
242
|
+
// Silently ignore component unmount aborts
|
|
243
|
+
console.log("[useFetch] Request aborted (component unmounted).");
|
|
244
|
+
}
|
|
200
245
|
}
|
|
201
246
|
finally {
|
|
202
247
|
safeSet(() => ({ loading: false }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kawaiininja/fetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Core fetch utility for Onyx Framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc"
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"rebuild": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
20
21
|
"react",
|