@kawaiininja/fetch 1.0.3 → 1.0.6
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 +172 -0
- package/dist/hooks/useCsrf.d.ts +1 -1
- package/dist/hooks/useCsrf.js +46 -3
- package/dist/hooks/useFetch.js +22 -10
- package/package.json +46 -46
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @kawaiininja/fetch
|
|
2
|
+
|
|
3
|
+
A production-grade, hybrid-native HTTP client designed for the Onyx Framework.
|
|
4
|
+
|
|
5
|
+
**Rating: S-Tier Utility 🏆**
|
|
6
|
+
|
|
7
|
+
This package automates enterprise-grade security, CSRF handling, and platform detection (Web vs. Native Mobile), allowing you to focus on building features rather than handling HTTP boilerplate.
|
|
8
|
+
|
|
9
|
+
## 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **Hybrid Intelligence**: Automatically detects if running on Web or Native Mobile (Capacitor).
|
|
12
|
+
- **Pro Security**:
|
|
13
|
+
- **Native**: Uses `SecureStoragePlugin` for token management.
|
|
14
|
+
- **Web**: Uses `HttpOnly` cookies + Auto-CSRF token rotation.
|
|
15
|
+
- **Smart Retries**: Automatically handles `403 CSRF` errors by fetching a new token and retrying the request.
|
|
16
|
+
- **Optimistic Updates**: Update your UI instantly before the server responds.
|
|
17
|
+
- **Type-Safe**: Full TypeScript support, but works efficiently in JS too.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🛠️ Setup
|
|
22
|
+
|
|
23
|
+
Wrap your application with the `ApiProvider` to configure your base URL and global behavior.
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
// src/App.jsx
|
|
27
|
+
import { ApiProvider } from "@kawaiininja/fetch";
|
|
28
|
+
|
|
29
|
+
const apiConfig = {
|
|
30
|
+
baseUrl: "https://api.myapp.com",
|
|
31
|
+
version: "v1",
|
|
32
|
+
// Optional: Global error handler (e.g., for Toast notifications)
|
|
33
|
+
onError: (msg, status) => console.error(`[API Error ${status}]: ${msg}`),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default function App() {
|
|
37
|
+
return (
|
|
38
|
+
<ApiProvider config={apiConfig}>
|
|
39
|
+
<YourApp />
|
|
40
|
+
</ApiProvider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📖 Usage Patterns (Efficient JS)
|
|
48
|
+
|
|
49
|
+
### 1. Lazy Action (Forms & Buttons)
|
|
50
|
+
|
|
51
|
+
Best for login forms, submitting data, or manual triggers. No need for `useState` or `try/catch` boilerplate.
|
|
52
|
+
|
|
53
|
+
```jsx
|
|
54
|
+
import { useFetch } from "@kawaiininja/fetch";
|
|
55
|
+
|
|
56
|
+
export const LoginForm = () => {
|
|
57
|
+
// Extract helpers directly
|
|
58
|
+
const { post, isLoading, error } = useFetch("/auth/login");
|
|
59
|
+
|
|
60
|
+
const handleLogin = async (e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
const formData = new FormData(e.target);
|
|
63
|
+
const payload = Object.fromEntries(formData);
|
|
64
|
+
|
|
65
|
+
// ✅ Efficient: helper handles JSON stringify, headers, and loading state
|
|
66
|
+
const user = await post(payload);
|
|
67
|
+
|
|
68
|
+
if (user) {
|
|
69
|
+
console.log("Welcome", user.name);
|
|
70
|
+
// Redirect or update context here
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<form onSubmit={handleLogin}>
|
|
76
|
+
{error && <div className="error-banner">{error}</div>}
|
|
77
|
+
|
|
78
|
+
<input name="email" type="email" placeholder="Email" />
|
|
79
|
+
<input name="password" type="password" placeholder="Password" />
|
|
80
|
+
|
|
81
|
+
<button disabled={isLoading}>
|
|
82
|
+
{isLoading ? "Logging in..." : "Log In"}
|
|
83
|
+
</button>
|
|
84
|
+
</form>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Optimistic Updates (Instant UX)
|
|
90
|
+
|
|
91
|
+
Make your app feel zero-latency by updating the local data _before_ the server responds.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
const UserProfile = () => {
|
|
95
|
+
// 'data' is your state, 'setData' lets you modify it instantly
|
|
96
|
+
const { data, setData, patch } = useFetch("/user/me");
|
|
97
|
+
|
|
98
|
+
const updateName = async (newName) => {
|
|
99
|
+
// 1. Instant UI update (Optimistic)
|
|
100
|
+
// We assume it will succeed to make it feel snappy
|
|
101
|
+
setData((prev) => ({ ...prev, name: newName }));
|
|
102
|
+
|
|
103
|
+
// 2. Send request in background
|
|
104
|
+
// If it fails, the error state will update automatically and revert (if you handle it)
|
|
105
|
+
await patch({ name: newName });
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<h1>Hello, {data?.name || "Guest"}</h1>
|
|
111
|
+
<button onClick={() => updateName("Vinay")}>Update Name</button>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Fetch on Load (Page Data)
|
|
118
|
+
|
|
119
|
+
For standard "load data when page opens" behavior, simply pair with `useEffect`.
|
|
120
|
+
|
|
121
|
+
```jsx
|
|
122
|
+
import { useEffect } from "react";
|
|
123
|
+
import { useFetch } from "@kawaiininja/fetch";
|
|
124
|
+
|
|
125
|
+
export const Dashboard = () => {
|
|
126
|
+
// 'get' is the trigger function
|
|
127
|
+
const { data, isLoading, get } = useFetch("/dashboard");
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
// Trigger the fetch when component mounts
|
|
131
|
+
get();
|
|
132
|
+
}, []); // Empty array ensures it only runs once
|
|
133
|
+
|
|
134
|
+
if (isLoading) return <div>Loading...</div>;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div>
|
|
138
|
+
<h1>Dashboard Stats</h1>
|
|
139
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 4. Reactive Fetch (Search & Filters)
|
|
146
|
+
|
|
147
|
+
Trigger a fetch automatically whenever a variable changes (like a filter button or search bar).
|
|
148
|
+
|
|
149
|
+
```jsx
|
|
150
|
+
import { useEffect, useState } from "react";
|
|
151
|
+
import { useFetch } from "@kawaiininja/fetch";
|
|
152
|
+
|
|
153
|
+
export const FilterList = () => {
|
|
154
|
+
const [filter, setFilter] = useState("all");
|
|
155
|
+
const [search, setSearch] = useState("");
|
|
156
|
+
const { data, get } = useFetch("/items");
|
|
157
|
+
|
|
158
|
+
// ✅ The "Watcher" Pattern
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
// Overrides the URL with new params whenever dependencies change
|
|
161
|
+
get({ url: `/items?status=${filter}&q=${search}` });
|
|
162
|
+
}, [filter, search]); // 👈 DEPENDENCY ARRAY controls the trigger
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
<button onClick={() => setFilter("active")}>Active</button>
|
|
167
|
+
<input onChange={(e) => setSearch(e.target.value)} />
|
|
168
|
+
<List items={data} />
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
```
|
package/dist/hooks/useCsrf.d.ts
CHANGED
package/dist/hooks/useCsrf.js
CHANGED
|
@@ -1,27 +1,59 @@
|
|
|
1
|
+
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
|
1
2
|
import { useCallback, useRef } from "react";
|
|
2
3
|
import { INTERNAL_HEADER } from "../context/ApiContext";
|
|
3
4
|
import { useApiConfig } from "./useApiConfig";
|
|
4
5
|
export function useCsrf() {
|
|
5
6
|
const { apiUrl } = useApiConfig();
|
|
6
7
|
const csrfRef = useRef(null);
|
|
7
|
-
const clearCsrf = useCallback(() => {
|
|
8
|
+
const clearCsrf = useCallback(async () => {
|
|
9
|
+
console.log("[useCsrf] Clearing CSRF token...");
|
|
8
10
|
csrfRef.current = null;
|
|
9
11
|
if (typeof window !== "undefined") {
|
|
12
|
+
const platform = window.Capacitor?.getPlatform() || "web";
|
|
13
|
+
const isNative = platform === "ios" || platform === "android";
|
|
14
|
+
if (isNative) {
|
|
15
|
+
try {
|
|
16
|
+
await SecureStoragePlugin.remove({ key: "csrf_token" });
|
|
17
|
+
console.log("[useCsrf] Cleared from SecureStorage.");
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.warn("[useCsrf] Failed to clear from SecureStorage, trying localStorage:", e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
localStorage.removeItem("csrf_token");
|
|
11
24
|
}
|
|
12
25
|
}, []);
|
|
13
26
|
const fetchCSRF = useCallback(async () => {
|
|
14
27
|
if (csrfRef.current)
|
|
15
28
|
return csrfRef.current;
|
|
16
|
-
// Try to get from
|
|
29
|
+
// Try to get from storage first (for Capacitor/mobile)
|
|
17
30
|
if (typeof window !== "undefined") {
|
|
31
|
+
const platform = window.Capacitor?.getPlatform() || "web";
|
|
32
|
+
const isNative = platform === "ios" || platform === "android";
|
|
33
|
+
if (isNative) {
|
|
34
|
+
try {
|
|
35
|
+
const { value } = await SecureStoragePlugin.get({
|
|
36
|
+
key: "csrf_token",
|
|
37
|
+
});
|
|
38
|
+
if (value) {
|
|
39
|
+
console.log("[useCsrf] Retrieved from SecureStorage.");
|
|
40
|
+
csrfRef.current = value;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.warn("[useCsrf] SecureStorage check failed:", e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
18
48
|
const storedCsrf = localStorage.getItem("csrf_token");
|
|
19
49
|
if (storedCsrf) {
|
|
50
|
+
console.log("[useCsrf] Retrieved from localStorage.");
|
|
20
51
|
csrfRef.current = storedCsrf;
|
|
21
52
|
return storedCsrf;
|
|
22
53
|
}
|
|
23
54
|
}
|
|
24
55
|
// Construct CSRF URL using the context's baseUrl
|
|
56
|
+
console.log("[useCsrf] Fetching new CSRF token from server...");
|
|
25
57
|
const csrfUrl = apiUrl("auth/csrf-token");
|
|
26
58
|
const res = await fetch(csrfUrl, {
|
|
27
59
|
method: "GET",
|
|
@@ -33,8 +65,19 @@ export function useCsrf() {
|
|
|
33
65
|
if (!token)
|
|
34
66
|
throw new Error("Missing CSRF token");
|
|
35
67
|
csrfRef.current = token;
|
|
36
|
-
// Store in
|
|
68
|
+
// Store in storage
|
|
37
69
|
if (typeof window !== "undefined") {
|
|
70
|
+
const platform = window.Capacitor?.getPlatform() || "web";
|
|
71
|
+
const isNative = platform === "ios" || platform === "android";
|
|
72
|
+
if (isNative) {
|
|
73
|
+
try {
|
|
74
|
+
await SecureStoragePlugin.set({ key: "csrf_token", value: token });
|
|
75
|
+
console.log("[useCsrf] Stored in SecureStorage.");
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.warn("[useCsrf] SecureStorage set failed, falling back to localStorage:", e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
38
81
|
localStorage.setItem("csrf_token", token);
|
|
39
82
|
}
|
|
40
83
|
return token;
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -51,30 +51,42 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
51
51
|
// 🔒 SECURITY STRATEGY: NATIVE (MOBILE)
|
|
52
52
|
if (isNative && isInternal) {
|
|
53
53
|
// Mobile relies on manual headers ("Active Courier")
|
|
54
|
-
// We DO NOT use CSRF tokens on mobile (Cookies generally don't work reliably here)
|
|
55
54
|
// 🛡️ S-RANK UPGRADE: Use Secure Storage (Async) instead of LocalStorage
|
|
55
|
+
console.log(`[useFetch] [Native] Strategy: Active Courier for ${url}`);
|
|
56
|
+
let authToken = null;
|
|
57
|
+
let sessionId = null;
|
|
56
58
|
try {
|
|
57
|
-
const { value:
|
|
59
|
+
const { value: secureAuthToken } = await SecureStoragePlugin.get({
|
|
58
60
|
key: "token",
|
|
59
|
-
})
|
|
60
|
-
const { value:
|
|
61
|
+
});
|
|
62
|
+
const { value: secureSessionId } = await SecureStoragePlugin.get({
|
|
61
63
|
key: "session_id",
|
|
62
|
-
})
|
|
64
|
+
});
|
|
65
|
+
authToken = secureAuthToken;
|
|
66
|
+
sessionId = secureSessionId;
|
|
63
67
|
if (authToken)
|
|
64
|
-
|
|
65
|
-
if (sessionId)
|
|
66
|
-
headersConfig["X-Session-ID"] = sessionId;
|
|
68
|
+
console.log("[useFetch] [Native] Token retrieved from SecureStorage.");
|
|
67
69
|
}
|
|
68
70
|
catch (err) {
|
|
69
|
-
|
|
71
|
+
console.warn("[useFetch] [Native] SecureStorage failed or not present, trying localStorage:", err);
|
|
72
|
+
authToken = localStorage.getItem("token");
|
|
73
|
+
sessionId = localStorage.getItem("session_id");
|
|
74
|
+
if (authToken)
|
|
75
|
+
console.log("[useFetch] [Native] Token retrieved from localStorage fallback.");
|
|
70
76
|
}
|
|
77
|
+
if (authToken)
|
|
78
|
+
headersConfig["Authorization"] = `Bearer ${authToken}`;
|
|
79
|
+
if (sessionId)
|
|
80
|
+
headersConfig["X-Session-ID"] = sessionId;
|
|
71
81
|
}
|
|
72
82
|
// 🔒 SECURITY STRATEGY: WEB (BROWSER)
|
|
73
83
|
if (!isNative && isInternal) {
|
|
74
84
|
// Web relies on Cookies ("Passive Courier") for Auth
|
|
75
85
|
// BUT we MUST attach the CSRF Token to prevent attacks
|
|
76
|
-
if (token)
|
|
86
|
+
if (token) {
|
|
77
87
|
headersConfig["X-CSRF-Token"] = token;
|
|
88
|
+
// console.log("[useFetch] [Web] CSRF token attached.");
|
|
89
|
+
}
|
|
78
90
|
// On Web, we DO NOT attach 'Authorization' or 'Session-ID' from localStorage
|
|
79
91
|
// This minimizes XSS risk. The HttpOnly cookie handles it.
|
|
80
92
|
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kawaiininja/fetch",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Core fetch utility for Onyx Framework",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"module": "dist/index.js",
|
|
8
|
-
"type": "module",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": "./dist/index.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"react",
|
|
21
|
-
"fetch",
|
|
22
|
-
"onyx",
|
|
23
|
-
"kawaiininja",
|
|
24
|
-
"http"
|
|
25
|
-
],
|
|
26
|
-
"author": "
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"peerDependencies": {
|
|
29
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
30
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
31
|
-
},
|
|
32
|
-
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
34
|
-
},
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@types/react": "^19.2.7",
|
|
37
|
-
"@types/react-dom": "^19.2.3",
|
|
38
|
-
"typescript": "^5.7.0"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@capacitor-community/security-provider": "^7.0.0",
|
|
42
|
-
"@capacitor-community/text-to-speech": "^6.1.0",
|
|
43
|
-
"@capacitor/core": "^8.0.1",
|
|
44
|
-
"capacitor-secure-storage-plugin": "^0.13.0"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@kawaiininja/fetch",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Core fetch utility for Onyx Framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react",
|
|
21
|
+
"fetch",
|
|
22
|
+
"onyx",
|
|
23
|
+
"kawaiininja",
|
|
24
|
+
"http"
|
|
25
|
+
],
|
|
26
|
+
"author": "Tristan (4kawaiininja)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
30
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.2.7",
|
|
37
|
+
"@types/react-dom": "^19.2.3",
|
|
38
|
+
"typescript": "^5.7.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@capacitor-community/security-provider": "^7.0.0",
|
|
42
|
+
"@capacitor-community/text-to-speech": "^6.1.0",
|
|
43
|
+
"@capacitor/core": "^8.0.1",
|
|
44
|
+
"capacitor-secure-storage-plugin": "^0.13.0"
|
|
45
|
+
}
|
|
46
|
+
}
|