@karbonjs/api 0.1.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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/client/client.d.ts +19 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +80 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +80 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server/server.d.ts +6 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +53 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/server.test.d.ts +2 -0
- package/dist/server/server.test.d.ts.map +1 -0
- package/dist/server/server.test.js +62 -0
- package/dist/server/server.test.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +53 -0
- package/dist/server.js.map +1 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KarbonJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @karbonjs/api
|
|
2
|
+
|
|
3
|
+
Type-safe API client for Karbon backends. Supports both **server-side** (SSR) and **client-side** with automatic token refresh.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @karbonjs/api @karbonjs/types
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Server-side (SSR)
|
|
12
|
+
|
|
13
|
+
For SvelteKit `load` functions, Next.js `getServerSideProps`, or any Node.js server code.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createServerApi } from '@karbonjs/api/server'
|
|
17
|
+
|
|
18
|
+
const callApi = createServerApi('http://localhost:3005/api/v1')
|
|
19
|
+
|
|
20
|
+
// In a SvelteKit load function
|
|
21
|
+
export async function load() {
|
|
22
|
+
const articles = await callApi('/articles?per_page=10')
|
|
23
|
+
return { articles: articles.data }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// With authentication
|
|
27
|
+
const user = await callApi('/account/profile', {
|
|
28
|
+
token: 'jwt-token-here'
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Client-side
|
|
33
|
+
|
|
34
|
+
For browser-side API calls with automatic 401 retry via refresh tokens.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createClientApi } from '@karbonjs/api'
|
|
38
|
+
|
|
39
|
+
const api = createClientApi({
|
|
40
|
+
baseUrl: '/api/v1',
|
|
41
|
+
getToken: () => localStorage.getItem('token'),
|
|
42
|
+
refreshToken: async () => {
|
|
43
|
+
const res = await fetch('/auth/refresh', { method: 'POST' })
|
|
44
|
+
const data = await res.json()
|
|
45
|
+
localStorage.setItem('token', data.token)
|
|
46
|
+
return data.token
|
|
47
|
+
},
|
|
48
|
+
onAuthFailure: () => {
|
|
49
|
+
window.location.href = '/login'
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Auto-refreshes token on 401
|
|
54
|
+
const articles = await api('/articles')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
await callApi('/articles', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: { title: 'New article' },
|
|
63
|
+
token: 'bearer-token',
|
|
64
|
+
headers: { 'X-Custom': 'value' },
|
|
65
|
+
timeout: 5000, // ms, default 15000
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ApiCallOptions, ApiResult } from '@karbonjs/types';
|
|
2
|
+
export interface ClientApiConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** Return current auth token */
|
|
5
|
+
getToken: () => string | null;
|
|
6
|
+
/** Called to refresh an expired token. Return new token or null. */
|
|
7
|
+
refreshToken?: () => Promise<string | null>;
|
|
8
|
+
/** Called when refresh fails (user should be logged out) */
|
|
9
|
+
onAuthFailure?: () => void;
|
|
10
|
+
/** Default timeout in ms (default: 15000) */
|
|
11
|
+
timeout?: number;
|
|
12
|
+
/** Default headers sent with every request */
|
|
13
|
+
defaultHeaders?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Client-side API helper with automatic token refresh on 401.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createClientApi(config: ClientApiConfig): <T extends ApiResult = ApiResult>(endpoint: string, options?: ApiCallOptions) => Promise<T>;
|
|
19
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC7B,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC3C,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,IAG3B,CAAC,SAAS,SAAS,GAAG,SAAS,EACvD,UAAU,MAAM,EAChB,UAAS,cAAmB,KAC3B,OAAO,CAAC,CAAC,CAAC,CA6Ed"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side API helper with automatic token refresh on 401.
|
|
3
|
+
*/
|
|
4
|
+
export function createClientApi(config) {
|
|
5
|
+
let refreshPromise = null;
|
|
6
|
+
return async function api(endpoint, options = {}) {
|
|
7
|
+
const { method = 'GET', body, headers: extraHeaders, timeout } = options;
|
|
8
|
+
const token = options.token ?? config.getToken();
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
...config.defaultHeaders,
|
|
12
|
+
...extraHeaders,
|
|
13
|
+
};
|
|
14
|
+
if (token) {
|
|
15
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
16
|
+
}
|
|
17
|
+
const doFetch = async (authToken) => {
|
|
18
|
+
const h = { ...headers };
|
|
19
|
+
if (authToken)
|
|
20
|
+
h['Authorization'] = `Bearer ${authToken}`;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const ms = timeout ?? config.timeout ?? 15_000;
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
24
|
+
try {
|
|
25
|
+
return await fetch(`${config.baseUrl}${endpoint}`, {
|
|
26
|
+
method,
|
|
27
|
+
headers: h,
|
|
28
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
let res = await doFetch(token);
|
|
38
|
+
// Auto-refresh on 401
|
|
39
|
+
if (res.status === 401 && config.refreshToken) {
|
|
40
|
+
if (!refreshPromise) {
|
|
41
|
+
refreshPromise = Promise.race([
|
|
42
|
+
config.refreshToken(),
|
|
43
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 10_000)),
|
|
44
|
+
]).finally(() => { refreshPromise = null; });
|
|
45
|
+
}
|
|
46
|
+
const newToken = await refreshPromise;
|
|
47
|
+
if (newToken) {
|
|
48
|
+
res = await doFetch(newToken);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
config.onAuthFailure?.();
|
|
52
|
+
return { ok: false, status: 401, message: 'Authentication failed' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
let message;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(text);
|
|
60
|
+
message = typeof parsed.message === 'string' ? parsed.message.slice(0, 500) : text.slice(0, 500);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
message = text.slice(0, 500);
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, status: res.status, message };
|
|
66
|
+
}
|
|
67
|
+
if (res.status === 204)
|
|
68
|
+
return { ok: true };
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return { ...data, ok: true };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const message = err instanceof DOMException && err.name === 'AbortError'
|
|
74
|
+
? 'Request timeout'
|
|
75
|
+
: 'Network error';
|
|
76
|
+
return { ok: false, status: 0, message };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApiCallOptions, ApiResult } from '@karbonjs/types';
|
|
2
|
+
export interface ClientApiConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** Return current auth token */
|
|
5
|
+
getToken: () => string | null;
|
|
6
|
+
/** Called to refresh an expired token. Return new token or null. */
|
|
7
|
+
refreshToken?: () => Promise<string | null>;
|
|
8
|
+
/** Called when refresh fails (user should be logged out) */
|
|
9
|
+
onAuthFailure?: () => void;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Client-side API helper with automatic token refresh on 401.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const api = createClientApi({
|
|
17
|
+
* baseUrl: '/api/v1',
|
|
18
|
+
* getToken: () => authStore.token,
|
|
19
|
+
* refreshToken: () => authStore.refresh(),
|
|
20
|
+
* onAuthFailure: () => goto('/login'),
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* const data = await api<{ data: Article[] }>('/articles')
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createClientApi(config: ClientApiConfig): <T extends ApiResult = ApiResult>(endpoint: string, options?: ApiCallOptions) => Promise<T>;
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC7B,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC3C,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,IAG3B,CAAC,SAAS,SAAS,GAAG,SAAS,EACvD,UAAU,MAAM,EAChB,UAAS,cAAmB,KAC3B,OAAO,CAAC,CAAC,CAAC,CAmEd"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side API helper with automatic token refresh on 401.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* const api = createClientApi({
|
|
6
|
+
* baseUrl: '/api/v1',
|
|
7
|
+
* getToken: () => authStore.token,
|
|
8
|
+
* refreshToken: () => authStore.refresh(),
|
|
9
|
+
* onAuthFailure: () => goto('/login'),
|
|
10
|
+
* })
|
|
11
|
+
*
|
|
12
|
+
* const data = await api<{ data: Article[] }>('/articles')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function createClientApi(config) {
|
|
16
|
+
let refreshPromise = null;
|
|
17
|
+
return async function api(endpoint, options = {}) {
|
|
18
|
+
const { method = 'GET', body, headers: extraHeaders, timeout } = options;
|
|
19
|
+
const token = options.token ?? config.getToken();
|
|
20
|
+
const headers = {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...extraHeaders,
|
|
23
|
+
};
|
|
24
|
+
if (token) {
|
|
25
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
const doFetch = async (authToken) => {
|
|
28
|
+
const h = { ...headers };
|
|
29
|
+
if (authToken)
|
|
30
|
+
h['Authorization'] = `Bearer ${authToken}`;
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), timeout ?? config.timeout ?? 15_000);
|
|
33
|
+
const res = await fetch(`${config.baseUrl}${endpoint}`, {
|
|
34
|
+
method,
|
|
35
|
+
headers: h,
|
|
36
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
});
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
return res;
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
let res = await doFetch(token);
|
|
44
|
+
// Auto-refresh on 401
|
|
45
|
+
if (res.status === 401 && config.refreshToken) {
|
|
46
|
+
if (!refreshPromise) {
|
|
47
|
+
refreshPromise = config.refreshToken().finally(() => { refreshPromise = null; });
|
|
48
|
+
}
|
|
49
|
+
const newToken = await refreshPromise;
|
|
50
|
+
if (newToken) {
|
|
51
|
+
res = await doFetch(newToken);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
config.onAuthFailure?.();
|
|
55
|
+
return { ok: false, status: 401, message: 'Authentication failed' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
let message;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(text);
|
|
63
|
+
message = parsed.message || text;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
message = text;
|
|
67
|
+
}
|
|
68
|
+
return { ok: false, status: res.status, message };
|
|
69
|
+
}
|
|
70
|
+
if (res.status === 204)
|
|
71
|
+
return { ok: true };
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
return { ok: true, ...data };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return { ok: false, status: 0, message: 'Network error' };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,IAAI,cAAc,GAAkC,IAAI,CAAA;IAExD,OAAO,KAAK,UAAU,GAAG,CACvB,QAAgB,EAChB,UAA0B,EAAE;QAE5B,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;QAExE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAA;QAEhD,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,GAAG,YAAY;SAChB,CAAA;QAED,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAA;QAC9C,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,EAAE,SAAyB,EAAqB,EAAE;YACrE,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAA;YACxB,IAAI,SAAS;gBAAE,CAAC,CAAC,eAAe,CAAC,GAAG,UAAU,SAAS,EAAE,CAAA;YAEzD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;YACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,CAAA;YACvF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,QAAQ,EAAE,EAAE;gBACtD,MAAM;gBACN,OAAO,EAAE,CAAC;gBACV,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC7C,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YACF,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO,GAAG,CAAA;QACZ,CAAC,CAAA;QAED,IAAI,CAAC;YACH,IAAI,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,CAAA;YAE9B,sBAAsB;YACtB,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;gBAC9C,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,cAAc,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,cAAc,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;gBACjF,CAAC;gBAED,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAA;gBACrC,IAAI,QAAQ,EAAE,CAAC;oBACb,GAAG,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAA;gBAC/B,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,aAAa,EAAE,EAAE,CAAA;oBACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,uBAAuB,EAAO,CAAA;gBAC1E,CAAC;YACH,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;gBAC7B,IAAI,OAAe,CAAA;gBACnB,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;oBAC/B,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAI,CAAA;gBAClC,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,GAAG,IAAI,CAAA;gBAChB,CAAC;gBACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAO,CAAA;YACxD,CAAC;YAED,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAO,CAAA;YAChD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAO,CAAA;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,EAAO,CAAA;QAChE,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ApiCallOptions, ApiResult } from '@karbonjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Server-side API client (for SSR / SvelteKit load functions / Next.js getServerSideProps).
|
|
4
|
+
*/
|
|
5
|
+
export declare function createServerApi(baseUrl: string, defaultTimeout?: number): <T extends ApiResult = ApiResult>(endpoint: string, options?: ApiCallOptions) => Promise<T>;
|
|
6
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhE;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,SAAS,IACxC,CAAC,SAAS,SAAS,GAAG,SAAS,EAC3D,UAAU,MAAM,EAChB,UAAS,cAAmB,KAC3B,OAAO,CAAC,CAAC,CAAC,CAiDd"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side API client (for SSR / SvelteKit load functions / Next.js getServerSideProps).
|
|
3
|
+
*/
|
|
4
|
+
export function createServerApi(baseUrl, defaultTimeout = 15_000) {
|
|
5
|
+
return async function callApi(endpoint, options = {}) {
|
|
6
|
+
const { method = 'GET', body, token, headers: extraHeaders, timeout } = options;
|
|
7
|
+
const headers = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
...extraHeaders,
|
|
10
|
+
};
|
|
11
|
+
if (token) {
|
|
12
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const ms = timeout ?? defaultTimeout;
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(`${baseUrl}${endpoint}`, {
|
|
20
|
+
method,
|
|
21
|
+
headers,
|
|
22
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const message = err instanceof DOMException && err.name === 'AbortError'
|
|
28
|
+
? 'Request timeout'
|
|
29
|
+
: 'Service temporarily unavailable';
|
|
30
|
+
return { ok: false, status: 503, message };
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
let message;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(text);
|
|
40
|
+
message = typeof parsed.message === 'string' ? parsed.message.slice(0, 500) : text.slice(0, 500);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
message = text.slice(0, 500);
|
|
44
|
+
}
|
|
45
|
+
return { ok: false, status: res.status, message };
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 204)
|
|
48
|
+
return { ok: true };
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
return { ...data, ok: true };
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,cAAc,GAAG,MAAM;IACtE,OAAO,KAAK,UAAU,OAAO,CAC3B,QAAgB,EAChB,UAA0B,EAAE;QAE5B,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;QAE/E,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,GAAG,YAAY;SAChB,CAAA;QAED,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAA;QAC9C,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,EAAE,GAAG,OAAO,IAAI,cAAc,CAAA;QACpC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;QAEtD,IAAI,GAAa,CAAA;QACjB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,GAAG,QAAQ,EAAE,EAAE;gBACzC,MAAM;gBACN,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC7C,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;gBACtE,CAAC,CAAC,iBAAiB;gBACnB,CAAC,CAAC,iCAAiC,CAAA;YACrC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAO,CAAA;QACjD,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,IAAI,OAAe,CAAA;YACnB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBAC/B,OAAO,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAClG,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAC9B,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAO,CAAA;QACxD,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAO,CAAA;QAChD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QAC7B,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,IAAI,EAAO,CAAA;IACnC,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.test.d.ts","sourceRoot":"","sources":["../../src/server/server.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { createServerApi } from './server';
|
|
3
|
+
describe('createServerApi', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
it('makes GET request with correct URL', async () => {
|
|
8
|
+
const mockResponse = { ok: true, status: 200, json: () => Promise.resolve({ data: [] }), text: () => Promise.resolve('') };
|
|
9
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
|
|
10
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
11
|
+
const result = await api('/articles');
|
|
12
|
+
expect(fetch).toHaveBeenCalledWith('http://localhost:3005/api/v1/articles', expect.objectContaining({ method: 'GET' }));
|
|
13
|
+
expect(result.ok).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('sends Authorization header when token provided', async () => {
|
|
16
|
+
const mockResponse = { ok: true, status: 200, json: () => Promise.resolve({}), text: () => Promise.resolve('') };
|
|
17
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
|
|
18
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
19
|
+
await api('/protected', { token: 'abc123' });
|
|
20
|
+
expect(fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
21
|
+
headers: expect.objectContaining({ Authorization: 'Bearer abc123' })
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
24
|
+
it('handles 204 No Content', async () => {
|
|
25
|
+
const mockResponse = { ok: true, status: 204 };
|
|
26
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
|
|
27
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
28
|
+
const result = await api('/delete-something', { method: 'DELETE' });
|
|
29
|
+
expect(result.ok).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('returns 503 on network error', async () => {
|
|
32
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
33
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
34
|
+
const result = await api('/articles');
|
|
35
|
+
expect(result.ok).toBe(false);
|
|
36
|
+
expect(result.status).toBe(503);
|
|
37
|
+
});
|
|
38
|
+
it('handles error responses with JSON body', async () => {
|
|
39
|
+
const mockResponse = {
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 404,
|
|
42
|
+
text: () => Promise.resolve(JSON.stringify({ message: 'Not found' }))
|
|
43
|
+
};
|
|
44
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
|
|
45
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
46
|
+
const result = await api('/missing');
|
|
47
|
+
expect(result.ok).toBe(false);
|
|
48
|
+
expect(result.status).toBe(404);
|
|
49
|
+
expect(result.message).toBe('Not found');
|
|
50
|
+
});
|
|
51
|
+
it('sends POST body as JSON', async () => {
|
|
52
|
+
const mockResponse = { ok: true, status: 200, json: () => Promise.resolve({ id: 1 }) };
|
|
53
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));
|
|
54
|
+
const api = createServerApi('http://localhost:3005/api/v1');
|
|
55
|
+
await api('/articles', { method: 'POST', body: { title: 'Test' } });
|
|
56
|
+
expect(fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: JSON.stringify({ title: 'Test' })
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=server.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.test.js","sourceRoot":"","sources":["../../src/server/server.test.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApiCallOptions, ApiResult } from '@karbonjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Server-side API client (for SSR / SvelteKit load functions / Next.js getServerSideProps).
|
|
4
|
+
* Uses fetch with AbortController timeout.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const data = await callApi<{ data: Article[] }>('/articles?per_page=10')
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare function createServerApi(baseUrl: string, defaultTimeout?: number): <T extends ApiResult = ApiResult>(endpoint: string, options?: ApiCallOptions) => Promise<T>;
|
|
11
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,SAAS,IACxC,CAAC,SAAS,SAAS,GAAG,SAAS,EAC3D,UAAU,MAAM,EAChB,UAAS,cAAmB,KAC3B,OAAO,CAAC,CAAC,CAAC,CA4Cd"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side API client (for SSR / SvelteKit load functions / Next.js getServerSideProps).
|
|
3
|
+
* Uses fetch with AbortController timeout.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const data = await callApi<{ data: Article[] }>('/articles?per_page=10')
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export function createServerApi(baseUrl, defaultTimeout = 15_000) {
|
|
10
|
+
return async function callApi(endpoint, options = {}) {
|
|
11
|
+
const { method = 'GET', body, token, headers: extraHeaders, timeout } = options;
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
...extraHeaders,
|
|
15
|
+
};
|
|
16
|
+
if (token) {
|
|
17
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
18
|
+
}
|
|
19
|
+
let res;
|
|
20
|
+
try {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), timeout ?? defaultTimeout);
|
|
23
|
+
res = await fetch(`${baseUrl}${endpoint}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
});
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`[karbon-api] Connection failed to ${baseUrl}:`, err);
|
|
33
|
+
return { ok: false, status: 503, message: 'Service temporarily unavailable' };
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
let message;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(text);
|
|
40
|
+
message = parsed.message || text;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
message = text;
|
|
44
|
+
}
|
|
45
|
+
return { ok: false, status: res.status, message };
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 204)
|
|
48
|
+
return { ok: true };
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
return { ok: true, ...data };
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,cAAc,GAAG,MAAM;IACtE,OAAO,KAAK,UAAU,OAAO,CAC3B,QAAgB,EAChB,UAA0B,EAAE;QAE5B,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;QAE/E,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,GAAG,YAAY;SAChB,CAAA;QAED,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAA;QAC9C,CAAC;QAED,IAAI,GAAa,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;YACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,IAAI,cAAc,CAAC,CAAA;YAC7E,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,GAAG,QAAQ,EAAE,EAAE;gBACzC,MAAM;gBACN,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC7C,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YACF,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,qCAAqC,OAAO,GAAG,EAAE,GAAG,CAAC,CAAA;YACnE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,iCAAiC,EAAO,CAAA;QACpF,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,IAAI,OAAe,CAAA;YACnB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBAC/B,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAI,CAAA;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,IAAI,CAAA;YAChB,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAO,CAAA;QACxD,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAO,CAAA;QAChD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QAC7B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAO,CAAA;IACnC,CAAC,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@karbonjs/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe API client for Karbon backends with SSR and client support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@karbonjs/types": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"dev": "tsc --watch"
|
|
27
|
+
}
|
|
28
|
+
}
|