@zshuangmu/agenthub 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 +176 -0
- package/package.json +41 -0
- package/src/api-server.js +195 -0
- package/src/cli.js +343 -0
- package/src/commands/api.js +8 -0
- package/src/commands/info.js +6 -0
- package/src/commands/install.js +10 -0
- package/src/commands/list.js +91 -0
- package/src/commands/pack.js +151 -0
- package/src/commands/publish-remote.js +9 -0
- package/src/commands/publish.js +7 -0
- package/src/commands/rollback.js +64 -0
- package/src/commands/search.js +7 -0
- package/src/commands/serve.js +9 -0
- package/src/commands/stats.js +90 -0
- package/src/commands/update.js +68 -0
- package/src/commands/versions.js +63 -0
- package/src/commands/web.js +8 -0
- package/src/index.js +14 -0
- package/src/lib/bundle-transfer.js +58 -0
- package/src/lib/database.js +244 -0
- package/src/lib/download-stats.js +77 -0
- package/src/lib/fs-utils.js +46 -0
- package/src/lib/html.js +1730 -0
- package/src/lib/http.js +24 -0
- package/src/lib/install.js +14 -0
- package/src/lib/manifest.js +123 -0
- package/src/lib/openclaw-config.js +40 -0
- package/src/lib/registry.js +64 -0
- package/src/lib/security-scanner.js +233 -0
- package/src/lib/skill-md.js +17 -0
- package/src/server.js +158 -0
- package/src/web-server.js +138 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AgentHub
|
|
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,176 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🤖 AgentHub
|
|
4
|
+
|
|
5
|
+
**The Open Source Marketplace for AI Agents**
|
|
6
|
+
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://github.com/agenthub/agenthub)
|
|
10
|
+
|
|
11
|
+
**Package and upload your Agent's full capabilities in one command.<br>Download and gain those powers with one click.**
|
|
12
|
+
|
|
13
|
+
🌐 **Live Demo**: [https://agenthub.cyou](https://agenthub.cyou/)
|
|
14
|
+
|
|
15
|
+
[English](README.md) | [中文](README_CN.md)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🎯 What is AgentHub?
|
|
22
|
+
|
|
23
|
+
AgentHub is an open-source platform for packaging and distributing AI Agents. It allows you to:
|
|
24
|
+
|
|
25
|
+
- **📦 Package** your Agent's personality, memory, and skills into a single bundle
|
|
26
|
+
- **🚀 Publish** to a local or remote registry with one command
|
|
27
|
+
- **🔍 Discover** and search agents from the marketplace
|
|
28
|
+
- **⚡ Install** agents to any workspace instantly
|
|
29
|
+
|
|
30
|
+
Perfect for teams who want to share AI capabilities across projects, or individuals who want to backup and version their agent configurations.
|
|
31
|
+
|
|
32
|
+
## ✨ Features
|
|
33
|
+
|
|
34
|
+
| Feature | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| 📦 **One-Click Packaging** | Automatically scan workspace and package Agent capabilities |
|
|
37
|
+
| 🚀 **Local-First** | No cloud required, runs entirely on your machine |
|
|
38
|
+
| 🌐 **Web Interface** | Beautiful dark-themed UI with i18n support (EN/中文) |
|
|
39
|
+
| 🔐 **Memory Layers** | Three-tier memory: public, portable, private |
|
|
40
|
+
| 🔄 **Version Control** | Full version history with rollback support |
|
|
41
|
+
| 📊 **Analytics** | Built-in download tracking and statistics |
|
|
42
|
+
|
|
43
|
+
## 📸 Screenshots
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
## 🚀 Quick Start
|
|
47
|
+
|
|
48
|
+
### Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Clone the repository
|
|
52
|
+
git clone https://github.com/agenthub/agenthub.git
|
|
53
|
+
cd agenthub
|
|
54
|
+
|
|
55
|
+
# Install dependencies
|
|
56
|
+
npm install
|
|
57
|
+
|
|
58
|
+
# Link globally (optional)
|
|
59
|
+
npm link
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Basic Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. Pack your agent
|
|
66
|
+
agenthub pack --workspace ./my-agent --config openclaw.json
|
|
67
|
+
|
|
68
|
+
# 2. Publish to registry
|
|
69
|
+
agenthub publish ./bundles/my-agent.agent --registry ./.registry
|
|
70
|
+
|
|
71
|
+
# 3. Start web interface
|
|
72
|
+
agenthub serve --registry ./.registry --port 3000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Visit http://localhost:3000 to see your agents!
|
|
76
|
+
|
|
77
|
+
## 📖 Documentation
|
|
78
|
+
|
|
79
|
+
### Commands
|
|
80
|
+
|
|
81
|
+
| Command | Description |
|
|
82
|
+
|---------|-------------|
|
|
83
|
+
| `pack` | Pack workspace into Agent Bundle |
|
|
84
|
+
| `publish` | Publish to local registry |
|
|
85
|
+
| `publish-remote` | Publish to remote server |
|
|
86
|
+
| `search` | Search agents in registry |
|
|
87
|
+
| `info` | View agent details |
|
|
88
|
+
| `install` | Install agent to workspace |
|
|
89
|
+
| `serve` | Start web + API service |
|
|
90
|
+
| `api` | Start API server only |
|
|
91
|
+
| `web` | Start web frontend only |
|
|
92
|
+
|
|
93
|
+
### HTTP API
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# List all agents
|
|
97
|
+
curl http://localhost:3001/api/agents
|
|
98
|
+
|
|
99
|
+
# Search agents
|
|
100
|
+
curl "http://localhost:3001/api/agents?q=react"
|
|
101
|
+
|
|
102
|
+
# Get agent details
|
|
103
|
+
curl http://localhost:3001/api/agents/my-agent
|
|
104
|
+
|
|
105
|
+
# Get statistics
|
|
106
|
+
curl http://localhost:3001/api/stats
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### AI Auto-Discovery
|
|
110
|
+
|
|
111
|
+
Let your AI assistant automatically discover available agents:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
curl http://localhost:3001/api/skills/agenthub-discover
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 🏗️ Architecture
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
┌─────────────────┐ pack ┌─────────────────┐ publish ┌─────────────────┐
|
|
121
|
+
│ OpenClaw │ ──────────► │ Bundle │ ──────────► │ Registry │
|
|
122
|
+
│ Workspace │ │ (*.agent) │ │ (.registry) │
|
|
123
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
124
|
+
│
|
|
125
|
+
┌───────────────────────────────┘
|
|
126
|
+
│
|
|
127
|
+
▼
|
|
128
|
+
┌─────────────────┐
|
|
129
|
+
│ AgentHub │
|
|
130
|
+
│ Web + API │
|
|
131
|
+
└─────────────────┘
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 🧪 Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Run tests
|
|
138
|
+
npm test
|
|
139
|
+
|
|
140
|
+
# Start development server
|
|
141
|
+
node src/cli.js serve --registry ./.registry
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 🐳 Docker Deployment
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Production start (in container)
|
|
148
|
+
NODE_ENV=production node src/cli.js serve --registry ./.registry --port 3000 --host 0.0.0.0
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 🤝 Contributing
|
|
152
|
+
|
|
153
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
154
|
+
|
|
155
|
+
- 🐛 [Report a Bug](https://github.com/agenthub/agenthub/issues/new?template=bug_report.md)
|
|
156
|
+
- 💡 [Request a Feature](https://github.com/agenthub/agenthub/issues/new?template=feature_request.md)
|
|
157
|
+
- 🔧 [Submit a Pull Request](https://github.com/agenthub/agenthub/pulls)
|
|
158
|
+
|
|
159
|
+
## 📄 License
|
|
160
|
+
|
|
161
|
+
[MIT License](LICENSE) © AgentHub Team
|
|
162
|
+
|
|
163
|
+
## 🙏 Acknowledgments
|
|
164
|
+
|
|
165
|
+
- Built for [OpenClaw](https://github.com/openclaw) ecosystem
|
|
166
|
+
- Inspired by npm and Docker Hub
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
<div align="center">
|
|
171
|
+
|
|
172
|
+
**[⬆ Back to Top](#agenthub)**
|
|
173
|
+
|
|
174
|
+
Made with ❤️ by the AgentHub Team
|
|
175
|
+
|
|
176
|
+
</div>
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zshuangmu/agenthub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI Agent 打包与分发平台 - 一句话打包上传,一键下载获得能力",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agenthub": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "NODE_OPTIONS='' node --test",
|
|
15
|
+
"start": "node src/cli.js serve --registry ./.registry --port 3000 --host 0.0.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"agent",
|
|
20
|
+
"openclaw",
|
|
21
|
+
"package-manager",
|
|
22
|
+
"registry",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "AgentHub Team",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/itshaungmu/AgentHub.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/itshaungmu/AgentHub/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/itshaungmu/AgentHub#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"sql.js": "^1.14.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { infoCommand, installCommand, publishCommand, searchCommand } from "./index.js";
|
|
5
|
+
import { publishUploadedBundle } from "./lib/bundle-transfer.js";
|
|
6
|
+
import { notFound, readJsonBody, sendJson } from "./lib/http.js";
|
|
7
|
+
import {
|
|
8
|
+
initDatabase,
|
|
9
|
+
incrementDownloads,
|
|
10
|
+
getAgentDownloads,
|
|
11
|
+
getAgentsDownloads,
|
|
12
|
+
getTotalDownloads,
|
|
13
|
+
getDownloadRanking,
|
|
14
|
+
getRecentDownloads,
|
|
15
|
+
getDatabaseStats
|
|
16
|
+
} from "./lib/database.js";
|
|
17
|
+
|
|
18
|
+
// 安全配置
|
|
19
|
+
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];
|
|
20
|
+
const MAX_UPLOAD_SIZE = parseInt(process.env.MAX_UPLOAD_SIZE || "10485760", 10); // 10MB default
|
|
21
|
+
const RATE_LIMIT_WINDOW = 60000; // 1 minute
|
|
22
|
+
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || "100", 10); // 100 requests per minute
|
|
23
|
+
|
|
24
|
+
// 简单的速率限制器
|
|
25
|
+
const rateLimiter = new Map();
|
|
26
|
+
|
|
27
|
+
function checkRateLimit(ip) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
30
|
+
const requests = rateLimiter.get(ip) || [];
|
|
31
|
+
const recentRequests = requests.filter(t => t > windowStart);
|
|
32
|
+
if (recentRequests.length >= RATE_LIMIT_MAX) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
recentRequests.push(now);
|
|
36
|
+
rateLimiter.set(ip, recentRequests);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// slug 格式验证
|
|
41
|
+
function isValidSlug(slug) {
|
|
42
|
+
return /^[a-z0-9-]+$/.test(slug) && slug.length <= 100;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function createApiServer({ registryDir, port = 3000 }) {
|
|
46
|
+
// 初始化数据库
|
|
47
|
+
await initDatabase(registryDir);
|
|
48
|
+
|
|
49
|
+
const server = http.createServer(async (request, response) => {
|
|
50
|
+
const origin = request.headers.origin || "*";
|
|
51
|
+
const allowedOrigin = ALLOWED_ORIGINS.includes("*") || ALLOWED_ORIGINS.includes(origin)
|
|
52
|
+
? (ALLOWED_ORIGINS.includes("*") ? "*" : origin)
|
|
53
|
+
: ALLOWED_ORIGINS[0];
|
|
54
|
+
|
|
55
|
+
// CORS 头
|
|
56
|
+
const corsHeaders = {
|
|
57
|
+
"Access-Control-Allow-Origin": allowedOrigin,
|
|
58
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
59
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
60
|
+
"X-Content-Type-Options": "nosniff",
|
|
61
|
+
"X-Frame-Options": "DENY"
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// 速率限制检查
|
|
65
|
+
const clientIp = request.socket.remoteAddress;
|
|
66
|
+
if (!checkRateLimit(clientIp)) {
|
|
67
|
+
response.writeHead(429, { "Content-Type": "application/json", ...corsHeaders });
|
|
68
|
+
response.end(JSON.stringify({ error: "Too many requests" }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 处理预检请求
|
|
73
|
+
if (request.method === "OPTIONS") {
|
|
74
|
+
response.writeHead(204, corsHeaders);
|
|
75
|
+
response.end();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(request.url, "http://127.0.0.1");
|
|
81
|
+
|
|
82
|
+
// API: 获取 AgentHub Discover Skill
|
|
83
|
+
if (url.pathname === "/api/skills/agenthub-discover") {
|
|
84
|
+
try {
|
|
85
|
+
const skillPath = path.join(process.cwd(), "skills", "agenthub-discover", "SKILL.md");
|
|
86
|
+
const content = await readFile(skillPath, "utf8");
|
|
87
|
+
response.writeHead(200, {
|
|
88
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
89
|
+
...corsHeaders
|
|
90
|
+
});
|
|
91
|
+
response.end(content);
|
|
92
|
+
} catch {
|
|
93
|
+
response.writeHead(404, { "Content-Type": "application/json", ...corsHeaders });
|
|
94
|
+
response.end(JSON.stringify({ error: "Skill not found" }));
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// API: 获取 Agent 列表
|
|
100
|
+
if (url.pathname === "/api/agents") {
|
|
101
|
+
const agents = await searchCommand(url.searchParams.get("q") ?? "", { registry: registryDir });
|
|
102
|
+
const slugs = agents.map(a => a.slug);
|
|
103
|
+
const downloads = await getAgentsDownloads(registryDir, slugs);
|
|
104
|
+
const agentsWithDownloads = agents.map(a => ({ ...a, downloads: downloads[a.slug] || 0 }));
|
|
105
|
+
sendJson(response, 200, { agents: agentsWithDownloads }, corsHeaders);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// API: 获取单个 Agent 详情
|
|
110
|
+
if (url.pathname.startsWith("/api/agents/")) {
|
|
111
|
+
const slug = url.pathname.slice("/api/agents/".length);
|
|
112
|
+
// 安全检查:验证 slug 格式
|
|
113
|
+
if (!isValidSlug(slug)) {
|
|
114
|
+
sendJson(response, 400, { error: "Invalid slug format" }, corsHeaders);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const manifest = await infoCommand(slug, { registry: registryDir });
|
|
118
|
+
const downloads = await getAgentDownloads(registryDir, slug);
|
|
119
|
+
sendJson(response, 200, { ...manifest, downloads }, corsHeaders);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// API: 发布 Agent
|
|
124
|
+
if (url.pathname === "/api/publish" && request.method === "POST") {
|
|
125
|
+
const body = await readJsonBody(request);
|
|
126
|
+
const manifest = await publishCommand(body.bundleDir, { registry: registryDir });
|
|
127
|
+
sendJson(response, 200, manifest, corsHeaders);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// API: 上传发布 Agent
|
|
132
|
+
if (url.pathname === "/api/publish-upload" && request.method === "POST") {
|
|
133
|
+
const body = await readJsonBody(request);
|
|
134
|
+
const manifest = await publishUploadedBundle({ payload: body, registryDir });
|
|
135
|
+
sendJson(response, 200, manifest, corsHeaders);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// API: 安装 Agent(记录下载)
|
|
140
|
+
if (url.pathname === "/api/install" && request.method === "POST") {
|
|
141
|
+
const body = await readJsonBody(request);
|
|
142
|
+
const result = await installCommand(body.agent, {
|
|
143
|
+
registry: registryDir,
|
|
144
|
+
targetWorkspace: body.targetWorkspace,
|
|
145
|
+
});
|
|
146
|
+
const slug = body.agent.split(":")[0];
|
|
147
|
+
await incrementDownloads(registryDir, slug, {
|
|
148
|
+
targetWorkspace: body.targetWorkspace,
|
|
149
|
+
ip: request.socket.remoteAddress,
|
|
150
|
+
userAgent: request.headers['user-agent']
|
|
151
|
+
});
|
|
152
|
+
sendJson(response, 200, result, corsHeaders);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// API: 获取下载统计
|
|
157
|
+
if (url.pathname === "/api/stats") {
|
|
158
|
+
const stats = await getDatabaseStats(registryDir);
|
|
159
|
+
const ranking = await getDownloadRanking(registryDir, 10);
|
|
160
|
+
const recent = await getRecentDownloads(registryDir, 20);
|
|
161
|
+
sendJson(response, 200, { stats, ranking, recent }, corsHeaders);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// API: 获取下载排行
|
|
166
|
+
if (url.pathname === "/api/stats/ranking") {
|
|
167
|
+
const limit = parseInt(url.searchParams.get("limit") || "10", 10);
|
|
168
|
+
const ranking = await getDownloadRanking(registryDir, limit);
|
|
169
|
+
sendJson(response, 200, { ranking }, corsHeaders);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// API: 健康检查
|
|
174
|
+
if (url.pathname === "/api/health") {
|
|
175
|
+
sendJson(response, 200, { status: "ok", timestamp: new Date().toISOString() }, corsHeaders);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
notFound(response, corsHeaders);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
sendJson(response, 500, { error: error.message }, corsHeaders);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await new Promise((resolve) => server.listen(port, "0.0.0.0", resolve));
|
|
186
|
+
const address = server.address();
|
|
187
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
server,
|
|
191
|
+
port: actualPort,
|
|
192
|
+
baseUrl: `http://127.0.0.1:${actualPort}`,
|
|
193
|
+
close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))),
|
|
194
|
+
};
|
|
195
|
+
}
|