@aigne/afs-http 1.11.0-beta.3

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.
Files changed (56) hide show
  1. package/LICENSE.md +26 -0
  2. package/README.md +318 -0
  3. package/dist/adapters/express.cjs +74 -0
  4. package/dist/adapters/express.d.cts +56 -0
  5. package/dist/adapters/express.d.cts.map +1 -0
  6. package/dist/adapters/express.d.mts +56 -0
  7. package/dist/adapters/express.d.mts.map +1 -0
  8. package/dist/adapters/express.mjs +74 -0
  9. package/dist/adapters/express.mjs.map +1 -0
  10. package/dist/adapters/koa.cjs +73 -0
  11. package/dist/adapters/koa.d.cts +56 -0
  12. package/dist/adapters/koa.d.cts.map +1 -0
  13. package/dist/adapters/koa.d.mts +56 -0
  14. package/dist/adapters/koa.d.mts.map +1 -0
  15. package/dist/adapters/koa.mjs +73 -0
  16. package/dist/adapters/koa.mjs.map +1 -0
  17. package/dist/client.cjs +143 -0
  18. package/dist/client.d.cts +70 -0
  19. package/dist/client.d.cts.map +1 -0
  20. package/dist/client.d.mts +70 -0
  21. package/dist/client.d.mts.map +1 -0
  22. package/dist/client.mjs +144 -0
  23. package/dist/client.mjs.map +1 -0
  24. package/dist/errors.cjs +105 -0
  25. package/dist/errors.d.cts +63 -0
  26. package/dist/errors.d.cts.map +1 -0
  27. package/dist/errors.d.mts +63 -0
  28. package/dist/errors.d.mts.map +1 -0
  29. package/dist/errors.mjs +98 -0
  30. package/dist/errors.mjs.map +1 -0
  31. package/dist/handler.cjs +126 -0
  32. package/dist/handler.d.cts +43 -0
  33. package/dist/handler.d.cts.map +1 -0
  34. package/dist/handler.d.mts +43 -0
  35. package/dist/handler.d.mts.map +1 -0
  36. package/dist/handler.mjs +127 -0
  37. package/dist/handler.mjs.map +1 -0
  38. package/dist/index.cjs +33 -0
  39. package/dist/index.d.cts +8 -0
  40. package/dist/index.d.mts +8 -0
  41. package/dist/index.mjs +9 -0
  42. package/dist/protocol.cjs +68 -0
  43. package/dist/protocol.d.cts +119 -0
  44. package/dist/protocol.d.cts.map +1 -0
  45. package/dist/protocol.d.mts +119 -0
  46. package/dist/protocol.d.mts.map +1 -0
  47. package/dist/protocol.mjs +64 -0
  48. package/dist/protocol.mjs.map +1 -0
  49. package/dist/retry.cjs +111 -0
  50. package/dist/retry.d.cts +57 -0
  51. package/dist/retry.d.cts.map +1 -0
  52. package/dist/retry.d.mts +57 -0
  53. package/dist/retry.d.mts.map +1 -0
  54. package/dist/retry.mjs +105 -0
  55. package/dist/retry.mjs.map +1 -0
  56. package/package.json +55 -0
