@actiondock/core 2.0.12 → 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 +1 -1
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +2 -2
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +5 -5
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +2 -2
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -2
- package/dist/doctor/index.js +2 -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 -2
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +46 -41
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +59 -2
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +45 -7
- package/dist/export/templates.js +621 -0
- package/dist/filter/index.d.ts +1 -1
- package/dist/filter/index.js +1 -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 -16
- package/dist/index.js +20 -8458
- 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 +58 -12
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -3
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +2 -2
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +1 -1
- package/dist/profile/types.js +1 -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 -5
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +3 -4
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +21 -27
- 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 +6 -28
- 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 +58 -82
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -2
- 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 +32 -11
- 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 +1 -1
- 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 +0 -2
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +16 -5
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +3 -1
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +46 -14
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +1 -1
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -9
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +0 -8
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +2 -4
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +117 -10
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +68 -19
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.js +116 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +2 -2
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +28 -17
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +2 -1
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +2 -2
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +2 -2
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -9
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +4 -2
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +2 -2
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +2 -2
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +2 -1
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +11 -4
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +6 -6
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +27 -8
- 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 +4 -10
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +30 -10
- 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 +12 -5
- 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 +19 -6
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +57 -6
- 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 +22 -2
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +4 -0
- package/package.json +6 -7
- package/dist/build/index.d.ts +0 -1
- package/dist/build/templates.d.ts +0 -19
- package/dist/server/runtime-registry.d.ts +0 -78
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
// 初始化全局 Ajv 实例,预载所有标准 format 格式校验器
|
|
4
|
+
const ajv = new Ajv({
|
|
5
|
+
allErrors: true,
|
|
6
|
+
strict: false,
|
|
7
|
+
coerceTypes: false,
|
|
8
|
+
});
|
|
9
|
+
addFormats(ajv);
|
|
10
|
+
// 缓存已编译的 ValidateFunction,提升性能并支持同对象引用复用
|
|
11
|
+
const validatorCache = new WeakMap();
|
|
12
|
+
/**
|
|
13
|
+
* 递归检查数据中是否包含危险的原型污染属性键名(__proto__、constructor、prototype)。
|
|
14
|
+
*/
|
|
15
|
+
export function hasDangerousKeys(data) {
|
|
16
|
+
if (data === null || typeof data !== "object") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(data)) {
|
|
20
|
+
for (const item of data) {
|
|
21
|
+
if (hasDangerousKeys(item))
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const obj = data;
|
|
27
|
+
const keys = Object.getOwnPropertyNames(obj);
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (hasDangerousKeys(obj[key])) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 校验指定数据是否符合给定的 JSON Schema 契约。
|
|
40
|
+
*
|
|
41
|
+
* 行为与策略约定:
|
|
42
|
+
* 1. 严格契约校验:当 Schema 中定义 additionalProperties: false 时,包含未声明属性的输入将被直接拒绝(返回 valid: false),系统不会静默过滤或篡改用户输入。
|
|
43
|
+
* 2. 危险原型键硬拦截:输入数据或其任意嵌套对象中若出现 __proto__、constructor 或 prototype 键名,即使 Schema 允许亦立即拒绝,彻底杜绝原型污染。
|
|
44
|
+
* 3. 布尔 Schema:false 显式拒绝所有输入;true 或未定义时允许所有合法非污染数据。
|
|
45
|
+
* 4. 无原型对象支持:完全支持 Object.create(null) 创建的无原型对象校验。
|
|
46
|
+
*
|
|
47
|
+
* @param schema 期望匹配的 JSON Schema 对象(未提供或为空对象/true 时默认通过;为 false 时拒绝所有数据)
|
|
48
|
+
* @param data 待校验的原始数据
|
|
49
|
+
* @returns 包含 valid 状态与错误信息列表的 ValidationResult
|
|
50
|
+
*/
|
|
51
|
+
export function validateSchema(schema, data) {
|
|
52
|
+
// 1. 布尔 Schema 支持:false 拒绝一切输入;true/undefined 允许一切输入
|
|
53
|
+
if (schema === false) {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
errors: ["Schema is false, rejecting all data"],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (schema === true || !schema) {
|
|
60
|
+
if (hasDangerousKeys(data)) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
errors: ["Data contains prohibited prototype pollution keys (__proto__, constructor, or prototype)"],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { valid: true };
|
|
67
|
+
}
|
|
68
|
+
if (typeof schema !== "object" || Object.keys(schema).length === 0) {
|
|
69
|
+
if (hasDangerousKeys(data)) {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
errors: ["Data contains prohibited prototype pollution keys (__proto__, constructor, or prototype)"],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { valid: true };
|
|
76
|
+
}
|
|
77
|
+
// 2. 危险原型键拦截
|
|
78
|
+
if (hasDangerousKeys(data)) {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
errors: ["Data contains prohibited prototype pollution keys (__proto__, constructor, or prototype)"],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
let validate = validatorCache.get(schema);
|
|
86
|
+
if (!validate) {
|
|
87
|
+
// 避免因相同 $id 误用全局旧验证器,剥离 $id 进行独立编译
|
|
88
|
+
if ("$id" in schema) {
|
|
89
|
+
const { $id, ...cleanSchema } = schema;
|
|
90
|
+
validate = ajv.compile(cleanSchema);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
validate = ajv.compile(schema);
|
|
94
|
+
}
|
|
95
|
+
validatorCache.set(schema, validate);
|
|
96
|
+
}
|
|
97
|
+
const valid = validate(data);
|
|
98
|
+
if (valid) {
|
|
99
|
+
return { valid: true };
|
|
100
|
+
}
|
|
101
|
+
const errors = (validate.errors || []).map((err) => {
|
|
102
|
+
const path = err.instancePath ? `at '${err.instancePath}' ` : "";
|
|
103
|
+
return `${path}${err.message || "failed validation"}`.trim();
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
valid: false,
|
|
107
|
+
errors: errors.length > 0 ? errors : ["Schema validation failed"],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
errors: [`Schema compilation error: ${err.message || String(err)}`],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认单次 HTTP 请求体最大字节上限(1 MiB)。
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
|
|
5
|
+
/**
|
|
6
|
+
* 请求体超出最大允许体积时抛出的异常(对应 HTTP 413 Payload Too Large)。
|
|
7
|
+
*/
|
|
8
|
+
export class RequestTooLargeError extends Error {
|
|
9
|
+
code = "REQUEST_TOO_LARGE";
|
|
10
|
+
constructor(message = "Request body exceeds maximum allowed size") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "RequestTooLargeError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 请求体非合法 JSON 格式时抛出的异常(对应 HTTP 400 Bad Request)。
|
|
17
|
+
*/
|
|
18
|
+
export class InvalidJsonError extends Error {
|
|
19
|
+
code = "INVALID_JSON";
|
|
20
|
+
constructor(message = "Failed to parse request body as JSON") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "InvalidJsonError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 流式安全地读取 HTTP 请求体并解析为 JSON 对象。
|
|
27
|
+
*
|
|
28
|
+
* 防护机制:
|
|
29
|
+
* 1. 快速拒绝(Fast-path):若请求头中存在 `Content-Length` 且超过 `maxBytes`,立即抛出 RequestTooLargeError,不进行内存分配。
|
|
30
|
+
* 2. 流式计数器(Chunk Streaming):在读取 ReadableStream 过程中实时累计字节数,中途超出上限即刻截断并释放流锁,防止大文件 DoS 与内存耗尽(OOM)。
|
|
31
|
+
* 3. 安全解码:处理空请求体与 UTF-8 字符集解码。
|
|
32
|
+
*
|
|
33
|
+
* @param req 传入的 Request 对象
|
|
34
|
+
* @param options 读取配置选项
|
|
35
|
+
* @returns 解析后的 JSON 对象
|
|
36
|
+
* @throws {RequestTooLargeError} 若请求体体积超限
|
|
37
|
+
* @throws {InvalidJsonError} 若请求体非合法 JSON
|
|
38
|
+
*/
|
|
39
|
+
export async function readJsonBody(req, options = {}) {
|
|
40
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
41
|
+
// 1. 快速检查 Content-Length 头部
|
|
42
|
+
const contentLengthHeader = req.headers.get("content-length");
|
|
43
|
+
if (contentLengthHeader) {
|
|
44
|
+
const parsedLength = parseInt(contentLengthHeader, 10);
|
|
45
|
+
if (!isNaN(parsedLength) && parsedLength > maxBytes) {
|
|
46
|
+
throw new RequestTooLargeError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 2. 流式读取并实时统计字节数
|
|
50
|
+
if (!req.body) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
const reader = req.body.getReader();
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let totalBytes = 0;
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
if (value) {
|
|
62
|
+
totalBytes += value.byteLength;
|
|
63
|
+
if (totalBytes > maxBytes) {
|
|
64
|
+
throw new RequestTooLargeError();
|
|
65
|
+
}
|
|
66
|
+
chunks.push(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
reader.releaseLock();
|
|
72
|
+
}
|
|
73
|
+
if (chunks.length === 0 || totalBytes === 0) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
// 3. 拼接字节数组并进行 UTF-8 解码
|
|
77
|
+
const merged = new Uint8Array(totalBytes);
|
|
78
|
+
let offset = 0;
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
merged.set(chunk, offset);
|
|
81
|
+
offset += chunk.byteLength;
|
|
82
|
+
}
|
|
83
|
+
const text = new TextDecoder("utf-8").decode(merged);
|
|
84
|
+
if (!text.trim()) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(text);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new InvalidJsonError(`Failed to parse request body: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export * from "./types";
|
|
2
|
-
export * from "./security";
|
|
3
|
-
export * from "./body";
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./server";
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./security.js";
|
|
3
|
+
export * from "./body.js";
|
|
4
|
+
export * from "./routes/index.js";
|
|
5
|
+
export * from "./server.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type RouteContext } from "./common";
|
|
1
|
+
import { type RouteContext } from "./common.js";
|
|
2
2
|
/**
|
|
3
|
-
* 处理 Action
|
|
3
|
+
* 处理 Action 相关的 HTTP 路由(列表、规范查询、同步执行与异步启动)。
|
|
4
4
|
*/
|
|
5
5
|
export declare function handleActionsRoutes(ctx: RouteContext): Promise<Response | null>;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ActionResolver } from "../../catalog/action-resolver.js";
|
|
3
|
+
import { filterByIntent } from "../../filter/index.js";
|
|
4
|
+
import { ACTION_NOT_FOUND, ACTION_TIMEOUT, IDEMPOTENCY_CONFLICT, INPUT_VALIDATION_FAILED, PACKAGE_NOT_FOUND } from "../../errors.js";
|
|
5
|
+
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body.js";
|
|
6
|
+
import { getSubPath, jsonResponse } from "./common.js";
|
|
7
|
+
/**
|
|
8
|
+
* 处理 Action 相关的 HTTP 路由(列表、规范查询、同步执行与异步启动)。
|
|
9
|
+
*/
|
|
10
|
+
export async function handleActionsRoutes(ctx) {
|
|
11
|
+
const { req, url, pathname, corsHeaders, options, target } = ctx;
|
|
12
|
+
const subpath = getSubPath(pathname);
|
|
13
|
+
// 1. Actions List: GET /api/v2/actions, GET /actions
|
|
14
|
+
if (subpath === "/actions" && req.method === "GET") {
|
|
15
|
+
try {
|
|
16
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
17
|
+
const query = url.searchParams.get("query") || intent;
|
|
18
|
+
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
19
|
+
const prefix = url.searchParams.get("prefix") || undefined;
|
|
20
|
+
const tags = url.searchParams.getAll("tag");
|
|
21
|
+
let actions = await target.listActions({
|
|
22
|
+
query,
|
|
23
|
+
prefix,
|
|
24
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
25
|
+
});
|
|
26
|
+
if (targetPkg) {
|
|
27
|
+
actions = actions.filter((a) => a.packageId === targetPkg || a.id.startsWith(`${targetPkg}/`));
|
|
28
|
+
}
|
|
29
|
+
if (options.packageAllowlist && options.packageAllowlist.length > 0) {
|
|
30
|
+
actions = actions.filter((a) => a.packageId && options.packageAllowlist.includes(a.packageId));
|
|
31
|
+
}
|
|
32
|
+
if (intent) {
|
|
33
|
+
actions = filterByIntent(actions, intent, [(a) => a.id, (a) => a.description || "", (a) => a.packageId || ""], false);
|
|
34
|
+
}
|
|
35
|
+
return jsonResponse(actions, 200, corsHeaders);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return jsonResponse({
|
|
39
|
+
ok: false,
|
|
40
|
+
error: { code: "ACTIONS_LIST_ERROR", message: err.message },
|
|
41
|
+
}, 500, corsHeaders);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 2. Multi-package Action Show: GET /api/v2/packages/:packageId/actions/:actionId
|
|
45
|
+
const pkgActionShowMatch = subpath.match(/^\/packages\/([^/]+)\/actions\/([^/]+)$/);
|
|
46
|
+
if (pkgActionShowMatch && req.method === "GET") {
|
|
47
|
+
const packageId = decodeURIComponent(pkgActionShowMatch[1]);
|
|
48
|
+
const actionId = decodeURIComponent(pkgActionShowMatch[2]);
|
|
49
|
+
if (options.packageAllowlist && options.packageAllowlist.length > 0) {
|
|
50
|
+
if (!options.packageAllowlist.includes(packageId)) {
|
|
51
|
+
return jsonResponse({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: "PACKAGE_FORBIDDEN",
|
|
55
|
+
message: `Package '${packageId}' is not in the allowed package list`,
|
|
56
|
+
},
|
|
57
|
+
}, 403, corsHeaders);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const ref = `${packageId}/${actionId}`;
|
|
61
|
+
try {
|
|
62
|
+
const spec = await target.describeAction(ref);
|
|
63
|
+
return jsonResponse(spec, 200, corsHeaders);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return jsonResponse({
|
|
67
|
+
ok: false,
|
|
68
|
+
error: {
|
|
69
|
+
code: ACTION_NOT_FOUND,
|
|
70
|
+
message: err.message || `Action '${actionId}' not found in package '${packageId}'`,
|
|
71
|
+
},
|
|
72
|
+
}, 404, corsHeaders);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 3. Short Action Show: GET /api/v2/actions/:actionId, GET /actions/:actionId
|
|
76
|
+
const actionShowMatch = subpath.match(/^\/actions\/(.+)$/);
|
|
77
|
+
if (actionShowMatch && req.method === "GET") {
|
|
78
|
+
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
79
|
+
try {
|
|
80
|
+
const parsed = ActionResolver.parseRef(actionId);
|
|
81
|
+
if (parsed.packageId && options.packageAllowlist && options.packageAllowlist.length > 0) {
|
|
82
|
+
if (!options.packageAllowlist.includes(parsed.packageId)) {
|
|
83
|
+
return jsonResponse({
|
|
84
|
+
ok: false,
|
|
85
|
+
error: {
|
|
86
|
+
code: "PACKAGE_FORBIDDEN",
|
|
87
|
+
message: `Package '${parsed.packageId}' is not in the allowed package list`,
|
|
88
|
+
},
|
|
89
|
+
}, 403, corsHeaders);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
try {
|
|
95
|
+
const spec = await target.describeAction(actionId);
|
|
96
|
+
if (options.packageAllowlist &&
|
|
97
|
+
options.packageAllowlist.length > 0 &&
|
|
98
|
+
(!spec.packageId || !options.packageAllowlist.includes(spec.packageId))) {
|
|
99
|
+
return jsonResponse({
|
|
100
|
+
ok: false,
|
|
101
|
+
error: {
|
|
102
|
+
code: "PACKAGE_FORBIDDEN",
|
|
103
|
+
message: `Package '${spec.packageId}' is not in the allowed package list`,
|
|
104
|
+
},
|
|
105
|
+
}, 403, corsHeaders);
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse(spec, 200, corsHeaders);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return jsonResponse({
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
code: ACTION_NOT_FOUND,
|
|
114
|
+
message: err.message || `Action '${actionId}' not found`,
|
|
115
|
+
},
|
|
116
|
+
}, 404, corsHeaders);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 4. Execution Routes (Run & Start)
|
|
120
|
+
// Match patterns:
|
|
121
|
+
// - /packages/:packageId/actions/:actionId/run
|
|
122
|
+
// - /packages/:packageId/actions/:actionId/start
|
|
123
|
+
// - /actions/:actionId/run
|
|
124
|
+
// - /actions/:actionId/start
|
|
125
|
+
const pkgRunMatch = subpath.match(/^\/packages\/([^/]+)\/actions\/([^/]+)\/(run|start)$/);
|
|
126
|
+
const shortRunMatch = subpath.match(/^\/actions\/(.+)\/(run|start)$/);
|
|
127
|
+
if ((pkgRunMatch || shortRunMatch) && req.method === "POST") {
|
|
128
|
+
let actionRef;
|
|
129
|
+
let endpointMode;
|
|
130
|
+
let pkgId;
|
|
131
|
+
if (pkgRunMatch) {
|
|
132
|
+
pkgId = decodeURIComponent(pkgRunMatch[1]);
|
|
133
|
+
const actId = decodeURIComponent(pkgRunMatch[2]);
|
|
134
|
+
endpointMode = pkgRunMatch[3];
|
|
135
|
+
actionRef = `${pkgId}/${actId}`;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
actionRef = decodeURIComponent(shortRunMatch[1]);
|
|
139
|
+
endpointMode = shortRunMatch[2];
|
|
140
|
+
try {
|
|
141
|
+
const parsed = ActionResolver.parseRef(actionRef);
|
|
142
|
+
pkgId = parsed.packageId;
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
}
|
|
146
|
+
// 校验 Package 允许白名单
|
|
147
|
+
if (options.packageAllowlist && options.packageAllowlist.length > 0) {
|
|
148
|
+
if (!pkgId) {
|
|
149
|
+
try {
|
|
150
|
+
const spec = await target.describeAction(actionRef);
|
|
151
|
+
pkgId = spec?.packageId;
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
if (!pkgId || !options.packageAllowlist.includes(pkgId)) {
|
|
156
|
+
return jsonResponse({
|
|
157
|
+
ok: false,
|
|
158
|
+
runId: randomUUID(),
|
|
159
|
+
error: {
|
|
160
|
+
code: "PACKAGE_FORBIDDEN",
|
|
161
|
+
message: pkgId
|
|
162
|
+
? `Package '${pkgId}' is not in the allowed package list`
|
|
163
|
+
: `Action '${actionRef}' does not belong to any allowed package`,
|
|
164
|
+
},
|
|
165
|
+
}, 403, corsHeaders);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let body = {};
|
|
169
|
+
try {
|
|
170
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err instanceof RequestTooLargeError) {
|
|
174
|
+
return jsonResponse({
|
|
175
|
+
ok: false,
|
|
176
|
+
runId: randomUUID(),
|
|
177
|
+
error: { code: "REQUEST_TOO_LARGE", message: err.message },
|
|
178
|
+
}, 413, corsHeaders);
|
|
179
|
+
}
|
|
180
|
+
if (err instanceof InvalidJsonError) {
|
|
181
|
+
return jsonResponse({
|
|
182
|
+
ok: false,
|
|
183
|
+
runId: randomUUID(),
|
|
184
|
+
error: { code: "INVALID_JSON", message: err.message },
|
|
185
|
+
}, 400, corsHeaders);
|
|
186
|
+
}
|
|
187
|
+
return jsonResponse({
|
|
188
|
+
ok: false,
|
|
189
|
+
runId: randomUUID(),
|
|
190
|
+
error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
|
|
191
|
+
}, 400, corsHeaders);
|
|
192
|
+
}
|
|
193
|
+
const isAsync = endpointMode === "start" ||
|
|
194
|
+
body?.execution?.mode === "async" ||
|
|
195
|
+
body?.async === true;
|
|
196
|
+
const timeoutMs = typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
|
|
197
|
+
? body.execution.timeoutMs
|
|
198
|
+
: undefined;
|
|
199
|
+
const input = body && "input" in body ? body.input : {};
|
|
200
|
+
const configOverrides = body?.config;
|
|
201
|
+
const requestId = req.headers.get("Idempotency-Key") ||
|
|
202
|
+
req.headers.get("idempotency-key") ||
|
|
203
|
+
req.headers.get("X-Request-Id") ||
|
|
204
|
+
req.headers.get("x-request-id") ||
|
|
205
|
+
body?.requestId ||
|
|
206
|
+
body?.execution?.requestId ||
|
|
207
|
+
undefined;
|
|
208
|
+
if (isAsync) {
|
|
209
|
+
try {
|
|
210
|
+
const ticket = await target.startAction(actionRef, input, {
|
|
211
|
+
timeoutMs,
|
|
212
|
+
config: configOverrides,
|
|
213
|
+
requestId,
|
|
214
|
+
});
|
|
215
|
+
if (ticket.status === "failed") {
|
|
216
|
+
const res = await ticket.result;
|
|
217
|
+
return jsonResponse(res, 400, corsHeaders);
|
|
218
|
+
}
|
|
219
|
+
return jsonResponse({
|
|
220
|
+
ok: true,
|
|
221
|
+
runId: ticket.runId,
|
|
222
|
+
status: ticket.status || "running",
|
|
223
|
+
streamUrl: `/api/v2/runs/${ticket.runId}/events`,
|
|
224
|
+
}, 202, corsHeaders);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (err?.code === IDEMPOTENCY_CONFLICT) {
|
|
228
|
+
return jsonResponse({
|
|
229
|
+
ok: false,
|
|
230
|
+
error: {
|
|
231
|
+
code: IDEMPOTENCY_CONFLICT,
|
|
232
|
+
message: err.message,
|
|
233
|
+
details: err.details,
|
|
234
|
+
},
|
|
235
|
+
}, 409, corsHeaders);
|
|
236
|
+
}
|
|
237
|
+
return jsonResponse({
|
|
238
|
+
ok: false,
|
|
239
|
+
runId: randomUUID(),
|
|
240
|
+
error: {
|
|
241
|
+
code: "ACTION_START_FAILED",
|
|
242
|
+
message: err.message || String(err),
|
|
243
|
+
},
|
|
244
|
+
}, 500, corsHeaders);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 同步执行模式
|
|
248
|
+
try {
|
|
249
|
+
const result = await target.runAction(actionRef, input, {
|
|
250
|
+
signal: req.signal,
|
|
251
|
+
timeoutMs,
|
|
252
|
+
config: configOverrides,
|
|
253
|
+
requestId,
|
|
254
|
+
});
|
|
255
|
+
let status = 200;
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
if (result.error?.code === INPUT_VALIDATION_FAILED) {
|
|
258
|
+
status = 400;
|
|
259
|
+
}
|
|
260
|
+
else if (result.error?.code === IDEMPOTENCY_CONFLICT) {
|
|
261
|
+
status = 409;
|
|
262
|
+
}
|
|
263
|
+
else if (result.error?.code === ACTION_NOT_FOUND ||
|
|
264
|
+
result.error?.code === PACKAGE_NOT_FOUND) {
|
|
265
|
+
status = 404;
|
|
266
|
+
}
|
|
267
|
+
else if (result.error?.code === ACTION_TIMEOUT) {
|
|
268
|
+
status = 504;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
status = 500;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return jsonResponse(result, status, corsHeaders);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
if (err?.code === IDEMPOTENCY_CONFLICT) {
|
|
278
|
+
return jsonResponse({
|
|
279
|
+
ok: false,
|
|
280
|
+
error: {
|
|
281
|
+
code: IDEMPOTENCY_CONFLICT,
|
|
282
|
+
message: err.message,
|
|
283
|
+
details: err.details,
|
|
284
|
+
},
|
|
285
|
+
}, 409, corsHeaders);
|
|
286
|
+
}
|
|
287
|
+
return jsonResponse({
|
|
288
|
+
ok: false,
|
|
289
|
+
runId: randomUUID(),
|
|
290
|
+
error: {
|
|
291
|
+
code: "ACTION_EXECUTION_ERROR",
|
|
292
|
+
message: err.message || String(err),
|
|
293
|
+
},
|
|
294
|
+
}, 500, corsHeaders);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type { ServerOptions } from "../types";
|
|
1
|
+
import type { ActionDockApp } from "../../app/types.js";
|
|
2
|
+
import type { ActionDockHost } from "../../host/types.js";
|
|
3
|
+
import type { ActionDockTarget } from "../../target/types.js";
|
|
4
|
+
import type { ServerOptions } from "../types.js";
|
|
5
5
|
/**
|
|
6
6
|
* 路由处理统一上下文对象。
|
|
7
7
|
*/
|
|
@@ -12,25 +12,36 @@ export interface RouteContext {
|
|
|
12
12
|
corsHeaders: Record<string, string>;
|
|
13
13
|
projectRoot: string | null;
|
|
14
14
|
customHome?: string;
|
|
15
|
-
|
|
15
|
+
host?: ActionDockHost;
|
|
16
|
+
target: ActionDockTarget;
|
|
16
17
|
options: ServerOptions;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
+
* 构造带 CORS 头的标准 JSON HTTP 响应。
|
|
20
21
|
*/
|
|
21
22
|
export declare function jsonResponse(data: unknown, status?: number, corsHeaders?: Record<string, string>): Response;
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
+
* 剥离标准版本前缀 (/api/v2/ 与兼容的 /api/v1/),获取相对路由子路径。
|
|
24
25
|
*/
|
|
25
|
-
export declare function
|
|
26
|
-
packageId: string;
|
|
27
|
-
storage: RuntimeStorage;
|
|
28
|
-
projectRoot?: string;
|
|
29
|
-
};
|
|
26
|
+
export declare function getSubPath(pathname: string): string;
|
|
30
27
|
/**
|
|
31
|
-
*
|
|
28
|
+
* 包未列入白名单拒绝访问异常。
|
|
32
29
|
*/
|
|
33
|
-
export declare
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
export declare class PackageNotAllowedError extends Error {
|
|
31
|
+
status: number;
|
|
32
|
+
statusCode: number;
|
|
33
|
+
code: string;
|
|
34
|
+
constructor(packageId: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 校验目标 packageId 是否属于允许包白名单。
|
|
38
|
+
* 当 options.packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertPackageAllowed(packageId: string | undefined | null, options?: {
|
|
41
|
+
packageAllowlist?: string[];
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* 从 Host 或 Target 中依据 packageId 解析对应的 ActionDockApp 实例。
|
|
45
|
+
* 若提供了 options,过滤或优先选择白名单内的 app。
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveAppForPackage(packageIdOrPath: string | undefined, host?: ActionDockHost, target?: ActionDockTarget, options?: ServerOptions): ActionDockApp;
|