@karbonjs/api 0.2.4 → 0.2.5

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/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createClientApi } from './client/client.js';
2
2
  export type { ClientApiConfig } from './client/client.js';
3
3
  export { createServerApi } from './server/server.js';
4
+ export { createProxy, type ProxyConfig } from './server/proxy.js';
5
+ export { createRateLimiter, type RateLimitRule, type RateLimitRules, type RateLimitResult, type RateLimiter } from './server/rate-limiter.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { createClientApi } from './client/client.js';
2
2
  export { createServerApi } from './server/server.js';
3
+ export { createProxy } from './server/proxy.js';
4
+ export { createRateLimiter } from './server/rate-limiter.js';
3
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGpD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGpD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAmF,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { createServerApi } from './server.js';
2
+ export { createProxy, type ProxyConfig } from './proxy.js';
3
+ export { createRateLimiter, type RateLimitRule, type RateLimitRules, type RateLimitResult, type RateLimiter } from './rate-limiter.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { createServerApi } from './server.js';
2
+ export { createProxy } from './proxy.js';
3
+ export { createRateLimiter } from './rate-limiter.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAmF,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Configurable API proxy for SvelteKit catch-all routes.
3
+ *
4
+ * Generates GET/POST/PUT/PATCH/DELETE handlers that proxy
5
+ * requests to a backend server with security built-in:
6
+ * - Path sanitization (blocks `../`, `//`)
7
+ * - Blocked path prefixes (e.g. `internal`)
8
+ * - CSRF protection (Origin vs Host check)
9
+ * - Rate limiting (sliding window, per IP)
10
+ * - Body size limit
11
+ * - Request/response streaming (no memory buffering)
12
+ * - Cookie & auth header forwarding
13
+ * - X-Forwarded-For client IP forwarding
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // src/routes/api/[...path]/+server.ts
18
+ * import { createProxy } from '@karbonjs/api'
19
+ *
20
+ * export const { GET, POST, PUT, PATCH, DELETE } = createProxy({
21
+ * backend: 'http://localhost:8080',
22
+ * prefix: '/api',
23
+ * blockedPrefixes: ['internal'],
24
+ * csrf: true,
25
+ * rateLimit: {
26
+ * 'auth/login': { max: 20, windowSec: 60 },
27
+ * '*': { max: 200, windowSec: 60 },
28
+ * },
29
+ * })
30
+ * ```
31
+ */
32
+ import { type RateLimitRules } from './rate-limiter.js';
33
+ export interface ProxyConfig {
34
+ /** Backend URL (e.g. `http://localhost:8080`) */
35
+ backend: string;
36
+ /** URL prefix to prepend to proxied path (default: `/api`) */
37
+ prefix?: string;
38
+ /** Max request body size in bytes (default: 10 MB) */
39
+ maxBodySize?: number;
40
+ /** Path prefixes to block — returns 404 (default: `['internal']`) */
41
+ blockedPrefixes?: string[];
42
+ /** Enable CSRF check on mutating requests (default: `true`) */
43
+ csrf?: boolean;
44
+ /** Rate limit rules per route pattern. Use `'*'` for default. Set `false` to disable. */
45
+ rateLimit?: RateLimitRules | false;
46
+ }
47
+ /** Minimal SvelteKit RequestEvent shape — keeps karbonjs framework-agnostic */
48
+ interface RequestEvent {
49
+ request: Request;
50
+ params: Record<string, string>;
51
+ url: URL;
52
+ getClientAddress: () => string;
53
+ }
54
+ type Handler = (event: RequestEvent) => Promise<Response>;
55
+ /**
56
+ * Create a proxy handler set for SvelteKit `+server.ts` catch-all routes.
57
+ */
58
+ export declare function createProxy(config: ProxyConfig): {
59
+ GET: Handler;
60
+ POST: Handler;
61
+ PUT: Handler;
62
+ PATCH: Handler;
63
+ DELETE: Handler;
64
+ };
65
+ export {};
66
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/server/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAqB,KAAK,cAAc,EAAoB,MAAM,mBAAmB,CAAA;AAE5F,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,yFAAyF;IACzF,SAAS,CAAC,EAAE,cAAc,GAAG,KAAK,CAAA;CACnC;AAED,+EAA+E;AAC/E,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,GAAG,EAAE,GAAG,CAAA;IACR,gBAAgB,EAAE,MAAM,MAAM,CAAA;CAC/B;AAED,KAAK,OAAO,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AASzD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;;;;;;EAiJ9C"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Configurable API proxy for SvelteKit catch-all routes.
3
+ *
4
+ * Generates GET/POST/PUT/PATCH/DELETE handlers that proxy
5
+ * requests to a backend server with security built-in:
6
+ * - Path sanitization (blocks `../`, `//`)
7
+ * - Blocked path prefixes (e.g. `internal`)
8
+ * - CSRF protection (Origin vs Host check)
9
+ * - Rate limiting (sliding window, per IP)
10
+ * - Body size limit
11
+ * - Request/response streaming (no memory buffering)
12
+ * - Cookie & auth header forwarding
13
+ * - X-Forwarded-For client IP forwarding
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // src/routes/api/[...path]/+server.ts
18
+ * import { createProxy } from '@karbonjs/api'
19
+ *
20
+ * export const { GET, POST, PUT, PATCH, DELETE } = createProxy({
21
+ * backend: 'http://localhost:8080',
22
+ * prefix: '/api',
23
+ * blockedPrefixes: ['internal'],
24
+ * csrf: true,
25
+ * rateLimit: {
26
+ * 'auth/login': { max: 20, windowSec: 60 },
27
+ * '*': { max: 200, windowSec: 60 },
28
+ * },
29
+ * })
30
+ * ```
31
+ */
32
+ import { createRateLimiter } from './rate-limiter.js';
33
+ function jsonResponse(status, message, extra) {
34
+ return new Response(JSON.stringify({ success: false, message }), {
35
+ status,
36
+ headers: { 'content-type': 'application/json', ...extra },
37
+ });
38
+ }
39
+ /**
40
+ * Create a proxy handler set for SvelteKit `+server.ts` catch-all routes.
41
+ */
42
+ export function createProxy(config) {
43
+ const { backend, prefix = '/api', maxBodySize = 10 * 1024 * 1024, blockedPrefixes = ['internal'], csrf = true, rateLimit: rateLimitConfig, } = config;
44
+ // Validate backend URL
45
+ try {
46
+ const url = new URL(backend);
47
+ if (!['http:', 'https:'].includes(url.protocol)) {
48
+ throw new Error('Backend URL must use http or https');
49
+ }
50
+ }
51
+ catch {
52
+ throw new Error(`Invalid backend URL: ${backend}`);
53
+ }
54
+ // Init rate limiter (unless disabled)
55
+ let limiter = null;
56
+ if (rateLimitConfig !== false) {
57
+ limiter = createRateLimiter(rateLimitConfig ?? undefined);
58
+ }
59
+ const handler = async (event) => {
60
+ const { request, params, url } = event;
61
+ const rawPath = params.path ?? '';
62
+ // 1. Sanitize path
63
+ if (rawPath.includes('..') || rawPath.includes('//')) {
64
+ return jsonResponse(400, 'Invalid path');
65
+ }
66
+ // 2. Block forbidden prefixes
67
+ const firstSegment = rawPath.split('/')[0];
68
+ if (blockedPrefixes.includes(firstSegment)) {
69
+ return jsonResponse(404, 'Not found');
70
+ }
71
+ const path = `${prefix}/${rawPath}${url.search}`;
72
+ // 3. Client IP
73
+ let clientIp = '127.0.0.1';
74
+ try {
75
+ clientIp = event.getClientAddress();
76
+ }
77
+ catch { /* dev mode */ }
78
+ // 4. Rate limiting
79
+ if (limiter) {
80
+ const rl = limiter.check(clientIp, path);
81
+ if (!rl.allowed) {
82
+ return jsonResponse(429, 'Too many requests', {
83
+ 'retry-after': String(rl.retryAfterSec ?? 60),
84
+ 'x-ratelimit-limit': String(rl.limit),
85
+ 'x-ratelimit-remaining': '0',
86
+ });
87
+ }
88
+ }
89
+ // 5. CSRF — mutating requests must come from same origin
90
+ if (csrf && request.method !== 'GET' && request.method !== 'HEAD') {
91
+ const origin = request.headers.get('origin');
92
+ const host = request.headers.get('host');
93
+ if (origin && host) {
94
+ try {
95
+ const originHost = new URL(origin).host;
96
+ if (originHost !== host) {
97
+ return jsonResponse(403, 'Forbidden');
98
+ }
99
+ }
100
+ catch {
101
+ return jsonResponse(403, 'Forbidden');
102
+ }
103
+ }
104
+ }
105
+ // 6. Build headers
106
+ const headers = new Headers();
107
+ headers.set('content-type', request.headers.get('content-type') || 'application/json');
108
+ const auth = request.headers.get('authorization');
109
+ if (auth)
110
+ headers.set('authorization', auth);
111
+ const cookies = request.headers.get('cookie');
112
+ if (cookies)
113
+ headers.set('cookie', cookies);
114
+ headers.set('x-forwarded-for', clientIp);
115
+ // 7. Stream body with size check
116
+ let body = null;
117
+ if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
118
+ const contentLength = request.headers.get('content-length');
119
+ if (contentLength && parseInt(contentLength) > maxBodySize) {
120
+ return jsonResponse(413, 'Request too large');
121
+ }
122
+ body = request.body;
123
+ }
124
+ // 8. Proxy to backend
125
+ let apiRes;
126
+ try {
127
+ apiRes = await fetch(`${backend}${path}`, {
128
+ method: request.method,
129
+ headers,
130
+ body,
131
+ // @ts-expect-error — duplex required for streaming body in Node
132
+ duplex: body ? 'half' : undefined,
133
+ });
134
+ }
135
+ catch {
136
+ return jsonResponse(502, 'Backend unreachable');
137
+ }
138
+ // 9. Build response — stream back
139
+ const responseHeaders = new Headers();
140
+ responseHeaders.set('content-type', apiRes.headers.get('content-type') || 'application/json');
141
+ // Rate limit info
142
+ if (limiter) {
143
+ const rl = limiter.check(clientIp, path);
144
+ responseHeaders.set('x-ratelimit-limit', String(rl.limit));
145
+ responseHeaders.set('x-ratelimit-remaining', String(rl.remaining));
146
+ }
147
+ // Forward Set-Cookie
148
+ const setCookies = apiRes.headers.getSetCookie?.() ?? [];
149
+ for (const cookie of setCookies) {
150
+ responseHeaders.append('set-cookie', cookie);
151
+ }
152
+ if (setCookies.length === 0) {
153
+ const raw = apiRes.headers.get('set-cookie');
154
+ if (raw)
155
+ responseHeaders.set('set-cookie', raw);
156
+ }
157
+ return new Response(apiRes.body, {
158
+ status: apiRes.status,
159
+ headers: responseHeaders,
160
+ });
161
+ };
162
+ return {
163
+ GET: handler,
164
+ POST: handler,
165
+ PUT: handler,
166
+ PATCH: handler,
167
+ DELETE: handler,
168
+ };
169
+ }
170
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/server/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * In-memory sliding window rate limiter.
3
+ *
4
+ * Tracks request timestamps per IP + route pattern.
5
+ * Auto-purges expired entries every 5 minutes.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const limiter = createRateLimiter({
10
+ * 'auth/login': { max: 20, windowSec: 60 },
11
+ * '*': { max: 200, windowSec: 60 },
12
+ * })
13
+ *
14
+ * const result = limiter.check('192.168.1.1', '/api/auth/login')
15
+ * if (!result.allowed) // return 429
16
+ * ```
17
+ */
18
+ export interface RateLimitRule {
19
+ max: number;
20
+ windowSec: number;
21
+ }
22
+ export type RateLimitRules = Record<string, RateLimitRule>;
23
+ export interface RateLimitResult {
24
+ allowed: boolean;
25
+ remaining: number;
26
+ limit: number;
27
+ retryAfterSec?: number;
28
+ }
29
+ export interface RateLimiter {
30
+ /** Check if request is allowed. Records the timestamp if allowed. */
31
+ check(ip: string, path: string): RateLimitResult;
32
+ /** Reset all entries (useful for testing) */
33
+ reset(): void;
34
+ /** Stop the cleanup interval */
35
+ destroy(): void;
36
+ }
37
+ /**
38
+ * Create a rate limiter with custom rules.
39
+ * Rules are matched by path prefix (after stripping `/api/`).
40
+ * Use `'*'` as the fallback rule.
41
+ */
42
+ export declare function createRateLimiter(rules?: RateLimitRules): RateLimiter;
43
+ //# sourceMappingURL=rate-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/server/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE1D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAUD,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAAA;IAChD,6CAA6C;IAC7C,KAAK,IAAI,IAAI,CAAA;IACb,gCAAgC;IAChC,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,cAA8B,GAAG,WAAW,CAqEpF"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * In-memory sliding window rate limiter.
3
+ *
4
+ * Tracks request timestamps per IP + route pattern.
5
+ * Auto-purges expired entries every 5 minutes.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const limiter = createRateLimiter({
10
+ * 'auth/login': { max: 20, windowSec: 60 },
11
+ * '*': { max: 200, windowSec: 60 },
12
+ * })
13
+ *
14
+ * const result = limiter.check('192.168.1.1', '/api/auth/login')
15
+ * if (!result.allowed) // return 429
16
+ * ```
17
+ */
18
+ const DEFAULT_RULES = {
19
+ '*': { max: 200, windowSec: 60 },
20
+ };
21
+ /**
22
+ * Create a rate limiter with custom rules.
23
+ * Rules are matched by path prefix (after stripping `/api/`).
24
+ * Use `'*'` as the fallback rule.
25
+ */
26
+ export function createRateLimiter(rules = DEFAULT_RULES) {
27
+ const mergedRules = { ...DEFAULT_RULES, ...rules };
28
+ const store = new Map();
29
+ // Cleanup expired entries every 5 min
30
+ const cleanupTimer = setInterval(() => {
31
+ const now = Date.now();
32
+ for (const [ip, routes] of store) {
33
+ for (const [route, entry] of routes) {
34
+ const rule = getRule(route);
35
+ entry.timestamps = entry.timestamps.filter(t => now - t < rule.windowSec * 1000);
36
+ if (entry.timestamps.length === 0)
37
+ routes.delete(route);
38
+ }
39
+ if (routes.size === 0)
40
+ store.delete(ip);
41
+ }
42
+ }, 5 * 60 * 1000);
43
+ // Don't block process exit
44
+ if (cleanupTimer && typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {
45
+ cleanupTimer.unref();
46
+ }
47
+ function matchRoute(path) {
48
+ const route = path.replace(/^\/api\//, '');
49
+ for (const pattern of Object.keys(mergedRules)) {
50
+ if (pattern === '*')
51
+ continue;
52
+ if (route.startsWith(pattern))
53
+ return pattern;
54
+ }
55
+ return '*';
56
+ }
57
+ function getRule(routeKey) {
58
+ return mergedRules[routeKey] ?? mergedRules['*'] ?? DEFAULT_RULES['*'];
59
+ }
60
+ function check(ip, path) {
61
+ const routeKey = matchRoute(path);
62
+ const rule = getRule(routeKey);
63
+ const now = Date.now();
64
+ const windowMs = rule.windowSec * 1000;
65
+ if (!store.has(ip))
66
+ store.set(ip, new Map());
67
+ const ipMap = store.get(ip);
68
+ if (!ipMap.has(routeKey))
69
+ ipMap.set(routeKey, { timestamps: [] });
70
+ const entry = ipMap.get(routeKey);
71
+ // Sliding window
72
+ entry.timestamps = entry.timestamps.filter(t => now - t < windowMs);
73
+ if (entry.timestamps.length >= rule.max) {
74
+ const oldest = entry.timestamps[0];
75
+ const retryAfterSec = Math.ceil((oldest + windowMs - now) / 1000);
76
+ return { allowed: false, remaining: 0, limit: rule.max, retryAfterSec };
77
+ }
78
+ entry.timestamps.push(now);
79
+ return { allowed: true, remaining: rule.max - entry.timestamps.length, limit: rule.max };
80
+ }
81
+ function reset() {
82
+ store.clear();
83
+ }
84
+ function destroy() {
85
+ clearInterval(cleanupTimer);
86
+ store.clear();
87
+ }
88
+ return { check, reset, destroy };
89
+ }
90
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/server/rate-limiter.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karbonjs/api",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Type-safe API client for Karbon backends with SSR and client support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,13 +9,17 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./server": {
14
+ "types": "./dist/server/index.d.ts",
15
+ "import": "./dist/server/index.js"
12
16
  }
13
17
  },
14
18
  "files": [
15
19
  "dist"
16
20
  ],
17
21
  "dependencies": {
18
- "@karbonjs/types": "0.2.4"
22
+ "@karbonjs/types": "0.2.5"
19
23
  },
20
24
  "publishConfig": {
21
25
  "access": "public"