package/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+ # Proprietary License
2
+
3
+ Copyright (c) 2024-2025 ArcBlock, Inc. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. Unauthorized copying, modification, distribution, or use of
7
+ this Software, via any medium, is strictly prohibited.
8
+
9
+ The Software is provided for internal use only within ArcBlock, Inc. and its
10
+ authorized affiliates.
11
+
12
+ ## No License Granted
13
+
14
+ No license, express or implied, is granted to any party for any purpose.
15
+ All rights are reserved by ArcBlock, Inc.
16
+
17
+ ## Public Artifact Distribution
18
+
19
+ Portions of this Software may be released publicly under separate open-source
20
+ licenses (such as MIT License) through designated public repositories. Such
21
+ public releases are governed by their respective licenses and do not affect
22
+ the proprietary nature of this repository.
23
+
24
+ ## Contact
25
+
26
+ For licensing inquiries, contact: legal@arcblock.io
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # @aigne/afs-http
2
+
3
+ HTTP Transport Provider for AFS (Agentic File System), allowing transparent mounting of remote AFS providers over HTTP.
4
+
5
+ ## Features
6
+
7
+ - 🌐 **RPC-style REST API** - Simple POST /rpc endpoint with method parameter
8
+ - 🔄 **Automatic Retry** - Exponential backoff with configurable options
9
+ - 🎯 **Dual-layer Error Codes** - HTTP status codes + CLI error codes for AFS consistency
10
+ - 🔌 **Framework Agnostic** - Web Standard Request/Response API
11
+ - 🚀 **Express/Koa Adapters** - Ready-to-use middleware for popular frameworks
12
+ - 📦 **Type Safe** - Full TypeScript support with Zod validation
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @aigne/afs-http
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Server Side
23
+
24
+ #### Express
25
+
26
+ ```typescript
27
+ import express from "express";
28
+ import { createAFSExpressHandler } from "@aigne/afs-http";
29
+ import { AFSLocalProvider } from "@aigne/afs-local";
30
+
31
+ const app = express();
32
+
33
+ // Create a local AFS provider to expose
34
+ const provider = new AFSLocalProvider({
35
+ name: "local",
36
+ rootPath: "./data",
37
+ });
38
+
39
+ // Mount the AFS HTTP handler
40
+ app.use("/afs", createAFSExpressHandler({ provider }));
41
+
42
+ app.listen(3000, () => {
43
+ console.log("AFS HTTP server listening on http://localhost:3000");
44
+ });
45
+ ```
46
+
47
+ #### Koa
48
+
49
+ ```typescript
50
+ import Koa from "koa";
51
+ import { createAFSKoaHandler } from "@aigne/afs-http";
52
+ import { AFSLocalProvider } from "@aigne/afs-local";
53
+
54
+ const app = new Koa();
55
+
56
+ const provider = new AFSLocalProvider({
57
+ name: "local",
58
+ rootPath: "./data",
59
+ });
60
+
61
+ app.use(createAFSKoaHandler({ provider }));
62
+
63
+ app.listen(3000);
64
+ ```
65
+
66
+ #### Custom Framework
67
+
68
+ ```typescript
69
+ import { createAFSHttpHandler } from "@aigne/afs-http";
70
+ import { AFSLocalProvider } from "@aigne/afs-local";
71
+
72
+ const provider = new AFSLocalProvider({
73
+ name: "local",
74
+ rootPath: "./data",
75
+ });
76
+
77
+ const handler = createAFSHttpHandler({ provider });
78
+
79
+ // Use with any framework that supports Web Standard Request/Response
80
+ async function handleRequest(request: Request): Promise<Response> {
81
+ return await handler(request);
82
+ }
83
+ ```
84
+
85
+ ### Client Side
86
+
87
+ ```typescript
88
+ import { AFS } from "@aigne/afs";
89
+ import { AFSHttpClient } from "@aigne/afs-http";
90
+
91
+ // Create HTTP client
92
+ const httpClient = new AFSHttpClient({
93
+ url: "http://localhost:3000/afs",
94
+ name: "remote",
95
+ description: "Remote AFS over HTTP",
96
+ });
97
+
98
+ // Mount to AFS
99
+ const afs = new AFS();
100
+ afs.mount(httpClient);
101
+
102
+ // Use like any other AFS provider
103
+ const files = await afs.list("/remote");
104
+ const content = await afs.read("/remote/file.txt");
105
+ await afs.write("/remote/new.txt", "Hello, World!");
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ ### Server Options
111
+
112
+ ```typescript
113
+ interface AFSHttpHandlerOptions {
114
+ /** AFS provider to expose */
115
+ provider: AFSModule;
116
+
117
+ /** Maximum request body size in bytes (default: 10MB) */
118
+ maxBodySize?: number;
119
+
120
+ /** Enable debug logging (default: false) */
121
+ debug?: boolean;
122
+ }
123
+ ```
124
+
125
+ ### Client Options
126
+
127
+ ```typescript
128
+ interface AFSHttpClientOptions {
129
+ /** Server URL (e.g., "http://localhost:3000/afs") */
130
+ url: string;
131
+
132
+ /** Provider name for AFS mounting */
133
+ name: string;
134
+
135
+ /** Optional description */
136
+ description?: string;
137
+
138
+ /** Retry configuration */
139
+ retry?: {
140
+ maxAttempts?: number; // Default: 3
141
+ initialDelay?: number; // Default: 1000ms
142
+ maxDelay?: number; // Default: 10000ms
143
+ multiplier?: number; // Default: 2
144
+ };
145
+
146
+ /** Request timeout in milliseconds (default: 30000) */
147
+ timeout?: number;
148
+
149
+ /** Custom fetch implementation */
150
+ fetch?: typeof fetch;
151
+ }
152
+ ```
153
+
154
+ ## API Methods
155
+
156
+ The HTTP transport supports all standard AFS operations:
157
+
158
+ - `list(path)` - List files and directories
159
+ - `read(path)` - Read file content
160
+ - `write(path, content, options)` - Write file content
161
+ - `delete(path)` - Delete file or directory
162
+ - `rename(oldPath, newPath)` - Rename/move file or directory
163
+ - `search(path, query, options)` - Search for files
164
+ - `exec(path, input)` - Execute command or script
165
+
166
+ ## Error Handling
167
+
168
+ The HTTP transport uses dual-layer error codes:
169
+
170
+ ### HTTP Status Codes
171
+
172
+ - `200 OK` - Success
173
+ - `400 Bad Request` - Invalid request format
174
+ - `404 Not Found` - Resource not found
175
+ - `409 Conflict` - Operation conflict (e.g., file exists)
176
+ - `413 Payload Too Large` - Request body exceeds maxBodySize
177
+ - `500 Internal Server Error` - Server error
178
+
179
+ ### CLI Error Codes
180
+
181
+ Consistent with AFS CLI tools:
182
+
183
+ ```typescript
184
+ enum AFSErrorCode {
185
+ OK = 0,
186
+ NOT_FOUND = 1,
187
+ PERMISSION_DENIED = 2,
188
+ CONFLICT = 3,
189
+ PARTIAL = 4,
190
+ RUNTIME_ERROR = 5,
191
+ }
192
+ ```
193
+
194
+ ## Protocol
195
+
196
+ The HTTP transport uses a simple RPC-style protocol:
197
+
198
+ ### Request
199
+
200
+ ```http
201
+ POST /rpc HTTP/1.1
202
+ Content-Type: application/json
203
+
204
+ {
205
+ "method": "read",
206
+ "params": {
207
+ "path": "/file.txt"
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### Response (Success)
213
+
214
+ ```http
215
+ HTTP/1.1 200 OK
216
+ Content-Type: application/json
217
+
218
+ {
219
+ "code": 0,
220
+ "data": {
221
+ "content": "file content",
222
+ "mimeType": "text/plain"
223
+ }
224
+ }
225
+ ```
226
+
227
+ ### Response (Error)
228
+
229
+ ```http
230
+ HTTP/1.1 404 Not Found
231
+ Content-Type: application/json
232
+
233
+ {
234
+ "code": 1,
235
+ "error": "File not found: /file.txt"
236
+ }
237
+ ```
238
+
239
+ ## Retry Behavior
240
+
241
+ The client automatically retries failed requests with exponential backoff:
242
+
243
+ - Retries on network errors and 5xx status codes
244
+ - Does not retry 4xx errors (client errors)
245
+ - Default: 3 attempts with 1s → 2s → 4s delays
246
+ - Configurable via `retry` options
247
+
248
+ ```typescript
249
+ const client = new AFSHttpClient({
250
+ url: "http://localhost:3000/afs",
251
+ name: "remote",
252
+ retry: {
253
+ maxAttempts: 5,
254
+ initialDelay: 500,
255
+ maxDelay: 30000,
256
+ multiplier: 2,
257
+ },
258
+ });
259
+ ```
260
+
261
+ ## Advanced Usage
262
+
263
+ ### Custom Authentication
264
+
265
+ Add authentication headers to the client:
266
+
267
+ ```typescript
268
+ class AuthenticatedHttpClient extends AFSHttpClient {
269
+ async fetch(url: string, options: RequestInit): Promise<Response> {
270
+ return super.fetch(url, {
271
+ ...options,
272
+ headers: {
273
+ ...options.headers,
274
+ "Authorization": `Bearer ${process.env.API_TOKEN}`,
275
+ },
276
+ });
277
+ }
278
+ }
279
+ ```
280
+
281
+ ### Custom Error Handling
282
+
283
+ ```typescript
284
+ try {
285
+ const content = await afs.read("/remote/file.txt");
286
+ } catch (error) {
287
+ if (error instanceof AFSHttpError) {
288
+ console.error(`HTTP ${error.status}: ${error.message}`);
289
+ console.error(`AFS Error Code: ${error.code}`);
290
+ }
291
+ }
292
+ ```
293
+
294
+ ## Performance Considerations
295
+
296
+ - **File Size Limits**: Configure `maxBodySize` based on your needs
297
+ - **Timeouts**: Adjust client timeout for slow networks
298
+ - **Retry Strategy**: Tune retry parameters for your use case
299
+ - **Caching**: Not implemented (prioritizes data consistency)
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ # Install dependencies
305
+ pnpm install
306
+
307
+ # Build
308
+ pnpm build
309
+
310
+ # Run tests
311
+ pnpm test
312
+
313
+ # Type check
314
+ pnpm check-types
315
+
316
+ # Lint
317
+ pnpm lint
318
+ ```
@@ -0,0 +1,74 @@
1
+
2
+ //#region src/adapters/express.ts
3
+ /**
4
+ * Collect stream data into a string
5
+ */
6
+ async function streamToString(stream) {
7
+ const chunks = [];
8
+ return new Promise((resolve, reject) => {
9
+ stream.on("data", (chunk) => chunks.push(chunk));
10
+ stream.on("error", reject);
11
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
12
+ });
13
+ }
14
+ /**
15
+ * Convert Express headers to Headers object
16
+ */
17
+ function convertHeaders(headers) {
18
+ const result = new Headers();
19
+ for (const [key, value] of Object.entries(headers)) {
20
+ if (value === void 0) continue;
21
+ if (Array.isArray(value)) for (const v of value) result.append(key, v);
22
+ else result.set(key, value);
23
+ }
24
+ return result;
25
+ }
26
+ /**
27
+ * Express adapter for AFS HTTP handler
28
+ *
29
+ * This adapter converts between Express request/response objects and
30
+ * Web Standard Request/Response objects, handling both scenarios where
31
+ * body parsing middleware is configured and where it isn't.
32
+ *
33
+ * @param handler - The AFS HTTP handler function
34
+ * @returns An Express-compatible request handler
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import express from "express";
39
+ * import { createAFSHttpHandler, expressAdapter } from "@aigne/afs-http";
40
+ *
41
+ * const handler = createAFSHttpHandler({ module: provider });
42
+ * const app = express();
43
+ *
44
+ * // Works with or without express.json() middleware
45
+ * app.post("/afs/rpc", expressAdapter(handler));
46
+ * ```
47
+ */
48
+ function expressAdapter(handler) {
49
+ return async (req, res, next) => {
50
+ try {
51
+ const host = req.get("host") || "localhost";
52
+ const url = `${req.protocol}://${host}${req.originalUrl}`;
53
+ let body;
54
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") if (req.body !== void 0 && req.body !== null && typeof req.body === "object" && Object.keys(req.body).length > 0) body = JSON.stringify(req.body);
55
+ else body = await streamToString(req);
56
+ const response = await handler(new Request(url, {
57
+ method: req.method,
58
+ headers: convertHeaders(req.headers),
59
+ body
60
+ }));
61
+ res.status(response.status);
62
+ response.headers.forEach((value, key) => {
63
+ res.setHeader(key, value);
64
+ });
65
+ const responseBody = await response.text();
66
+ res.send(responseBody);
67
+ } catch (error) {
68
+ next(error instanceof Error ? error : new Error(String(error)));
69
+ }
70
+ };
71
+ }
72
+
73
+ //#endregion
74
+ exports.expressAdapter = expressAdapter;
@@ -0,0 +1,56 @@
1
+ //#region src/adapters/express.d.ts
2
+ /**
3
+ * Express-compatible request interface
4
+ */
5
+ interface ExpressRequest {
6
+ method: string;
7
+ protocol: string;
8
+ originalUrl: string;
9
+ body?: unknown;
10
+ headers: Record<string, string | string[] | undefined>;
11
+ get(name: string): string | undefined;
12
+ on(event: string, callback: (chunk: Buffer) => void): void;
13
+ pipe<T extends NodeJS.WritableStream>(destination: T): T;
14
+ }
15
+ /**
16
+ * Express-compatible response interface
17
+ */
18
+ interface ExpressResponse {
19
+ status(code: number): ExpressResponse;
20
+ setHeader(name: string, value: string): void;
21
+ send(body: string): void;
22
+ }
23
+ /**
24
+ * Express-compatible next function
25
+ */
26
+ type ExpressNextFunction = (error?: Error) => void;
27
+ /**
28
+ * Express-compatible request handler
29
+ */
30
+ type ExpressRequestHandler = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void | Promise<void>;
31
+ /**
32
+ * Express adapter for AFS HTTP handler
33
+ *
34
+ * This adapter converts between Express request/response objects and
35
+ * Web Standard Request/Response objects, handling both scenarios where
36
+ * body parsing middleware is configured and where it isn't.
37
+ *
38
+ * @param handler - The AFS HTTP handler function
39
+ * @returns An Express-compatible request handler
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import express from "express";
44
+ * import { createAFSHttpHandler, expressAdapter } from "@aigne/afs-http";
45
+ *
46
+ * const handler = createAFSHttpHandler({ module: provider });
47
+ * const app = express();
48
+ *
49
+ * // Works with or without express.json() middleware
50
+ * app.post("/afs/rpc", expressAdapter(handler));
51
+ * ```
52
+ */
53
+ declare function expressAdapter(handler: (request: Request) => Promise<Response>): ExpressRequestHandler;
54
+ //#endregion
55
+ export { expressAdapter };
56
+ //# sourceMappingURL=express.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.cts","names":[],"sources":["../../src/adapters/express.ts"],"mappings":";;;;UAKU,cAAA;EAAA,MAAA;EAAA,QAAA;EAAA,WAAA;EAAA,IAAA;EAAA,OAAA,EAKC,MAAA;EAAA,GAAA,CAAA,IAAA;EAAA,EAAA,CAAA,KAAA,UAAA,QAAA,GAAA,KAAA,EAE2B,MAAA;EAAA,IAAA,WACrB,MAAA,CAAO,cAAA,EAAA,WAAA,EAA6B,CAAA,GAAI,CAAA;AAAA;AAAA;;AAAC;AAAD,UAM/C,eAAA;EAAA,MAAA,CAAA,IAAA,WACc,eAAA;EAAA,SAAA,CAAA,IAAA,UAAA,KAAA;EAAA,IAAA,CAAA,IAAA;AAAA;AAAA;AAAe;AAQE;AARjB,KAQnB,mBAAA,IAAA,KAAA,GAA+B,KAAA;AAAA;AAAK;;AAAL,KAK/B,qBAAA,IAAA,GAAA,EACE,cAAA,EAAA,GAAA,EACA,eAAA,EAAA,IAAA,EACC,mBAAA,YACI,OAAA;AAAA;;AAsDZ;;;;;;;;;;;;;;;;;;;;AAtDY,iBAsDI,cAAA,CAAA,OAAA,GAAA,OAAA,EACK,OAAA,KAAY,OAAA,CAAQ,QAAA,IACtC,qBAAA"}
@@ -0,0 +1,56 @@
1
+ //#region src/adapters/express.d.ts
2
+ /**
3
+ * Express-compatible request interface
4
+ */
5
+ interface ExpressRequest {
6
+ method: string;
7
+ protocol: string;
8
+ originalUrl: string;
9
+ body?: unknown;
10
+ headers: Record<string, string | string[] | undefined>;
11
+ get(name: string): string | undefined;
12
+ on(event: string, callback: (chunk: Buffer) => void): void;
13
+ pipe<T extends NodeJS.WritableStream>(destination: T): T;
14
+ }
15
+ /**
16
+ * Express-compatible response interface
17
+ */
18
+ interface ExpressResponse {
19
+ status(code: number): ExpressResponse;
20
+ setHeader(name: string, value: string): void;
21
+ send(body: string): void;
22
+ }
23
+ /**
24
+ * Express-compatible next function
25
+ */
26
+ type ExpressNextFunction = (error?: Error) => void;
27
+ /**
28
+ * Express-compatible request handler
29
+ */
30
+ type ExpressRequestHandler = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void | Promise<void>;
31
+ /**
32
+ * Express adapter for AFS HTTP handler
33
+ *
34
+ * This adapter converts between Express request/response objects and
35
+ * Web Standard Request/Response objects, handling both scenarios where
36
+ * body parsing middleware is configured and where it isn't.
37
+ *
38
+ * @param handler - The AFS HTTP handler function
39
+ * @returns An Express-compatible request handler
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import express from "express";
44
+ * import { createAFSHttpHandler, expressAdapter } from "@aigne/afs-http";
45
+ *
46
+ * const handler = createAFSHttpHandler({ module: provider });
47
+ * const app = express();
48
+ *
49
+ * // Works with or without express.json() middleware
50
+ * app.post("/afs/rpc", expressAdapter(handler));
51
+ * ```
52
+ */
53
+ declare function expressAdapter(handler: (request: Request) => Promise<Response>): ExpressRequestHandler;
54
+ //#endregion
55
+ export { expressAdapter };
56
+ //# sourceMappingURL=express.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../../src/adapters/express.ts"],"mappings":";;;;UAKU,cAAA;EAAA,MAAA;EAAA,QAAA;EAAA,WAAA;EAAA,IAAA;EAAA,OAAA,EAKC,MAAA;EAAA,GAAA,CAAA,IAAA;EAAA,EAAA,CAAA,KAAA,UAAA,QAAA,GAAA,KAAA,EAE2B,MAAA;EAAA,IAAA,WACrB,MAAA,CAAO,cAAA,EAAA,WAAA,EAA6B,CAAA,GAAI,CAAA;AAAA;AAAA;;AAAC;AAAD,UAM/C,eAAA;EAAA,MAAA,CAAA,IAAA,WACc,eAAA;EAAA,SAAA,CAAA,IAAA,UAAA,KAAA;EAAA,IAAA,CAAA,IAAA;AAAA;AAAA;AAAe;AAQE;AARjB,KAQnB,mBAAA,IAAA,KAAA,GAA+B,KAAA;AAAA;AAAK;;AAAL,KAK/B,qBAAA,IAAA,GAAA,EACE,cAAA,EAAA,GAAA,EACA,eAAA,EAAA,IAAA,EACC,mBAAA,YACI,OAAA;AAAA;;AAsDZ;;;;;;;;;;;;;;;;;;;;AAtDY,iBAsDI,cAAA,CAAA,OAAA,GAAA,OAAA,EACK,OAAA,KAAY,OAAA,CAAQ,QAAA,IACtC,qBAAA"}
@@ -0,0 +1,74 @@
1
+ //#region src/adapters/express.ts
2
+ /**
3
+ * Collect stream data into a string
4
+ */
5
+ async function streamToString(stream) {
6
+ const chunks = [];
7
+ return new Promise((resolve, reject) => {
8
+ stream.on("data", (chunk) => chunks.push(chunk));
9
+ stream.on("error", reject);
10
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
11
+ });
12
+ }
13
+ /**
14
+ * Convert Express headers to Headers object
15
+ */
16
+ function convertHeaders(headers) {
17
+ const result = new Headers();
18
+ for (const [key, value] of Object.entries(headers)) {
19
+ if (value === void 0) continue;
20
+ if (Array.isArray(value)) for (const v of value) result.append(key, v);
21
+ else result.set(key, value);
22
+ }
23
+ return result;
24
+ }
25
+ /**
26
+ * Express adapter for AFS HTTP handler
27
+ *
28
+ * This adapter converts between Express request/response objects and
29
+ * Web Standard Request/Response objects, handling both scenarios where
30
+ * body parsing middleware is configured and where it isn't.
31
+ *
32
+ * @param handler - The AFS HTTP handler function
33
+ * @returns An Express-compatible request handler
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * import express from "express";
38
+ * import { createAFSHttpHandler, expressAdapter } from "@aigne/afs-http";
39
+ *
40
+ * const handler = createAFSHttpHandler({ module: provider });
41
+ * const app = express();
42
+ *
43
+ * // Works with or without express.json() middleware
44
+ * app.post("/afs/rpc", expressAdapter(handler));
45
+ * ```
46
+ */
47
+ function expressAdapter(handler) {
48
+ return async (req, res, next) => {
49
+ try {
50
+ const host = req.get("host") || "localhost";
51
+ const url = `${req.protocol}://${host}${req.originalUrl}`;
52
+ let body;
53
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") if (req.body !== void 0 && req.body !== null && typeof req.body === "object" && Object.keys(req.body).length > 0) body = JSON.stringify(req.body);
54
+ else body = await streamToString(req);
55
+ const response = await handler(new Request(url, {
56
+ method: req.method,
57
+ headers: convertHeaders(req.headers),
58
+ body
59
+ }));
60
+ res.status(response.status);
61
+ response.headers.forEach((value, key) => {
62
+ res.setHeader(key, value);
63
+ });
64
+ const responseBody = await response.text();
65
+ res.send(responseBody);
66
+ } catch (error) {
67
+ next(error instanceof Error ? error : new Error(String(error)));
68
+ }
69
+ };
70
+ }
71
+
72
+ //#endregion
73
+ export { expressAdapter };
74
+ //# sourceMappingURL=express.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../../src/adapters/express.ts"],"sourcesContent":["import type { Readable } from \"node:stream\";\n\n/**\n * Express-compatible request interface\n */\ninterface ExpressRequest {\n method: string;\n protocol: string;\n originalUrl: string;\n body?: unknown;\n headers: Record<string, string | string[] | undefined>;\n get(name: string): string | undefined;\n on(event: string, callback: (chunk: Buffer) => void): void;\n pipe<T extends NodeJS.WritableStream>(destination: T): T;\n}\n\n/**\n * Express-compatible response interface\n */\ninterface ExpressResponse {\n status(code: number): ExpressResponse;\n setHeader(name: string, value: string): void;\n send(body: string): void;\n}\n\n/**\n * Express-compatible next function\n */\ntype ExpressNextFunction = (error?: Error) => void;\n\n/**\n * Express-compatible request handler\n */\ntype ExpressRequestHandler = (\n req: ExpressRequest,\n res: ExpressResponse,\n next: ExpressNextFunction,\n) => void | Promise<void>;\n\n/**\n * Collect stream data into a string\n */\nasync function streamToString(stream: Readable): Promise<string> {\n const chunks: Buffer[] = [];\n return new Promise((resolve, reject) => {\n stream.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n stream.on(\"error\", reject);\n stream.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n });\n}\n\n/**\n * Convert Express headers to Headers object\n */\nfunction convertHeaders(headers: Record<string, string | string[] | undefined>): Headers {\n const result = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const v of value) {\n result.append(key, v);\n }\n } else {\n result.set(key, value);\n }\n }\n return result;\n}\n\n/**\n * Express adapter for AFS HTTP handler\n *\n * This adapter converts between Express request/response objects and\n * Web Standard Request/Response objects, handling both scenarios where\n * body parsing middleware is configured and where it isn't.\n *\n * @param handler - The AFS HTTP handler function\n * @returns An Express-compatible request handler\n *\n * @example\n * ```typescript\n * import express from \"express\";\n * import { createAFSHttpHandler, expressAdapter } from \"@aigne/afs-http\";\n *\n * const handler = createAFSHttpHandler({ module: provider });\n * const app = express();\n *\n * // Works with or without express.json() middleware\n * app.post(\"/afs/rpc\", expressAdapter(handler));\n * ```\n */\nexport function expressAdapter(\n handler: (request: Request) => Promise<Response>,\n): ExpressRequestHandler {\n return async (req, res, next) => {\n try {\n // Build URL from request\n const host = req.get(\"host\") || \"localhost\";\n const url = `${req.protocol}://${host}${req.originalUrl}`;\n\n // Get body - handle both parsed and unparsed scenarios\n let body: string | undefined;\n\n if (req.method === \"POST\" || req.method === \"PUT\" || req.method === \"PATCH\") {\n if (\n req.body !== undefined &&\n req.body !== null &&\n typeof req.body === \"object\" &&\n Object.keys(req.body).length > 0\n ) {\n // Scenario 1: Body already parsed by middleware (e.g., express.json())\n body = JSON.stringify(req.body);\n } else {\n // Scenario 2: Body not parsed, read from stream\n body = await streamToString(req as unknown as Readable);\n }\n }\n\n // Create Web Standard Request\n const request = new Request(url, {\n method: req.method,\n headers: convertHeaders(req.headers),\n body,\n });\n\n // Call the handler\n const response = await handler(request);\n\n // Write response back to Express\n res.status(response.status);\n\n // Copy headers\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n // Send body\n const responseBody = await response.text();\n res.send(responseBody);\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n };\n}\n"],"mappings":";;;;AA0CA,eAAe,eAAe,QAAmC;CAC/D,MAAM,SAAmB,EAAE;AAC3B,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,SAAO,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AACxD,SAAO,GAAG,SAAS,OAAO;AAC1B,SAAO,GAAG,aAAa,QAAQ,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,CAAC,CAAC;GACvE;;;;;AAMJ,SAAS,eAAe,SAAiE;CACvF,MAAM,SAAS,IAAI,SAAS;AAC5B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,EAAE;AAClD,MAAI,UAAU,OAAW;AACzB,MAAI,MAAM,QAAQ,MAAM,CACtB,MAAK,MAAM,KAAK,MACd,QAAO,OAAO,KAAK,EAAE;MAGvB,QAAO,IAAI,KAAK,MAAM;;AAG1B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBT,SAAgB,eACd,SACuB;AACvB,QAAO,OAAO,KAAK,KAAK,SAAS;AAC/B,MAAI;GAEF,MAAM,OAAO,IAAI,IAAI,OAAO,IAAI;GAChC,MAAM,MAAM,GAAG,IAAI,SAAS,KAAK,OAAO,IAAI;GAG5C,IAAI;AAEJ,OAAI,IAAI,WAAW,UAAU,IAAI,WAAW,SAAS,IAAI,WAAW,QAClE,KACE,IAAI,SAAS,UACb,IAAI,SAAS,QACb,OAAO,IAAI,SAAS,YACpB,OAAO,KAAK,IAAI,KAAK,CAAC,SAAS,EAG/B,QAAO,KAAK,UAAU,IAAI,KAAK;OAG/B,QAAO,MAAM,eAAe,IAA2B;GAY3D,MAAM,WAAW,MAAM,QAPP,IAAI,QAAQ,KAAK;IAC/B,QAAQ,IAAI;IACZ,SAAS,eAAe,IAAI,QAAQ;IACpC;IACD,CAAC,CAGqC;AAGvC,OAAI,OAAO,SAAS,OAAO;AAG3B,YAAS,QAAQ,SAAS,OAAO,QAAQ;AACvC,QAAI,UAAU,KAAK,MAAM;KACzB;GAGF,MAAM,eAAe,MAAM,SAAS,MAAM;AAC1C,OAAI,KAAK,aAAa;WACf,OAAO;AACd,QAAK,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC"}
@@ -0,0 +1,73 @@
1
+
2
+ //#region src/adapters/koa.ts
3
+ /**
4
+ * Collect stream data into a string
5
+ */
6
+ async function streamToString(stream) {
7
+ const chunks = [];
8
+ return new Promise((resolve, reject) => {
9
+ stream.on("data", (chunk) => chunks.push(chunk));
10
+ stream.on("error", reject);
11
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
12
+ });
13
+ }
14
+ /**
15
+ * Convert Koa headers to Headers object
16
+ */
17
+ function convertHeaders(headers) {
18
+ const result = new Headers();
19
+ for (const [key, value] of Object.entries(headers)) {
20
+ if (value === void 0) continue;
21
+ if (Array.isArray(value)) for (const v of value) result.append(key, v);
22
+ else result.set(key, value);
23
+ }
24
+ return result;
25
+ }
26
+ /**
27
+ * Koa adapter for AFS HTTP handler
28
+ *
29
+ * This adapter converts between Koa context and Web Standard Request/Response
30
+ * objects, handling both scenarios where body parsing middleware is configured
31
+ * (like koa-bodyparser) and where it isn't.
32
+ *
33
+ * @param handler - The AFS HTTP handler function
34
+ * @returns A Koa-compatible middleware
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import Koa from "koa";
39
+ * import Router from "@koa/router";
40
+ * import { createAFSHttpHandler, koaAdapter } from "@aigne/afs-http";
41
+ *
42
+ * const handler = createAFSHttpHandler({ module: provider });
43
+ * const app = new Koa();
44
+ * const router = new Router();
45
+ *
46
+ * router.post("/afs/rpc", koaAdapter(handler));
47
+ * app.use(router.routes());
48
+ * ```
49
+ */
50
+ function koaAdapter(handler) {
51
+ return async (ctx, _next) => {
52
+ const url = `${ctx.protocol}://${ctx.host}${ctx.originalUrl}`;
53
+ let body;
54
+ if (ctx.method === "POST" || ctx.method === "PUT" || ctx.method === "PATCH") {
55
+ const requestBody = ctx.request.body;
56
+ if (requestBody !== void 0 && requestBody !== null && typeof requestBody === "object" && Object.keys(requestBody).length > 0) body = JSON.stringify(requestBody);
57
+ else body = await streamToString(ctx.req);
58
+ }
59
+ const response = await handler(new Request(url, {
60
+ method: ctx.method,
61
+ headers: convertHeaders(ctx.request.headers),
62
+ body
63
+ }));
64
+ ctx.status = response.status;
65
+ response.headers.forEach((value, key) => {
66
+ ctx.set(key, value);
67
+ });
68
+ ctx.body = await response.text();
69
+ };
70
+ }
71
+
72
+ //#endregion
73
+ exports.koaAdapter = koaAdapter;