@kawaiininja/fetch 1.0.17 โ 1.0.18
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/useFetch.d.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import { BaseFetchOptions } from "./types";
|
|
2
1
|
import { ApiSurface } from "./utils";
|
|
3
|
-
|
|
4
|
-
* ๐ฅ SECURITY UPGRADE: Clear Vault
|
|
5
|
-
* Call this on logout to ensure tokens are purged from memory.
|
|
6
|
-
*/
|
|
7
|
-
export declare const clearNativeAuthVault: (debug?: boolean) => void;
|
|
2
|
+
export { clearNativeAuthVault } from "./useFetch.vault";
|
|
8
3
|
/**
|
|
9
4
|
* useFetch Hook
|
|
10
|
-
*
|
|
11
|
-
* A comprehensive hook for data fetching with automatic CSRF handling,
|
|
12
|
-
* request cancellation, and a rich API surface.
|
|
5
|
+
* ๐ก๏ธ STABILITY UPGRADE: Internal Recursion Guard + Optimized Lifecycle
|
|
13
6
|
*/
|
|
14
|
-
export declare const useFetch: <T = any>(endpoint: string, baseOptions?:
|
|
7
|
+
export declare const useFetch: <T = any>(endpoint: string, baseOptions?: any) => ApiSurface<T>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ก๏ธ CORE FETCH EXECUTOR
|
|
3
|
+
* Handles the actual network request with retries and timeout.
|
|
4
|
+
*/
|
|
5
|
+
export declare const performFetch: (url: string, method: string, token: string, body?: BodyInit, headers?: HeadersInit, rest?: RequestInit, debug?: boolean, abortSignal?: AbortSignal, apiUrl?: (path: string) => string) => Promise<Response>;
|
|
6
|
+
export declare const parseResponse: (res: Response, parseAs: string) => Promise<any>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { INTERNAL_HEADER } from "../context/ApiContext";
|
|
2
|
+
import { isNative } from "./platform";
|
|
3
|
+
import { initializeVault, nativeAuthVault } from "./useFetch.vault";
|
|
4
|
+
/**
|
|
5
|
+
* ๐ก๏ธ CORE FETCH EXECUTOR
|
|
6
|
+
* Handles the actual network request with retries and timeout.
|
|
7
|
+
*/
|
|
8
|
+
export const performFetch = async (url, method, token, body, headers, rest, debug, abortSignal, apiUrl) => {
|
|
9
|
+
const _isNative = isNative();
|
|
10
|
+
const isInternal = url.startsWith("/") || (apiUrl && url.startsWith(apiUrl(""))) || false;
|
|
11
|
+
const headersConfig = {
|
|
12
|
+
...(headers || {}),
|
|
13
|
+
...INTERNAL_HEADER,
|
|
14
|
+
};
|
|
15
|
+
// ๐ NATIVE SECURITY
|
|
16
|
+
if (_isNative && isInternal) {
|
|
17
|
+
await initializeVault(debug);
|
|
18
|
+
const authToken = nativeAuthVault.token;
|
|
19
|
+
const sessionId = nativeAuthVault.sessionId;
|
|
20
|
+
if (authToken)
|
|
21
|
+
headersConfig["Authorization"] = `Bearer ${authToken}`;
|
|
22
|
+
if (sessionId)
|
|
23
|
+
headersConfig["X-Session-ID"] = sessionId;
|
|
24
|
+
}
|
|
25
|
+
// ๐ WEB SECURITY
|
|
26
|
+
if (!_isNative && isInternal && token) {
|
|
27
|
+
headersConfig["X-CSRF-Token"] = token;
|
|
28
|
+
}
|
|
29
|
+
return executeWithRetry(url, method, headersConfig, body, rest, debug, abortSignal);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Executes a fetch request with 3 retry attempts.
|
|
33
|
+
*/
|
|
34
|
+
async function executeWithRetry(url, method, headers, body, rest, debug, abortSignal) {
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
const maxAttempts = 3;
|
|
37
|
+
let lastError = null;
|
|
38
|
+
while (attempt < maxAttempts) {
|
|
39
|
+
attempt++;
|
|
40
|
+
const TIMEOUT_MS = 15000;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
+
// Link to external abort signal if provided
|
|
44
|
+
const onAbort = () => controller.abort();
|
|
45
|
+
if (abortSignal)
|
|
46
|
+
abortSignal.addEventListener("abort", onAbort);
|
|
47
|
+
try {
|
|
48
|
+
const response = await Promise.race([
|
|
49
|
+
fetch(url, { ...rest, method, signal: controller.signal, credentials: "include", headers, body }),
|
|
50
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout of ${TIMEOUT_MS}ms exceeded`)), TIMEOUT_MS)),
|
|
51
|
+
]);
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
if (abortSignal)
|
|
54
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
55
|
+
return response;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
clearTimeout(timeoutId);
|
|
59
|
+
if (abortSignal)
|
|
60
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
61
|
+
lastError = error;
|
|
62
|
+
if (error.name === "AbortError" || abortSignal?.aborted)
|
|
63
|
+
throw error;
|
|
64
|
+
if (debug)
|
|
65
|
+
console.warn(`[useFetch] Attempt ${attempt} failed: ${error.message}.`);
|
|
66
|
+
if (attempt < maxAttempts)
|
|
67
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw lastError || new Error("Request failed after 3 attempts");
|
|
71
|
+
}
|
|
72
|
+
export const parseResponse = async (res, parseAs) => {
|
|
73
|
+
const type = res.headers.get("content-type") || "";
|
|
74
|
+
if (parseAs === "json" || (parseAs === "auto" && type.includes("application/json"))) {
|
|
75
|
+
return res.json();
|
|
76
|
+
}
|
|
77
|
+
else if (parseAs === "text") {
|
|
78
|
+
return res.text();
|
|
79
|
+
}
|
|
80
|
+
else if (parseAs === "blob") {
|
|
81
|
+
return res.blob();
|
|
82
|
+
}
|
|
83
|
+
return res.text();
|
|
84
|
+
};
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -1,295 +1,89 @@
|
|
|
1
|
-
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
|
2
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { INTERNAL_HEADER } from "../context/ApiContext";
|
|
4
|
-
import { isNative } from "./platform";
|
|
5
2
|
import { useApiConfig } from "./useApiConfig";
|
|
6
3
|
import { useCsrf } from "./useCsrf";
|
|
4
|
+
import { parseResponse, performFetch } from "./useFetch.executor";
|
|
7
5
|
import { createApiMethods } from "./utils";
|
|
8
|
-
|
|
9
|
-
* ๐ก๏ธ SESSION MEMORY VAULT
|
|
10
|
-
* Caches native tokens in memory for the lifetime of the JS process.
|
|
11
|
-
* This avoids expensive SecureStorage calls on every request.
|
|
12
|
-
*/
|
|
13
|
-
const nativeAuthVault = {
|
|
14
|
-
token: undefined,
|
|
15
|
-
sessionId: undefined,
|
|
16
|
-
loaded: false,
|
|
17
|
-
/**
|
|
18
|
-
* ๐ก๏ธ RECURSION GUARD: Initialization Promise
|
|
19
|
-
* Prevents multiple concurrent requests from triggering separate
|
|
20
|
-
* SecureStorage calls. All requests will await the same first check.
|
|
21
|
-
*/
|
|
22
|
-
initPromise: null,
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* ๐ฅ SECURITY UPGRADE: Clear Vault
|
|
26
|
-
* Call this on logout to ensure tokens are purged from memory.
|
|
27
|
-
*/
|
|
28
|
-
export const clearNativeAuthVault = (debug) => {
|
|
29
|
-
nativeAuthVault.loaded = false;
|
|
30
|
-
nativeAuthVault.token = undefined;
|
|
31
|
-
nativeAuthVault.sessionId = undefined;
|
|
32
|
-
if (debug)
|
|
33
|
-
console.log("[useFetch] [Native] Auth vault cleared.");
|
|
34
|
-
};
|
|
6
|
+
export { clearNativeAuthVault } from "./useFetch.vault";
|
|
35
7
|
/**
|
|
36
8
|
* useFetch Hook
|
|
37
|
-
*
|
|
38
|
-
* A comprehensive hook for data fetching with automatic CSRF handling,
|
|
39
|
-
* request cancellation, and a rich API surface.
|
|
9
|
+
* ๐ก๏ธ STABILITY UPGRADE: Internal Recursion Guard + Optimized Lifecycle
|
|
40
10
|
*/
|
|
41
11
|
export const useFetch = (endpoint, baseOptions = {}) => {
|
|
42
12
|
const { apiUrl, onError, debug } = useApiConfig();
|
|
43
13
|
const { fetchCSRF, clearCsrf } = useCsrf();
|
|
44
14
|
const runsRef = useRef(0);
|
|
45
|
-
const resolvedUrl = endpoint.startsWith("http") ? endpoint : apiUrl(endpoint);
|
|
46
15
|
const abortRef = useRef(new AbortController());
|
|
47
16
|
const mounted = useRef(true);
|
|
48
|
-
const
|
|
17
|
+
const isRequestingRef = useRef(false);
|
|
49
18
|
const [state, setState] = useState({
|
|
50
19
|
data: null,
|
|
51
20
|
loading: false,
|
|
52
21
|
error: null,
|
|
53
22
|
status: null,
|
|
54
23
|
});
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
optionsRef.current = baseOptions;
|
|
57
|
-
}, [baseOptions]);
|
|
58
24
|
const safeSet = useCallback((fn) => {
|
|
59
|
-
if (mounted.current)
|
|
25
|
+
if (mounted.current)
|
|
60
26
|
setState((prev) => ({ ...prev, ...fn(prev) }));
|
|
61
|
-
}
|
|
62
27
|
}, []);
|
|
63
|
-
const performFetch = async (url, method, token, body, headers, rest) => {
|
|
64
|
-
// ๐ PLATFORM DETECTION
|
|
65
|
-
const _isNative = isNative();
|
|
66
|
-
// ๐ฏ TARGET CHECK (Prevent Credential Leakage)
|
|
67
|
-
// Only attach sensitive headers if sending to our own API
|
|
68
|
-
const isInternal = url.startsWith("/") ||
|
|
69
|
-
(apiUrl("") && url.startsWith(apiUrl(""))) ||
|
|
70
|
-
false;
|
|
71
|
-
const headersConfig = {
|
|
72
|
-
...(optionsRef.current.headers || {}),
|
|
73
|
-
...(headers || {}),
|
|
74
|
-
...INTERNAL_HEADER,
|
|
75
|
-
};
|
|
76
|
-
// ๐ SECURITY STRATEGY: NATIVE (MOBILE)
|
|
77
|
-
if (_isNative && isInternal) {
|
|
78
|
-
if (!nativeAuthVault.loaded) {
|
|
79
|
-
if (debug)
|
|
80
|
-
console.log("[useFetch] [Native] Vault not loaded, initializing...");
|
|
81
|
-
if (!nativeAuthVault.initPromise) {
|
|
82
|
-
nativeAuthVault.initPromise = (async () => {
|
|
83
|
-
try {
|
|
84
|
-
if (debug)
|
|
85
|
-
console.log("[useFetch] [Native] Reading SecureStorage...");
|
|
86
|
-
const { value: t } = await SecureStoragePlugin.get({
|
|
87
|
-
key: "token",
|
|
88
|
-
});
|
|
89
|
-
const { value: s } = await SecureStoragePlugin.get({
|
|
90
|
-
key: "session_id",
|
|
91
|
-
});
|
|
92
|
-
nativeAuthVault.token = t || undefined;
|
|
93
|
-
nativeAuthVault.sessionId = s || undefined;
|
|
94
|
-
if (!nativeAuthVault.token) {
|
|
95
|
-
if (debug)
|
|
96
|
-
console.log("[useFetch] [Native] SecureStorage empty, trying localStorage fallback");
|
|
97
|
-
nativeAuthVault.token =
|
|
98
|
-
localStorage.getItem("token") || undefined;
|
|
99
|
-
nativeAuthVault.sessionId =
|
|
100
|
-
localStorage.getItem("session_id") || undefined;
|
|
101
|
-
}
|
|
102
|
-
nativeAuthVault.loaded = true;
|
|
103
|
-
if (debug)
|
|
104
|
-
console.log("[useFetch] [Native] Auth vault initialized.");
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
if (debug)
|
|
108
|
-
console.warn("[useFetch] [Native] SecureStorage failed, falling back to localStorage:", err);
|
|
109
|
-
nativeAuthVault.token =
|
|
110
|
-
localStorage.getItem("token") || undefined;
|
|
111
|
-
nativeAuthVault.sessionId =
|
|
112
|
-
localStorage.getItem("session_id") || undefined;
|
|
113
|
-
nativeAuthVault.loaded = true;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
nativeAuthVault.initPromise = null;
|
|
117
|
-
}
|
|
118
|
-
})();
|
|
119
|
-
}
|
|
120
|
-
await nativeAuthVault.initPromise;
|
|
121
|
-
if (debug)
|
|
122
|
-
console.log("[useFetch] [Native] Vault ready.");
|
|
123
|
-
}
|
|
124
|
-
const authToken = nativeAuthVault.token;
|
|
125
|
-
const sessionId = nativeAuthVault.sessionId;
|
|
126
|
-
if (authToken)
|
|
127
|
-
headersConfig["Authorization"] = `Bearer ${authToken}`;
|
|
128
|
-
if (sessionId)
|
|
129
|
-
headersConfig["X-Session-ID"] = sessionId;
|
|
130
|
-
}
|
|
131
|
-
// ๐ SECURITY STRATEGY: WEB (BROWSER)
|
|
132
|
-
if (!_isNative && isInternal) {
|
|
133
|
-
// Web relies on Cookies ("Passive Courier") for Auth
|
|
134
|
-
if (token) {
|
|
135
|
-
headersConfig["X-CSRF-Token"] = token;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// ๐ก๏ธ RETRY LOGIC (3 Attempts)
|
|
139
|
-
// We try 3 times. If all 3 fail, we throw.
|
|
140
|
-
let attempt = 0;
|
|
141
|
-
const maxAttempts = 3;
|
|
142
|
-
let lastError = null;
|
|
143
|
-
while (attempt < maxAttempts) {
|
|
144
|
-
attempt++;
|
|
145
|
-
const TIMEOUT_MS = 15000;
|
|
146
|
-
const controller = new AbortController();
|
|
147
|
-
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
148
|
-
// Link to component lifecycle
|
|
149
|
-
const onUnmount = () => controller.abort();
|
|
150
|
-
abortRef.current.signal.addEventListener("abort", onUnmount);
|
|
151
|
-
try {
|
|
152
|
-
const response = await Promise.race([
|
|
153
|
-
fetch(url, {
|
|
154
|
-
...optionsRef.current,
|
|
155
|
-
...rest,
|
|
156
|
-
method,
|
|
157
|
-
signal: controller.signal,
|
|
158
|
-
credentials: "include",
|
|
159
|
-
headers: headersConfig,
|
|
160
|
-
body,
|
|
161
|
-
}),
|
|
162
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout of ${TIMEOUT_MS}ms exceeded`)), TIMEOUT_MS)),
|
|
163
|
-
]);
|
|
164
|
-
clearTimeout(timeoutId);
|
|
165
|
-
abortRef.current.signal.removeEventListener("abort", onUnmount);
|
|
166
|
-
return response; // Success!
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
clearTimeout(timeoutId);
|
|
170
|
-
abortRef.current.signal.removeEventListener("abort", onUnmount);
|
|
171
|
-
lastError = error;
|
|
172
|
-
// If aborted by user/unmount, DO NOT RETRY
|
|
173
|
-
if (error.name === "AbortError" || abortRef.current.signal.aborted) {
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
if (debug) {
|
|
177
|
-
console.warn(`[useFetch] Attempt ${attempt} failed: ${error.message}. Retrying...`);
|
|
178
|
-
}
|
|
179
|
-
// Wait 1s before retry
|
|
180
|
-
if (attempt < maxAttempts) {
|
|
181
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
throw lastError || new Error("Request failed after 3 attempts");
|
|
186
|
-
};
|
|
187
|
-
const parseResponse = async (res, parseAs) => {
|
|
188
|
-
const type = res.headers.get("content-type") || "";
|
|
189
|
-
if (parseAs === "json" ||
|
|
190
|
-
(parseAs === "auto" && type.includes("application/json"))) {
|
|
191
|
-
return res.json();
|
|
192
|
-
}
|
|
193
|
-
else if (parseAs === "text") {
|
|
194
|
-
return res.text();
|
|
195
|
-
}
|
|
196
|
-
else if (parseAs === "blob") {
|
|
197
|
-
return res.blob();
|
|
198
|
-
}
|
|
199
|
-
return res.text();
|
|
200
|
-
};
|
|
201
|
-
// ๐ก๏ธ RECURSION GUARD
|
|
202
|
-
const isRequestingRef = useRef(false);
|
|
203
|
-
// ๐ STABLE REQUEST METHOD
|
|
204
28
|
const request = useCallback(async (params = {}) => {
|
|
29
|
+
// ๐ก๏ธ RECURSION GUARD: Instant rejection of concurrent loops
|
|
205
30
|
if (isRequestingRef.current) {
|
|
206
31
|
if (debug)
|
|
207
|
-
console.warn("[useFetch] ๐ฅ RECURSION GUARD: Blocking concurrent
|
|
32
|
+
console.warn("[useFetch] ๐ฅ RECURSION GUARD: Blocking concurrent call.");
|
|
208
33
|
return;
|
|
209
34
|
}
|
|
210
35
|
const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (debug) {
|
|
214
|
-
console.group(`[useFetch] Request #${runs}: ${method} ${finalUrl}`);
|
|
215
|
-
console.log(`[useFetch] Config:`, {
|
|
216
|
-
isNative: isNative(),
|
|
217
|
-
parseAs,
|
|
218
|
-
endpoint,
|
|
219
|
-
});
|
|
220
|
-
console.groupEnd();
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
console.log(`[useFetch] Request initiated: ${method} ${finalUrl}`);
|
|
224
|
-
}
|
|
225
|
-
if (!finalUrl) {
|
|
226
|
-
console.error("[useFetch] Aborted: Empty URL");
|
|
36
|
+
const finalUrl = url.startsWith("http") ? url : apiUrl(url);
|
|
37
|
+
if (!finalUrl)
|
|
227
38
|
return;
|
|
228
|
-
}
|
|
229
39
|
isRequestingRef.current = true;
|
|
230
40
|
safeSet(() => ({ loading: true, error: null }));
|
|
231
41
|
let lastStatus = null;
|
|
232
42
|
try {
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
let res = await performFetch(finalUrl, method, csrfToken, body, headers, rest);
|
|
43
|
+
let token = await fetchCSRF();
|
|
44
|
+
let res = await performFetch(finalUrl, method, token, body, headers, { ...baseOptions, ...rest }, debug, abortRef.current.signal, apiUrl);
|
|
236
45
|
lastStatus = res.status;
|
|
237
|
-
//
|
|
46
|
+
// Auto-retry on 403 CSRF Error
|
|
238
47
|
if (res.status === 403) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
catch (e) {
|
|
250
|
-
/* ignore parse error */
|
|
48
|
+
const bodyClone = await res
|
|
49
|
+
.clone()
|
|
50
|
+
.json()
|
|
51
|
+
.catch(() => ({}));
|
|
52
|
+
if (bodyClone?.code === "CSRF_ERROR" || bodyClone?.message?.includes("CSRF")) {
|
|
53
|
+
await clearCsrf();
|
|
54
|
+
token = await fetchCSRF();
|
|
55
|
+
res = await performFetch(finalUrl, method, token, body, headers, { ...baseOptions, ...rest }, debug, abortRef.current.signal, apiUrl);
|
|
56
|
+
lastStatus = res.status;
|
|
251
57
|
}
|
|
252
58
|
}
|
|
253
59
|
const parsed = await parseResponse(res, parseAs);
|
|
254
|
-
if (!res.ok)
|
|
60
|
+
if (!res.ok)
|
|
255
61
|
throw new Error(parsed?.message || res.statusText);
|
|
256
|
-
}
|
|
257
62
|
safeSet(() => ({ data: parsed, status: res.status }));
|
|
258
63
|
return parsed;
|
|
259
64
|
}
|
|
260
65
|
catch (err) {
|
|
261
66
|
if (err.name !== "AbortError") {
|
|
262
|
-
console.error("[useFetch] Request Failed:", err);
|
|
263
67
|
safeSet(() => ({ error: err.message, status: lastStatus }));
|
|
264
68
|
if (onError)
|
|
265
69
|
onError(err.message, lastStatus);
|
|
266
70
|
throw err;
|
|
267
71
|
}
|
|
268
|
-
else {
|
|
269
|
-
// Silently ignore component unmount aborts
|
|
270
|
-
if (debug)
|
|
271
|
-
console.log("[useFetch] Request aborted (component unmounted).");
|
|
272
|
-
}
|
|
273
72
|
}
|
|
274
73
|
finally {
|
|
275
74
|
safeSet(() => ({ loading: false }));
|
|
276
75
|
isRequestingRef.current = false;
|
|
277
76
|
}
|
|
278
|
-
}, [endpoint, fetchCSRF, clearCsrf, apiUrl, safeSet, onError, debug]);
|
|
279
|
-
// ๐งน LIFECYCLE
|
|
77
|
+
}, [endpoint, fetchCSRF, clearCsrf, apiUrl, safeSet, onError, debug, baseOptions]);
|
|
280
78
|
useEffect(() => {
|
|
281
79
|
mounted.current = true;
|
|
282
|
-
if (debug) {
|
|
283
|
-
console.log(`[useFetch] [Init] Hook initialized for: ${endpoint}`);
|
|
284
|
-
}
|
|
285
80
|
return () => {
|
|
286
81
|
mounted.current = false;
|
|
287
82
|
abortRef.current?.abort();
|
|
288
83
|
};
|
|
289
|
-
}, [
|
|
290
|
-
|
|
84
|
+
}, []);
|
|
85
|
+
const resolvedUrl = useMemo(() => (endpoint.startsWith("http") ? endpoint : apiUrl(endpoint)), [endpoint, apiUrl]);
|
|
291
86
|
const methods = useMemo(() => createApiMethods({ request, baseUrl: resolvedUrl, safeSet }), [request, resolvedUrl, safeSet]);
|
|
292
|
-
// 2. Merge state with methods
|
|
293
87
|
return useMemo(() => ({
|
|
294
88
|
state,
|
|
295
89
|
isLoading: state.loading,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ก๏ธ SESSION MEMORY VAULT
|
|
3
|
+
* Caches native tokens in memory for the lifetime of the JS process.
|
|
4
|
+
* This avoids expensive SecureStorage calls on every request.
|
|
5
|
+
*/
|
|
6
|
+
export declare const nativeAuthVault: {
|
|
7
|
+
token: string | undefined;
|
|
8
|
+
sessionId: string | undefined;
|
|
9
|
+
loaded: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* ๐ก๏ธ RECURSION GUARD: Initialization Promise
|
|
12
|
+
* Prevents multiple concurrent requests from triggering separate
|
|
13
|
+
* SecureStorage calls. All requests will await the same first check.
|
|
14
|
+
*/
|
|
15
|
+
initPromise: Promise<void> | null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* ๐ฅ SECURITY UPGRADE: Clear Vault
|
|
19
|
+
* Call this on logout to ensure tokens are purged from memory.
|
|
20
|
+
*/
|
|
21
|
+
export declare const clearNativeAuthVault: (debug?: boolean) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the auth vault from SecureStorage or localStorage.
|
|
24
|
+
*/
|
|
25
|
+
export declare const initializeVault: (debug?: boolean) => Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
|
2
|
+
/**
|
|
3
|
+
* ๐ก๏ธ SESSION MEMORY VAULT
|
|
4
|
+
* Caches native tokens in memory for the lifetime of the JS process.
|
|
5
|
+
* This avoids expensive SecureStorage calls on every request.
|
|
6
|
+
*/
|
|
7
|
+
export const nativeAuthVault = {
|
|
8
|
+
token: undefined,
|
|
9
|
+
sessionId: undefined,
|
|
10
|
+
loaded: false,
|
|
11
|
+
/**
|
|
12
|
+
* ๐ก๏ธ RECURSION GUARD: Initialization Promise
|
|
13
|
+
* Prevents multiple concurrent requests from triggering separate
|
|
14
|
+
* SecureStorage calls. All requests will await the same first check.
|
|
15
|
+
*/
|
|
16
|
+
initPromise: null,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* ๐ฅ SECURITY UPGRADE: Clear Vault
|
|
20
|
+
* Call this on logout to ensure tokens are purged from memory.
|
|
21
|
+
*/
|
|
22
|
+
export const clearNativeAuthVault = (debug) => {
|
|
23
|
+
nativeAuthVault.loaded = false;
|
|
24
|
+
nativeAuthVault.token = undefined;
|
|
25
|
+
nativeAuthVault.sessionId = undefined;
|
|
26
|
+
if (debug)
|
|
27
|
+
console.log("[useFetch] [Native] Auth vault cleared.");
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the auth vault from SecureStorage or localStorage.
|
|
31
|
+
*/
|
|
32
|
+
export const initializeVault = async (debug) => {
|
|
33
|
+
if (nativeAuthVault.loaded)
|
|
34
|
+
return;
|
|
35
|
+
if (nativeAuthVault.initPromise)
|
|
36
|
+
return nativeAuthVault.initPromise;
|
|
37
|
+
nativeAuthVault.initPromise = (async () => {
|
|
38
|
+
try {
|
|
39
|
+
if (debug)
|
|
40
|
+
console.log("[useFetch] [Native] Reading SecureStorage...");
|
|
41
|
+
const { value: t } = await SecureStoragePlugin.get({ key: "token" });
|
|
42
|
+
const { value: s } = await SecureStoragePlugin.get({ key: "session_id" });
|
|
43
|
+
nativeAuthVault.token = t || undefined;
|
|
44
|
+
nativeAuthVault.sessionId = s || undefined;
|
|
45
|
+
if (!nativeAuthVault.token) {
|
|
46
|
+
if (debug)
|
|
47
|
+
console.log("[useFetch] [Native] SecureStorage empty, trying localStorage fallback");
|
|
48
|
+
nativeAuthVault.token = localStorage.getItem("token") || undefined;
|
|
49
|
+
nativeAuthVault.sessionId = localStorage.getItem("session_id") || undefined;
|
|
50
|
+
}
|
|
51
|
+
nativeAuthVault.loaded = true;
|
|
52
|
+
if (debug)
|
|
53
|
+
console.log("[useFetch] [Native] Auth vault initialized.");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (debug)
|
|
57
|
+
console.warn("[useFetch] [Native] SecureStorage failed, falling back to localStorage:", err);
|
|
58
|
+
nativeAuthVault.token = localStorage.getItem("token") || undefined;
|
|
59
|
+
nativeAuthVault.sessionId = localStorage.getItem("session_id") || undefined;
|
|
60
|
+
nativeAuthVault.loaded = true;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
nativeAuthVault.initPromise = null;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return nativeAuthVault.initPromise;
|
|
67
|
+
};
|