@actiondock/core 2.0.11 → 2.2.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/README.md +61 -30
- package/dist/app/app.d.ts +73 -0
- package/dist/app/app.js +700 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/types.d.ts +283 -0
- package/dist/app/types.js +1 -0
- package/dist/catalog/action-index.d.ts +9 -0
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +20 -0
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -0
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +15 -0
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +7 -0
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +68 -0
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +7 -0
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/types.d.ts +23 -0
- package/dist/doctor/types.js +1 -0
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +123 -0
- package/dist/execution/index.d.ts +2 -0
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +79 -0
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +131 -0
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -0
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +71 -0
- package/dist/export/templates.js +621 -0
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.js +1 -0
- package/dist/filter/intent.d.ts +33 -0
- package/dist/filter/intent.js +123 -0
- package/dist/host/host.d.ts +55 -0
- package/dist/host/host.js +740 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.js +2 -0
- package/dist/host/types.d.ts +81 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +20 -0
- package/dist/ipc/diagnostic.d.ts +65 -0
- package/dist/ipc/diagnostic.js +129 -0
- package/dist/ipc/host.d.ts +7 -0
- package/dist/ipc/host.js +93 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +4 -0
- package/dist/ipc/target.d.ts +56 -0
- package/dist/ipc/target.js +297 -0
- package/dist/ipc/types.d.ts +42 -0
- package/dist/ipc/types.js +1 -0
- package/dist/platform/default.d.ts +32 -0
- package/dist/platform/default.js +40 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/node-fs.d.ts +37 -0
- package/dist/platform/node-fs.js +81 -0
- package/dist/platform/types.d.ts +104 -0
- package/dist/platform/types.js +1 -0
- package/dist/profile/client.d.ts +162 -0
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -0
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +47 -0
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +61 -0
- package/dist/profile/types.js +1 -0
- package/dist/project/closure.d.ts +25 -0
- package/dist/project/closure.js +68 -0
- package/dist/project/digest.d.ts +29 -0
- package/dist/project/digest.js +324 -0
- package/dist/project/index.d.ts +10 -0
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +26 -0
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +99 -0
- package/dist/project/loader.js +484 -0
- package/dist/project/lockfile.d.ts +55 -0
- package/dist/project/lockfile.js +175 -0
- package/dist/project/manifest.d.ts +22 -0
- package/dist/project/manifest.js +220 -0
- package/dist/project/resolver.d.ts +116 -0
- package/dist/project/resolver.js +376 -0
- package/dist/project/transactions.d.ts +103 -0
- package/dist/project/transactions.js +1203 -0
- package/dist/project/types-generator.d.ts +33 -0
- package/dist/project/types-generator.js +168 -0
- package/dist/project/types.d.ts +107 -0
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +5 -0
- package/dist/registry/lock.d.ts +27 -0
- package/dist/registry/lock.js +93 -0
- package/dist/registry/registry.d.ts +45 -0
- package/dist/registry/registry.js +611 -0
- package/dist/registry/resolve.d.ts +62 -0
- package/dist/registry/resolve.js +195 -0
- package/dist/registry/scan.d.ts +38 -0
- package/dist/registry/scan.js +135 -0
- package/dist/registry/types.d.ts +117 -0
- package/dist/registry/types.js +1 -0
- package/dist/runtime/action-collection.d.ts +27 -0
- package/dist/runtime/action-collection.js +57 -0
- package/dist/runtime/clock.d.ts +19 -0
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +86 -0
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +56 -0
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +93 -0
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +48 -0
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +34 -0
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +9 -0
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +293 -0
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +89 -0
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.d.ts +28 -0
- package/dist/schema/validator.js +116 -0
- package/dist/server/body.d.ts +40 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +5 -0
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +47 -0
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +6 -0
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +5 -0
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +5 -0
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -0
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +7 -0
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +5 -0
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +5 -0
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +6 -0
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +44 -0
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +15 -0
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +78 -0
- package/dist/server/types.js +1 -0
- package/dist/storage/data-dir-lock.d.ts +119 -0
- package/dist/storage/data-dir-lock.js +1016 -0
- package/dist/storage/driver.d.ts +9 -0
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +64 -0
- package/dist/storage/index.js +98 -0
- package/dist/storage/lazy.d.ts +8 -0
- package/dist/storage/lazy.js +44 -0
- package/dist/storage/mask.d.ts +26 -0
- package/dist/storage/mask.js +48 -0
- package/dist/storage/params.d.ts +13 -0
- package/dist/storage/params.js +32 -0
- package/dist/storage/sqlite.d.ts +79 -0
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +172 -0
- package/dist/storage/types.js +14 -0
- package/dist/target/index.d.ts +4 -0
- package/dist/target/index.js +4 -0
- package/dist/target/local.d.ts +52 -0
- package/dist/target/local.js +343 -0
- package/dist/target/remote.d.ts +76 -0
- package/dist/target/remote.js +839 -0
- package/dist/target/target.d.ts +6 -0
- package/dist/target/target.js +35 -0
- package/dist/target/types.d.ts +246 -0
- package/dist/target/types.js +34 -0
- package/dist/utils/index.d.ts +49 -0
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +4 -0
- package/package.json +13 -13
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +0 -59
- package/src/catalog/action-index.ts +0 -92
- package/src/catalog/action-resolver.ts +0 -120
- package/src/catalog/index.ts +0 -5
- package/src/catalog/location-registry.ts +0 -134
- package/src/catalog/package-catalog.ts +0 -87
- package/src/catalog/types.ts +0 -75
- package/src/doctor/doctor.ts +0 -419
- package/src/doctor/index.ts +0 -2
- package/src/doctor/types.ts +0 -25
- package/src/execution/index.ts +0 -2
- package/src/execution/service.ts +0 -322
- package/src/execution/types.ts +0 -73
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +0 -590
- package/src/filter/index.ts +0 -1
- package/src/filter/intent.ts +0 -154
- package/src/index.ts +0 -16
- package/src/profile/client.ts +0 -552
- package/src/profile/index.ts +0 -3
- package/src/profile/manager.ts +0 -341
- package/src/profile/types.ts +0 -71
- package/src/project/index.ts +0 -4
- package/src/project/init.ts +0 -242
- package/src/project/loader.ts +0 -479
- package/src/project/manifest.ts +0 -299
- package/src/project/types.ts +0 -141
- package/src/registry/index.ts +0 -2
- package/src/registry/registry.ts +0 -940
- package/src/registry/types.ts +0 -127
- package/src/runtime/clock.ts +0 -41
- package/src/runtime/context.ts +0 -259
- package/src/runtime/env.ts +0 -172
- package/src/runtime/events.ts +0 -142
- package/src/runtime/execution-manager.ts +0 -74
- package/src/runtime/index.ts +0 -10
- package/src/runtime/module-loader.ts +0 -67
- package/src/runtime/process.ts +0 -245
- package/src/runtime/runner.ts +0 -527
- package/src/runtime/standalone.ts +0 -429
- package/src/schema/validator.ts +0 -61
- package/src/server/body.ts +0 -112
- package/src/server/index.ts +0 -7
- package/src/server/routes/actions.ts +0 -262
- package/src/server/routes/common.ts +0 -126
- package/src/server/routes/config.ts +0 -147
- package/src/server/routes/doctor.ts +0 -29
- package/src/server/routes/health.ts +0 -40
- package/src/server/routes/index.ts +0 -9
- package/src/server/routes/info.ts +0 -221
- package/src/server/routes/playbooks.ts +0 -109
- package/src/server/routes/runs.ts +0 -301
- package/src/server/routes/state.ts +0 -133
- package/src/server/runtime-registry.ts +0 -115
- package/src/server/security.ts +0 -115
- package/src/server/server.ts +0 -251
- package/src/server/types.ts +0 -57
- package/src/storage/driver.ts +0 -126
- package/src/storage/index.ts +0 -81
- package/src/storage/mask.ts +0 -34
- package/src/storage/sqlite.ts +0 -714
- package/src/storage/types.ts +0 -139
- package/src/utils/index.ts +0 -62
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 恒定时间字符串比较(Constant-time comparison)。
|
|
3
|
+
* 使用底层的 `crypto.timingSafeEqual` 防范时序攻击(Timing Attack)。
|
|
4
|
+
*
|
|
5
|
+
* @param a 字符串 A
|
|
6
|
+
* @param b 字符串 B
|
|
7
|
+
* @returns 两个字符串内容是否完全一致
|
|
8
|
+
*/
|
|
9
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 检查指定的主机地址是否为本地回环接口(Loopback Host)。
|
|
12
|
+
* 支持 127.0.0.1, localhost, ::1 等形式。
|
|
13
|
+
*
|
|
14
|
+
* @param host 主机名或 IP 字符串
|
|
15
|
+
*/
|
|
16
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 校验 HTTP 请求中的鉴权令牌选项。
|
|
19
|
+
*/
|
|
20
|
+
export interface VerifyBearerTokenOptions {
|
|
21
|
+
/** 是否允许通过 URL 查询参数携带 Token(默认 false) */
|
|
22
|
+
allowQueryToken?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 校验 HTTP 请求中的鉴权令牌是否有效。
|
|
26
|
+
*
|
|
27
|
+
* 默认仅接受 HTTP 请求头: `Authorization: Bearer <token>`。
|
|
28
|
+
* 仅当 options.allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
|
|
29
|
+
*
|
|
30
|
+
* @param req 传入的 HTTP Request 对象
|
|
31
|
+
* @param expectedToken 服务端预期的正确 Token(若未配置 Token 则默认放行)
|
|
32
|
+
* @param options 校验选项或布尔值 allowQueryToken 开关
|
|
33
|
+
* @returns 是否鉴权成功
|
|
34
|
+
*/
|
|
35
|
+
export declare function verifyBearerToken(req: Request, expectedToken?: string, options?: VerifyBearerTokenOptions | boolean): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 根据配置的 CORS 白名单解析并返回跨域响应头。
|
|
38
|
+
* 默认情况下若未配置 corsOrigins,返回空对象(即默认关闭跨域访问,防范 CSRF)。
|
|
39
|
+
*
|
|
40
|
+
* @param origin 请求头中的 Origin 字段
|
|
41
|
+
* @param allowedOrigins 允许跨域的白名单来源数组
|
|
42
|
+
* @returns 包含 Access-Control-* 的响应头对象
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveCorsHeaders(origin: string | null, allowedOrigins?: string[]): Record<string, string>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* 恒定时间字符串比较(Constant-time comparison)。
|
|
4
|
+
* 使用底层的 `crypto.timingSafeEqual` 防范时序攻击(Timing Attack)。
|
|
5
|
+
*
|
|
6
|
+
* @param a 字符串 A
|
|
7
|
+
* @param b 字符串 B
|
|
8
|
+
* @returns 两个字符串内容是否完全一致
|
|
9
|
+
*/
|
|
10
|
+
export function safeEqual(a, b) {
|
|
11
|
+
const aa = Buffer.from(a);
|
|
12
|
+
const bb = Buffer.from(b);
|
|
13
|
+
if (aa.length !== bb.length) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return timingSafeEqual(aa, bb);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 检查指定的主机地址是否为本地回环接口(Loopback Host)。
|
|
20
|
+
* 支持 127.0.0.1, localhost, ::1 等形式。
|
|
21
|
+
*
|
|
22
|
+
* @param host 主机名或 IP 字符串
|
|
23
|
+
*/
|
|
24
|
+
export function isLoopbackHost(host) {
|
|
25
|
+
const trimmed = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
26
|
+
return (trimmed === "127.0.0.1" ||
|
|
27
|
+
trimmed === "::1" ||
|
|
28
|
+
trimmed === "localhost" ||
|
|
29
|
+
trimmed === "0:0:0:0:0:0:0:1");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 校验 HTTP 请求中的鉴权令牌是否有效。
|
|
33
|
+
*
|
|
34
|
+
* 默认仅接受 HTTP 请求头: `Authorization: Bearer <token>`。
|
|
35
|
+
* 仅当 options.allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
|
|
36
|
+
*
|
|
37
|
+
* @param req 传入的 HTTP Request 对象
|
|
38
|
+
* @param expectedToken 服务端预期的正确 Token(若未配置 Token 则默认放行)
|
|
39
|
+
* @param options 校验选项或布尔值 allowQueryToken 开关
|
|
40
|
+
* @returns 是否鉴权成功
|
|
41
|
+
*/
|
|
42
|
+
export function verifyBearerToken(req, expectedToken, options) {
|
|
43
|
+
if (!expectedToken || !expectedToken.trim()) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const trimmedExpected = expectedToken.trim();
|
|
47
|
+
// 1. 请求头 Authorization: Bearer <token>
|
|
48
|
+
const authHeader = req.headers.get("authorization");
|
|
49
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
50
|
+
const token = authHeader.slice(7).trim();
|
|
51
|
+
if (safeEqual(token, trimmedExpected)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2. URL 查询参数: ?token=<token>(仅当显式开启 allowQueryToken 时允许)
|
|
56
|
+
const allowQuery = typeof options === "boolean" ? options : options?.allowQueryToken === true;
|
|
57
|
+
if (allowQuery) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
const tokenParam = url.searchParams.get("token");
|
|
61
|
+
if (tokenParam && safeEqual(tokenParam.trim(), trimmedExpected)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// 畸形 URL 视作鉴权失败
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 根据配置的 CORS 白名单解析并返回跨域响应头。
|
|
73
|
+
* 默认情况下若未配置 corsOrigins,返回空对象(即默认关闭跨域访问,防范 CSRF)。
|
|
74
|
+
*
|
|
75
|
+
* @param origin 请求头中的 Origin 字段
|
|
76
|
+
* @param allowedOrigins 允许跨域的白名单来源数组
|
|
77
|
+
* @returns 包含 Access-Control-* 的响应头对象
|
|
78
|
+
*/
|
|
79
|
+
export function resolveCorsHeaders(origin, allowedOrigins) {
|
|
80
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
const hasWildcard = allowedOrigins.includes("*");
|
|
84
|
+
if (hasWildcard) {
|
|
85
|
+
return {
|
|
86
|
+
"Access-Control-Allow-Origin": "*",
|
|
87
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
88
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Idempotency-Key, X-Request-Id, Last-Event-ID",
|
|
89
|
+
Vary: "Origin",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
93
|
+
return {
|
|
94
|
+
"Access-Control-Allow-Origin": origin,
|
|
95
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
96
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Idempotency-Key, X-Request-Id, Last-Event-ID",
|
|
97
|
+
Vary: "Origin",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ActionDockServerInstance, CoreHttpServerInstance, ServerOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 规范化主机地址用于拼接 URL。
|
|
4
|
+
* 若 host 包含冒号 : 且不以 [ 开头,则添加中括号包裹(IPv6 标准格式)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatHostForUrl(host: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
|
|
9
|
+
*/
|
|
10
|
+
export declare function launchHttpServer(port: number, host: string, fetchHandler: (req: Request) => Promise<Response>): Promise<CoreHttpServerInstance>;
|
|
11
|
+
/**
|
|
12
|
+
* 启动 ActionDock 2.0 原生轻量级 HTTP 服务端。
|
|
13
|
+
* 作为 ActionDockHost 与 ActionDockTarget 的薄适配层,负责中间件流转、认证拦截与路由分发。
|
|
14
|
+
*/
|
|
15
|
+
export declare function startActionDockServer(options?: ServerOptions): Promise<ActionDockServerInstance>;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { createServer as createNodeHttpServer } from "node:http";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { createActionDockHost } from "../host/host.js";
|
|
6
|
+
import { NOT_FOUND, UNAUTHORIZED } from "../errors.js";
|
|
7
|
+
import { ensureDependencyClosure } from "../project/closure.js";
|
|
8
|
+
import { findProjectRoot } from "../project/loader.js";
|
|
9
|
+
import { listLinkedPackages, resolvePackageRoot } from "../registry/registry.js";
|
|
10
|
+
import { LocalActionDockTarget } from "../target/local.js";
|
|
11
|
+
import { handleActionsRoutes, handleConfigRoutes, handleDoctorRoute, handleHealthRoute, handleInfoRoute, handlePlaybooksRoutes, handleRunsRoutes, handleStateRoutes, jsonResponse, } from "./routes/index.js";
|
|
12
|
+
import { DEFAULT_MAX_BODY_BYTES } from "./body.js";
|
|
13
|
+
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security.js";
|
|
14
|
+
/**
|
|
15
|
+
* 规范化主机地址用于拼接 URL。
|
|
16
|
+
* 若 host 包含冒号 : 且不以 [ 开头,则添加中括号包裹(IPv6 标准格式)。
|
|
17
|
+
*/
|
|
18
|
+
export function formatHostForUrl(host) {
|
|
19
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
|
|
23
|
+
*/
|
|
24
|
+
export async function launchHttpServer(port, host, fetchHandler) {
|
|
25
|
+
const srv = createNodeHttpServer(async (req, res) => {
|
|
26
|
+
const ac = new AbortController();
|
|
27
|
+
const onReqClose = () => {
|
|
28
|
+
if (!req.complete) {
|
|
29
|
+
ac.abort(new Error("Request aborted by client"));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const onResClose = () => {
|
|
33
|
+
if (!res.writableFinished) {
|
|
34
|
+
ac.abort(new Error("Request aborted by client"));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
req.on("close", onReqClose);
|
|
38
|
+
res.on("close", onResClose);
|
|
39
|
+
try {
|
|
40
|
+
const protocol = req.socket?.encrypted ? "https" : "http";
|
|
41
|
+
const hostHeader = req.headers.host || "127.0.0.1";
|
|
42
|
+
const url = new URL(req.url || "/", `${protocol}://${hostHeader}`).href;
|
|
43
|
+
const headers = new Headers();
|
|
44
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
45
|
+
if (v === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
if (Array.isArray(v)) {
|
|
48
|
+
for (const item of v)
|
|
49
|
+
headers.append(k, item);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
headers.set(k, v);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const method = (req.method || "GET").toUpperCase();
|
|
56
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
57
|
+
const init = { method, headers, signal: ac.signal };
|
|
58
|
+
if (hasBody) {
|
|
59
|
+
init.body = Readable.toWeb(req);
|
|
60
|
+
init.duplex = "half";
|
|
61
|
+
}
|
|
62
|
+
const webReq = new Request(url, init);
|
|
63
|
+
const webRes = await fetchHandler(webReq);
|
|
64
|
+
res.statusCode = webRes.status;
|
|
65
|
+
if (webRes.statusText)
|
|
66
|
+
res.statusMessage = webRes.statusText;
|
|
67
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
68
|
+
if (!webRes.body) {
|
|
69
|
+
res.end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await pipeline(Readable.fromWeb(webRes.body), res);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (!res.headersSent) {
|
|
76
|
+
res.statusCode = 500;
|
|
77
|
+
res.end(JSON.stringify({ error: err?.message || String(err) }));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
res.destroy(err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
req.removeListener("close", onReqClose);
|
|
85
|
+
res.removeListener("close", onResClose);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const instance = {
|
|
89
|
+
port,
|
|
90
|
+
stop: async () => {
|
|
91
|
+
await new Promise((resolve) => {
|
|
92
|
+
srv.close(() => resolve());
|
|
93
|
+
srv.closeAllConnections?.();
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
await new Promise((resolve, reject) => {
|
|
98
|
+
srv.once("error", (err) => {
|
|
99
|
+
reject(err);
|
|
100
|
+
});
|
|
101
|
+
srv.listen(port, host, () => {
|
|
102
|
+
const addr = srv.address();
|
|
103
|
+
if (typeof addr === "object" && addr) {
|
|
104
|
+
instance.port = addr.port;
|
|
105
|
+
}
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
instance.ready = Promise.resolve();
|
|
110
|
+
return instance;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 启动 ActionDock 2.0 原生轻量级 HTTP 服务端。
|
|
114
|
+
* 作为 ActionDockHost 与 ActionDockTarget 的薄适配层,负责中间件流转、认证拦截与路由分发。
|
|
115
|
+
*/
|
|
116
|
+
export async function startActionDockServer(options = {}) {
|
|
117
|
+
let hostInstance = options.hostInstance ??
|
|
118
|
+
(options.host && typeof options.host === "object" && "listActions" in options.host
|
|
119
|
+
? options.host
|
|
120
|
+
: undefined);
|
|
121
|
+
let targetInstance = options.target;
|
|
122
|
+
const hostString = typeof options.host === "string"
|
|
123
|
+
? options.host
|
|
124
|
+
: (options.hostname ?? "127.0.0.1");
|
|
125
|
+
const port = options.port ?? 5177;
|
|
126
|
+
const host = hostString;
|
|
127
|
+
const token = options.token;
|
|
128
|
+
const customHome = options.customHome;
|
|
129
|
+
const projectRoot = options.projectRoot
|
|
130
|
+
? resolve(options.projectRoot)
|
|
131
|
+
: findProjectRoot(process.cwd());
|
|
132
|
+
// 非回环地址强制要求配置 Token 鉴权(防裸奔)
|
|
133
|
+
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
134
|
+
throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
|
|
135
|
+
}
|
|
136
|
+
// 若调用方未传入 host 或 target,通过 projectRoot、customHome、platform 等直接创建宿主
|
|
137
|
+
if (!targetInstance && !hostInstance) {
|
|
138
|
+
const scanLinkedPackages = options.scanLinkedPackages ?? !projectRoot;
|
|
139
|
+
hostInstance = await createActionDockHost({
|
|
140
|
+
projectRoot: projectRoot || undefined,
|
|
141
|
+
customHome,
|
|
142
|
+
platform: options.platform,
|
|
143
|
+
dataDir: options.dataDir,
|
|
144
|
+
inMemory: options.inMemory,
|
|
145
|
+
scanLinkedPackages,
|
|
146
|
+
autoLoadCurrentProject: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!targetInstance && hostInstance) {
|
|
150
|
+
targetInstance = new LocalActionDockTarget(hostInstance);
|
|
151
|
+
}
|
|
152
|
+
else if (targetInstance && !hostInstance) {
|
|
153
|
+
const inner = targetInstance?.unwrap?.();
|
|
154
|
+
if (inner && "listApps" in inner) {
|
|
155
|
+
hostInstance = inner;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const roots = [];
|
|
159
|
+
if (projectRoot) {
|
|
160
|
+
roots.push(projectRoot);
|
|
161
|
+
}
|
|
162
|
+
if (options.packageAllowlist && options.packageAllowlist.length > 0) {
|
|
163
|
+
for (const pkgId of options.packageAllowlist) {
|
|
164
|
+
const r = resolvePackageRoot(pkgId, projectRoot || undefined, customHome);
|
|
165
|
+
if (r && !roots.includes(r)) {
|
|
166
|
+
roots.push(r);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (!projectRoot) {
|
|
171
|
+
for (const pkg of listLinkedPackages(customHome)) {
|
|
172
|
+
if (!roots.includes(pkg.path)) {
|
|
173
|
+
roots.push(pkg.path);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (roots.length > 0) {
|
|
178
|
+
await ensureDependencyClosure(roots, { customHome });
|
|
179
|
+
}
|
|
180
|
+
const fetchHandler = async (req) => {
|
|
181
|
+
const origin = req.headers.get("origin");
|
|
182
|
+
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
183
|
+
if (req.method === "OPTIONS") {
|
|
184
|
+
return new Response(null, {
|
|
185
|
+
status: 204,
|
|
186
|
+
headers: corsHeaders,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const url = new URL(req.url);
|
|
190
|
+
const pathname = url.pathname;
|
|
191
|
+
const ctx = {
|
|
192
|
+
req,
|
|
193
|
+
url,
|
|
194
|
+
pathname,
|
|
195
|
+
corsHeaders,
|
|
196
|
+
projectRoot,
|
|
197
|
+
customHome,
|
|
198
|
+
host: hostInstance,
|
|
199
|
+
target: targetInstance,
|
|
200
|
+
options,
|
|
201
|
+
};
|
|
202
|
+
// 1. 健康检查路由(内部处理独立鉴权逻辑)
|
|
203
|
+
const healthResponse = await handleHealthRoute(ctx);
|
|
204
|
+
if (healthResponse) {
|
|
205
|
+
return healthResponse;
|
|
206
|
+
}
|
|
207
|
+
// 2. MCP 统一网关端点
|
|
208
|
+
if (options.enableMcp !== false &&
|
|
209
|
+
options.mcpHandler &&
|
|
210
|
+
(pathname === "/mcp" || pathname.startsWith("/mcp/"))) {
|
|
211
|
+
if (!verifyBearerToken(req, token, options)) {
|
|
212
|
+
return jsonResponse({
|
|
213
|
+
ok: false,
|
|
214
|
+
error: {
|
|
215
|
+
code: UNAUTHORIZED,
|
|
216
|
+
message: "Invalid or missing Bearer token",
|
|
217
|
+
},
|
|
218
|
+
}, 401, corsHeaders);
|
|
219
|
+
}
|
|
220
|
+
// 请求体体积限制保护
|
|
221
|
+
const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
222
|
+
const contentLengthHeader = req.headers.get("content-length");
|
|
223
|
+
if (contentLengthHeader) {
|
|
224
|
+
const parsedLength = parseInt(contentLengthHeader, 10);
|
|
225
|
+
if (!isNaN(parsedLength) && parsedLength > maxBytes) {
|
|
226
|
+
return jsonResponse({
|
|
227
|
+
ok: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: "REQUEST_TOO_LARGE",
|
|
230
|
+
message: "Request body exceeds maximum allowed size",
|
|
231
|
+
},
|
|
232
|
+
}, 413, corsHeaders);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let mcpReq = req;
|
|
236
|
+
if (req.body && req.method !== "GET" && req.method !== "HEAD") {
|
|
237
|
+
const reader = req.body.getReader();
|
|
238
|
+
const chunks = [];
|
|
239
|
+
let totalBytes = 0;
|
|
240
|
+
let tooLarge = false;
|
|
241
|
+
try {
|
|
242
|
+
while (true) {
|
|
243
|
+
const { done, value } = await reader.read();
|
|
244
|
+
if (done)
|
|
245
|
+
break;
|
|
246
|
+
if (value) {
|
|
247
|
+
totalBytes += value.byteLength;
|
|
248
|
+
if (totalBytes > maxBytes) {
|
|
249
|
+
tooLarge = true;
|
|
250
|
+
await reader.cancel();
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
chunks.push(value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
reader.releaseLock();
|
|
259
|
+
}
|
|
260
|
+
if (tooLarge) {
|
|
261
|
+
return jsonResponse({
|
|
262
|
+
ok: false,
|
|
263
|
+
error: {
|
|
264
|
+
code: "REQUEST_TOO_LARGE",
|
|
265
|
+
message: "Request body exceeds maximum allowed size",
|
|
266
|
+
},
|
|
267
|
+
}, 413, corsHeaders);
|
|
268
|
+
}
|
|
269
|
+
const merged = new Uint8Array(totalBytes);
|
|
270
|
+
let offset = 0;
|
|
271
|
+
for (const chunk of chunks) {
|
|
272
|
+
merged.set(chunk, offset);
|
|
273
|
+
offset += chunk.byteLength;
|
|
274
|
+
}
|
|
275
|
+
mcpReq = new Request(req.url, {
|
|
276
|
+
method: req.method,
|
|
277
|
+
headers: req.headers,
|
|
278
|
+
body: merged,
|
|
279
|
+
signal: req.signal,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const mcpRes = await options.mcpHandler(mcpReq);
|
|
283
|
+
if (mcpRes)
|
|
284
|
+
return mcpRes;
|
|
285
|
+
}
|
|
286
|
+
// 3. 全局 API 认证鉴权拦截
|
|
287
|
+
if (!verifyBearerToken(req, token, options)) {
|
|
288
|
+
return jsonResponse({
|
|
289
|
+
ok: false,
|
|
290
|
+
error: {
|
|
291
|
+
code: UNAUTHORIZED,
|
|
292
|
+
message: "Invalid or missing Bearer token",
|
|
293
|
+
},
|
|
294
|
+
}, 401, corsHeaders);
|
|
295
|
+
}
|
|
296
|
+
// 4. 业务领域路由分发(完全委托 Target / Host)
|
|
297
|
+
const routeResponse = (await handleInfoRoute(ctx)) ||
|
|
298
|
+
(await handleDoctorRoute(ctx)) ||
|
|
299
|
+
(await handleActionsRoutes(ctx)) ||
|
|
300
|
+
(await handlePlaybooksRoutes(ctx)) ||
|
|
301
|
+
(await handleRunsRoutes(ctx)) ||
|
|
302
|
+
(await handleStateRoutes(ctx)) ||
|
|
303
|
+
(await handleConfigRoutes(ctx));
|
|
304
|
+
if (routeResponse) {
|
|
305
|
+
return routeResponse;
|
|
306
|
+
}
|
|
307
|
+
// 5. 404 路由兜底
|
|
308
|
+
return jsonResponse({
|
|
309
|
+
ok: false,
|
|
310
|
+
error: {
|
|
311
|
+
code: NOT_FOUND,
|
|
312
|
+
message: `Route not found: ${req.method} ${pathname}`,
|
|
313
|
+
},
|
|
314
|
+
}, 404, corsHeaders);
|
|
315
|
+
};
|
|
316
|
+
const server = await launchHttpServer(port, host, fetchHandler);
|
|
317
|
+
if (server.ready) {
|
|
318
|
+
await server.ready;
|
|
319
|
+
}
|
|
320
|
+
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
321
|
+
const instance = {
|
|
322
|
+
get port() {
|
|
323
|
+
return server.port ?? port;
|
|
324
|
+
},
|
|
325
|
+
set port(val) {
|
|
326
|
+
server.port = val;
|
|
327
|
+
},
|
|
328
|
+
host: hostInstance,
|
|
329
|
+
target: targetInstance,
|
|
330
|
+
get url() {
|
|
331
|
+
return `http://${formatHostForUrl(actualHost)}:${this.port}`;
|
|
332
|
+
},
|
|
333
|
+
ready: Promise.resolve(),
|
|
334
|
+
stop: async (stopOptions) => {
|
|
335
|
+
if (targetInstance) {
|
|
336
|
+
try {
|
|
337
|
+
await targetInstance.close();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// 忽略关闭异常
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (hostInstance && hostInstance !== targetInstance?.unwrap?.()) {
|
|
344
|
+
try {
|
|
345
|
+
await hostInstance.close(stopOptions);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// 忽略宿主关闭异常
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
await server.stop(true);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
return instance;
|
|
355
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ActionDockHost } from "../host/types.js";
|
|
2
|
+
import type { ActionDockTarget } from "../target/types.js";
|
|
3
|
+
export interface CoreHttpServerInstance {
|
|
4
|
+
port: number;
|
|
5
|
+
stop: (closeActiveConnections?: boolean) => void | Promise<void>;
|
|
6
|
+
ready?: Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export type CoreHttpServerFactory = (options: {
|
|
9
|
+
port: number;
|
|
10
|
+
host: string;
|
|
11
|
+
fetch: (req: Request) => Promise<Response>;
|
|
12
|
+
}) => CoreHttpServerInstance | Promise<CoreHttpServerInstance>;
|
|
13
|
+
/**
|
|
14
|
+
* 启动 ActionDock HTTP Runner 服务端的配置选项。
|
|
15
|
+
*/
|
|
16
|
+
export interface ServerOptions {
|
|
17
|
+
/** 监听端口号(默认 5177) */
|
|
18
|
+
port?: number;
|
|
19
|
+
/** 绑定监听的主机地址(默认 "127.0.0.1"),或关联的 ActionDockHost 宿主实例 */
|
|
20
|
+
host?: string | ActionDockHost;
|
|
21
|
+
/** 显式绑定的 ActionDockHost 宿主实例(向前兼容别名) */
|
|
22
|
+
hostInstance?: ActionDockHost;
|
|
23
|
+
/** 关联的目标 ActionDockTarget 门面实例 */
|
|
24
|
+
target?: ActionDockTarget;
|
|
25
|
+
/** 显式绑定的主机地址(当 host 传入 ActionDockHost 时的可选覆盖项) */
|
|
26
|
+
hostname?: string;
|
|
27
|
+
/** 用于 HTTP Bearer Token 鉴权的密钥令牌 */
|
|
28
|
+
token?: string;
|
|
29
|
+
/** 是否允许通过 URL 查询参数携带 Token 进行鉴权(默认 false,关闭以防泄露) */
|
|
30
|
+
allowQueryToken?: boolean;
|
|
31
|
+
/** 服务的项目根目录(可选,若未指定则自动向上查找或进入全局 Registry 模式) */
|
|
32
|
+
projectRoot?: string;
|
|
33
|
+
/** 自定义 ActionDock 用户家目录 */
|
|
34
|
+
customHome?: string;
|
|
35
|
+
/** 数据持久化存储目录 */
|
|
36
|
+
dataDir?: string;
|
|
37
|
+
/** 是否采用纯内存运行模式 */
|
|
38
|
+
inMemory?: boolean;
|
|
39
|
+
/** 是否允许非回环地址(如 0.0.0.0)在未配置 Token 的情况下启动(不安全) */
|
|
40
|
+
allowInsecureNoAuth?: boolean;
|
|
41
|
+
/** 允许跨域请求的 CORS Origin 白名单列表 */
|
|
42
|
+
corsOrigins?: string[];
|
|
43
|
+
/** 允许访问执行的 Package ID 白名单列表(为空允许全部) */
|
|
44
|
+
packageAllowlist?: string[];
|
|
45
|
+
/** 最大允许的请求体字节限制(默认 1MB,防 DoS) */
|
|
46
|
+
maxBodyBytes?: number;
|
|
47
|
+
/** 是否在 health 和 info 接口中透传本地 projectRoot 等调试路径 */
|
|
48
|
+
exposeDebugInfo?: boolean;
|
|
49
|
+
/** 是否启用一体化 MCP 协议支持(默认开启) */
|
|
50
|
+
enableMcp?: boolean;
|
|
51
|
+
/** 自定义 MCP 请求处理器钩子(若挂载则 /mcp 路由交由其处理) */
|
|
52
|
+
mcpHandler?: (req: Request) => Promise<Response | null | undefined> | Response | null | undefined;
|
|
53
|
+
/** 可选注入的统一运行时底层平台 */
|
|
54
|
+
platform?: import("../platform/types.js").RuntimePlatform;
|
|
55
|
+
/** 是否显式开启配置与状态管理路由能力(默认关闭,关闭时返回 403) */
|
|
56
|
+
enableManagement?: boolean;
|
|
57
|
+
/** 是否扫描并加载外部链接包(默认在未指定 projectRoot 时为 true,指定时为 false) */
|
|
58
|
+
scanLinkedPackages?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 已启动的 ActionDock HTTP Runner 实例句柄。
|
|
62
|
+
*/
|
|
63
|
+
export interface ActionDockServerInstance {
|
|
64
|
+
/** 实际监听的端口号 */
|
|
65
|
+
port: number;
|
|
66
|
+
/** 关联的 ActionDockHost 宿主实例(若启动时传入或创建) */
|
|
67
|
+
host?: ActionDockHost;
|
|
68
|
+
/** 关联的 ActionDockTarget 目标门面实例 */
|
|
69
|
+
target?: ActionDockTarget;
|
|
70
|
+
/** 服务端可访问的基础 URL(如 "http://127.0.0.1:5177") */
|
|
71
|
+
url: string;
|
|
72
|
+
/** 服务就绪 Promise(可等待端口解析与监听建立) */
|
|
73
|
+
ready?: Promise<void>;
|
|
74
|
+
/** 优雅关闭服务端并释放底层资源 */
|
|
75
|
+
stop: (options?: {
|
|
76
|
+
graceMs?: number;
|
|
77
|
+
}) => Promise<void> | void;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|