@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.
- package/LICENSE.md +26 -0
- package/README.md +318 -0
- package/dist/adapters/express.cjs +74 -0
- package/dist/adapters/express.d.cts +56 -0
- package/dist/adapters/express.d.cts.map +1 -0
- package/dist/adapters/express.d.mts +56 -0
- package/dist/adapters/express.d.mts.map +1 -0
- package/dist/adapters/express.mjs +74 -0
- package/dist/adapters/express.mjs.map +1 -0
- package/dist/adapters/koa.cjs +73 -0
- package/dist/adapters/koa.d.cts +56 -0
- package/dist/adapters/koa.d.cts.map +1 -0
- package/dist/adapters/koa.d.mts +56 -0
- package/dist/adapters/koa.d.mts.map +1 -0
- package/dist/adapters/koa.mjs +73 -0
- package/dist/adapters/koa.mjs.map +1 -0
- package/dist/client.cjs +143 -0
- package/dist/client.d.cts +70 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +70 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +144 -0
- package/dist/client.mjs.map +1 -0
- package/dist/errors.cjs +105 -0
- package/dist/errors.d.cts +63 -0
- package/dist/errors.d.cts.map +1 -0
- package/dist/errors.d.mts +63 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +98 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/handler.cjs +126 -0
- package/dist/handler.d.cts +43 -0
- package/dist/handler.d.cts.map +1 -0
- package/dist/handler.d.mts +43 -0
- package/dist/handler.d.mts.map +1 -0
- package/dist/handler.mjs +127 -0
- package/dist/handler.mjs.map +1 -0
- package/dist/index.cjs +33 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +9 -0
- package/dist/protocol.cjs +68 -0
- package/dist/protocol.d.cts +119 -0
- package/dist/protocol.d.cts.map +1 -0
- package/dist/protocol.d.mts +119 -0
- package/dist/protocol.d.mts.map +1 -0
- package/dist/protocol.mjs +64 -0
- package/dist/protocol.mjs.map +1 -0
- package/dist/retry.cjs +111 -0
- package/dist/retry.d.cts +57 -0
- package/dist/retry.d.cts.map +1 -0
- package/dist/retry.d.mts +57 -0
- package/dist/retry.d.mts.map +1 -0
- package/dist/retry.mjs +105 -0
- package/dist/retry.mjs.map +1 -0
- 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;
|