@actiondock/core 2.8.0 → 2.9.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 +20 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/project/init.js +2 -2
- package/dist/project/types.d.ts +5 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/policy.d.ts +138 -0
- package/dist/server/policy.js +368 -0
- package/dist/server/routes/actions.js +86 -42
- package/dist/server/routes/common.d.ts +14 -25
- package/dist/server/routes/common.js +25 -21
- package/dist/server/routes/config.d.ts +1 -1
- package/dist/server/routes/config.js +8 -8
- package/dist/server/routes/doctor.js +4 -4
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/info.js +6 -12
- package/dist/server/routes/playbooks.js +8 -8
- package/dist/server/routes/runs.js +61 -29
- package/dist/server/routes/state.d.ts +1 -1
- package/dist/server/routes/state.js +7 -7
- package/dist/server/security.d.ts +11 -0
- package/dist/server/security.js +36 -19
- package/dist/server/server.js +200 -30
- package/dist/server/types.d.ts +60 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { parseActionRef } from "../../catalog/resolve-action.js";
|
|
2
2
|
import { filterByIntent } from "../../filter/index.js";
|
|
3
|
-
import { ACTION_EXECUTION_ERROR, ACTION_NOT_FOUND, ACTION_START_FAILED, ACTION_TIMEOUT, ACTIONS_LIST_ERROR, IDEMPOTENCY_CONFLICT, INPUT_VALIDATION_FAILED, INVALID_JSON, PACKAGE_FORBIDDEN, PACKAGE_NOT_FOUND, REQUEST_TOO_LARGE, } from "../../errors.js";
|
|
3
|
+
import { ACTION_EXECUTION_ERROR, ACTION_FORBIDDEN, ACTION_NOT_FOUND, ACTION_START_FAILED, ACTION_TIMEOUT, ACTIONS_LIST_ERROR, IDEMPOTENCY_CONFLICT, INPUT_VALIDATION_FAILED, INVALID_JSON, PACKAGE_FORBIDDEN, PACKAGE_NOT_FOUND, REQUEST_TOO_LARGE, } from "../../errors.js";
|
|
4
4
|
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body.js";
|
|
5
|
-
import { getSubPath, jsonResponse } from "./common.js";
|
|
5
|
+
import { filterActionsByPolicy, getSubPath, isActionAllowedByPolicy, isPackageAllowedByPolicy, jsonResponse, } from "./common.js";
|
|
6
6
|
/**
|
|
7
7
|
* 处理 Action 相关的 HTTP 路由(列表、规范查询、同步执行与异步启动)。
|
|
8
8
|
*/
|
|
9
9
|
export async function handleActionsRoutes(ctx) {
|
|
10
|
-
const { req, url, pathname, corsHeaders, options, service } = ctx;
|
|
10
|
+
const { req, url, pathname, corsHeaders, options, service, activePolicy: policy } = ctx;
|
|
11
11
|
const subpath = getSubPath(pathname);
|
|
12
12
|
// 1. Actions List: GET /api/v2/actions, GET /actions
|
|
13
13
|
if (subpath === "/actions" && req.method === "GET") {
|
|
@@ -25,9 +25,7 @@ export async function handleActionsRoutes(ctx) {
|
|
|
25
25
|
if (targetPkg) {
|
|
26
26
|
actions = actions.filter((a) => a.packageId === targetPkg || a.id.startsWith(`${targetPkg}/`));
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
actions = actions.filter((a) => a.packageId && options.packageAllowlist.includes(a.packageId));
|
|
30
|
-
}
|
|
28
|
+
actions = filterActionsByPolicy(actions, policy);
|
|
31
29
|
if (intent) {
|
|
32
30
|
actions = filterByIntent(actions, intent, [(a) => a.id, (a) => a.description || "", (a) => a.packageId || ""], false);
|
|
33
31
|
}
|
|
@@ -45,16 +43,23 @@ export async function handleActionsRoutes(ctx) {
|
|
|
45
43
|
if (pkgActionShowMatch && req.method === "GET") {
|
|
46
44
|
const packageId = decodeURIComponent(pkgActionShowMatch[1]);
|
|
47
45
|
const actionId = decodeURIComponent(pkgActionShowMatch[2]);
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
if (!isPackageAllowedByPolicy(packageId, policy)) {
|
|
47
|
+
return jsonResponse({
|
|
48
|
+
ok: false,
|
|
49
|
+
error: {
|
|
50
|
+
code: PACKAGE_FORBIDDEN,
|
|
51
|
+
message: `Package '${packageId}' is not in the allowed package list`,
|
|
52
|
+
},
|
|
53
|
+
}, 403, corsHeaders);
|
|
54
|
+
}
|
|
55
|
+
if (!isActionAllowedByPolicy({ packageId, actionId }, policy)) {
|
|
56
|
+
return jsonResponse({
|
|
57
|
+
ok: false,
|
|
58
|
+
error: {
|
|
59
|
+
code: ACTION_FORBIDDEN,
|
|
60
|
+
message: `Action '${packageId}/${actionId}' is not in the allowed action list`,
|
|
61
|
+
},
|
|
62
|
+
}, 403, corsHeaders);
|
|
58
63
|
}
|
|
59
64
|
const ref = `${packageId}/${actionId}`;
|
|
60
65
|
try {
|
|
@@ -77,8 +82,8 @@ export async function handleActionsRoutes(ctx) {
|
|
|
77
82
|
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
78
83
|
try {
|
|
79
84
|
const parsed = parseActionRef(actionId);
|
|
80
|
-
if (parsed.packageId
|
|
81
|
-
if (!
|
|
85
|
+
if (parsed.packageId) {
|
|
86
|
+
if (!isPackageAllowedByPolicy(parsed.packageId, policy)) {
|
|
82
87
|
return jsonResponse({
|
|
83
88
|
ok: false,
|
|
84
89
|
error: {
|
|
@@ -87,19 +92,35 @@ export async function handleActionsRoutes(ctx) {
|
|
|
87
92
|
},
|
|
88
93
|
}, 403, corsHeaders);
|
|
89
94
|
}
|
|
95
|
+
if (!isActionAllowedByPolicy({ packageId: parsed.packageId, actionId: parsed.actionId }, policy)) {
|
|
96
|
+
return jsonResponse({
|
|
97
|
+
ok: false,
|
|
98
|
+
error: {
|
|
99
|
+
code: ACTION_FORBIDDEN,
|
|
100
|
+
message: `Action '${actionId}' is not in the allowed action list`,
|
|
101
|
+
},
|
|
102
|
+
}, 403, corsHeaders);
|
|
103
|
+
}
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
catch { }
|
|
93
107
|
try {
|
|
94
108
|
const spec = await service.discovery.describeAction(actionId);
|
|
95
|
-
if (
|
|
96
|
-
options.packageAllowlist.length > 0 &&
|
|
97
|
-
(!spec.packageId || !options.packageAllowlist.includes(spec.packageId))) {
|
|
109
|
+
if (!isPackageAllowedByPolicy(spec.packageId, policy)) {
|
|
98
110
|
return jsonResponse({
|
|
99
111
|
ok: false,
|
|
100
112
|
error: {
|
|
101
113
|
code: PACKAGE_FORBIDDEN,
|
|
102
|
-
message: `Package '${spec.packageId}' is not in the allowed package list`,
|
|
114
|
+
message: `Package '${spec.packageId || "unknown"}' is not in the allowed package list`,
|
|
115
|
+
},
|
|
116
|
+
}, 403, corsHeaders);
|
|
117
|
+
}
|
|
118
|
+
if (!isActionAllowedByPolicy({ packageId: spec.packageId, actionId: spec.id }, policy)) {
|
|
119
|
+
return jsonResponse({
|
|
120
|
+
ok: false,
|
|
121
|
+
error: {
|
|
122
|
+
code: ACTION_FORBIDDEN,
|
|
123
|
+
message: `Action '${spec.packageId ? `${spec.packageId}/${spec.id}` : spec.id}' is not in the allowed action list`,
|
|
103
124
|
},
|
|
104
125
|
}, 403, corsHeaders);
|
|
105
126
|
}
|
|
@@ -127,9 +148,10 @@ export async function handleActionsRoutes(ctx) {
|
|
|
127
148
|
let actionRef;
|
|
128
149
|
let endpointMode;
|
|
129
150
|
let pkgId;
|
|
151
|
+
let actId;
|
|
130
152
|
if (pkgRunMatch) {
|
|
131
153
|
pkgId = decodeURIComponent(pkgRunMatch[1]);
|
|
132
|
-
|
|
154
|
+
actId = decodeURIComponent(pkgRunMatch[2]);
|
|
133
155
|
endpointMode = pkgRunMatch[3];
|
|
134
156
|
actionRef = `${pkgId}/${actId}`;
|
|
135
157
|
}
|
|
@@ -139,30 +161,52 @@ export async function handleActionsRoutes(ctx) {
|
|
|
139
161
|
try {
|
|
140
162
|
const parsed = parseActionRef(actionRef);
|
|
141
163
|
pkgId = parsed.packageId;
|
|
164
|
+
actId = parsed.actionId;
|
|
142
165
|
}
|
|
143
166
|
catch { }
|
|
144
167
|
}
|
|
145
|
-
//
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
catch { }
|
|
168
|
+
// 解析并补全包与动作标识(仅在必要时单次查询 spec)
|
|
169
|
+
if ((!pkgId || !actId) && (policy.packageAllowlist?.length || policy.actionAllowlist?.length)) {
|
|
170
|
+
try {
|
|
171
|
+
const spec = await service.discovery.describeAction(actionRef);
|
|
172
|
+
pkgId = pkgId || spec?.packageId;
|
|
173
|
+
actId = actId || spec?.id;
|
|
153
174
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
? `Package '${pkgId}' is not in the allowed package list`
|
|
162
|
-
: `Action '${actionRef}' does not belong to any allowed package`,
|
|
163
|
-
},
|
|
164
|
-
}, 403, corsHeaders);
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
if (!actId) {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = parseActionRef(actionRef);
|
|
180
|
+
actId = parsed.actionId;
|
|
181
|
+
pkgId = pkgId || parsed.packageId;
|
|
165
182
|
}
|
|
183
|
+
catch {
|
|
184
|
+
actId = actionRef;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 校验 Package 允许白名单
|
|
188
|
+
if (!isPackageAllowedByPolicy(pkgId, policy)) {
|
|
189
|
+
return jsonResponse({
|
|
190
|
+
ok: false,
|
|
191
|
+
runId: "",
|
|
192
|
+
error: {
|
|
193
|
+
code: PACKAGE_FORBIDDEN,
|
|
194
|
+
message: pkgId
|
|
195
|
+
? `Package '${pkgId}' is not in the allowed package list`
|
|
196
|
+
: `Action '${actionRef}' does not belong to any allowed package`,
|
|
197
|
+
},
|
|
198
|
+
}, 403, corsHeaders);
|
|
199
|
+
}
|
|
200
|
+
// 校验 Action 允许白名单
|
|
201
|
+
if (!isActionAllowedByPolicy({ packageId: pkgId, actionId: actId }, policy)) {
|
|
202
|
+
return jsonResponse({
|
|
203
|
+
ok: false,
|
|
204
|
+
runId: "",
|
|
205
|
+
error: {
|
|
206
|
+
code: ACTION_FORBIDDEN,
|
|
207
|
+
message: `Action '${actionRef}' is not in the allowed action list`,
|
|
208
|
+
},
|
|
209
|
+
}, 403, corsHeaders);
|
|
166
210
|
}
|
|
167
211
|
let body = {};
|
|
168
212
|
try {
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import type { PackageInfo } from "../../package/types.js";
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
export interface RouteContext {
|
|
8
|
-
req: Request;
|
|
9
|
-
url: URL;
|
|
10
|
-
pathname: string;
|
|
11
|
-
corsHeaders: Record<string, string>;
|
|
12
|
-
projectRoot: string | null;
|
|
13
|
-
customHome?: string;
|
|
14
|
-
service: ActionDockService;
|
|
15
|
-
options: ServerOptions;
|
|
16
|
-
}
|
|
2
|
+
import type { EffectiveServerPolicy, RouteContext, ServerOptions } from "../types.js";
|
|
3
|
+
import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isManagementAllowedByPolicy, isPackageAllowedByPolicy, PackageNotAllowedError } from "../policy.js";
|
|
4
|
+
export type { RouteContext };
|
|
5
|
+
export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isPackageAllowedByPolicy, isManagementAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
|
|
17
6
|
/**
|
|
18
7
|
* 构造带 CORS 头的标准 JSON HTTP 响应。
|
|
19
8
|
*/
|
|
@@ -22,22 +11,22 @@ export declare function jsonResponse(data: unknown, status?: number, corsHeaders
|
|
|
22
11
|
* 剥离标准版本前缀 (/api/v2/),获取相对路由子路径。
|
|
23
12
|
*/
|
|
24
13
|
export declare function getSubPath(pathname: string): string;
|
|
25
|
-
import { ActionDockError } from "../../errors.js";
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
15
|
+
* 校验目标 action 是否属于允许动作白名单。
|
|
16
|
+
* 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
|
|
28
17
|
*/
|
|
29
|
-
export declare
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
18
|
+
export declare function assertActionAllowed(action: {
|
|
19
|
+
actionId?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
packageId?: string;
|
|
22
|
+
} | string, options?: ServerOptions | EffectiveServerPolicy): void;
|
|
34
23
|
/**
|
|
35
24
|
* 校验目标 packageId 是否属于允许包白名单。
|
|
36
|
-
* 当 options
|
|
25
|
+
* 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
37
26
|
*/
|
|
38
|
-
export declare function assertPackageAllowed(packageId: string, options?: ServerOptions): void;
|
|
27
|
+
export declare function assertPackageAllowed(packageId: string, options?: ServerOptions | EffectiveServerPolicy): void;
|
|
39
28
|
/**
|
|
40
29
|
* 解析并确定生效的目标 packageId。
|
|
41
30
|
* 遵循严格的解析规则与白名单检查。
|
|
42
31
|
*/
|
|
43
|
-
export declare function resolveTargetPackageId(packages: PackageInfo[], requestedPackageId?: string, options?: ServerOptions): string;
|
|
32
|
+
export declare function resolveTargetPackageId(packages: PackageInfo[], requestedPackageId?: string, options?: ServerOptions | EffectiveServerPolicy): string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { assertValidPackageId } from "../../utils/index.js";
|
|
2
|
+
import { ActionDockError, PACKAGE_NOT_FOUND } from "../../errors.js";
|
|
3
|
+
import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isManagementAllowedByPolicy, isPackageAllowedByPolicy, PackageNotAllowedError, } from "../policy.js";
|
|
4
|
+
export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isPackageAllowedByPolicy, isManagementAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
|
|
2
5
|
/**
|
|
3
6
|
* 构造带 CORS 头的标准 JSON HTTP 响应。
|
|
4
7
|
*/
|
|
@@ -20,36 +23,37 @@ export function getSubPath(pathname) {
|
|
|
20
23
|
}
|
|
21
24
|
return pathname;
|
|
22
25
|
}
|
|
23
|
-
import { ActionDockError, PACKAGE_NOT_FOUND, PACKAGE_NOT_ALLOWED } from "../../errors.js";
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
27
|
+
* 校验目标 action 是否属于允许动作白名单。
|
|
28
|
+
* 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
|
|
26
29
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
export function assertActionAllowed(action, options) {
|
|
31
|
+
const policy = options
|
|
32
|
+
? {
|
|
33
|
+
packageAllowlist: options.packageAllowlist,
|
|
34
|
+
actionAllowlist: options.actionAllowlist,
|
|
35
|
+
}
|
|
36
|
+
: undefined;
|
|
37
|
+
assertActionAllowedByPolicy(action, policy);
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
40
|
* 校验目标 packageId 是否属于允许包白名单。
|
|
38
|
-
* 当 options
|
|
41
|
+
* 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
39
42
|
*/
|
|
40
43
|
export function assertPackageAllowed(packageId, options) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const policy = options
|
|
45
|
+
? {
|
|
46
|
+
packageAllowlist: options.packageAllowlist,
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
49
|
+
assertPackageAllowedByPolicy(packageId, policy);
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
49
52
|
* 解析并确定生效的目标 packageId。
|
|
50
53
|
* 遵循严格的解析规则与白名单检查。
|
|
51
54
|
*/
|
|
52
55
|
export function resolveTargetPackageId(packages, requestedPackageId, options) {
|
|
56
|
+
const allowlist = options?.packageAllowlist;
|
|
53
57
|
if (requestedPackageId) {
|
|
54
58
|
if (requestedPackageId === "global") {
|
|
55
59
|
return "global";
|
|
@@ -63,10 +67,10 @@ export function resolveTargetPackageId(packages, requestedPackageId, options) {
|
|
|
63
67
|
return matched.id;
|
|
64
68
|
}
|
|
65
69
|
let targetId;
|
|
66
|
-
if (
|
|
67
|
-
Array.isArray(
|
|
68
|
-
|
|
69
|
-
const allowed = packages.find((p) =>
|
|
70
|
+
if (allowlist &&
|
|
71
|
+
Array.isArray(allowlist) &&
|
|
72
|
+
allowlist.length > 0) {
|
|
73
|
+
const allowed = packages.find((p) => allowlist.includes(p.id));
|
|
70
74
|
targetId = allowed ? allowed.id : (packages[0]?.id || "");
|
|
71
75
|
}
|
|
72
76
|
else if (packages.length > 0) {
|
|
@@ -2,7 +2,7 @@ import { CAPABILITY_UNAVAILABLE, CONFIG_DELETE_ERROR, CONFIG_ENV_ERROR, CONFIG_L
|
|
|
2
2
|
import { resolveEnvValue } from "../../runtime/index.js";
|
|
3
3
|
import { isSecretConfigKey, sanitizeConfigDefinitions } from "../../storage/index.js";
|
|
4
4
|
import { readJsonBody } from "../body.js";
|
|
5
|
-
import { getSubPath, jsonResponse, resolveTargetPackageId } from "./common.js";
|
|
5
|
+
import { getSubPath, isManagementAllowedByPolicy, jsonResponse, resolveTargetPackageId, } from "./common.js";
|
|
6
6
|
function isClientConfigError(err) {
|
|
7
7
|
return (err?.code === PACKAGE_NOT_FOUND ||
|
|
8
8
|
err?.code === INVALID_PACKAGE_ID ||
|
|
@@ -13,10 +13,10 @@ function isClientConfigError(err) {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* 处理配置元数据与当前值读取、更新及删除接口。
|
|
16
|
-
*
|
|
16
|
+
* 在未显式开启管理功能时返回 403 拒绝。
|
|
17
17
|
*/
|
|
18
18
|
export async function handleConfigRoutes(ctx) {
|
|
19
|
-
const { req, url, pathname, corsHeaders, options, service } = ctx;
|
|
19
|
+
const { req, url, pathname, corsHeaders, options, service, activePolicy: policy } = ctx;
|
|
20
20
|
const subpath = getSubPath(pathname);
|
|
21
21
|
const isConfigRoute = subpath === "/config" ||
|
|
22
22
|
subpath.startsWith("/config/") ||
|
|
@@ -25,7 +25,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
// 校验管理能力开关
|
|
28
|
-
if (
|
|
28
|
+
if (!isManagementAllowedByPolicy(policy) || !service.management?.config) {
|
|
29
29
|
return jsonResponse({
|
|
30
30
|
ok: false,
|
|
31
31
|
error: {
|
|
@@ -39,7 +39,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
39
39
|
try {
|
|
40
40
|
const pkgs = await service.discovery.listPackages();
|
|
41
41
|
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
42
|
-
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam,
|
|
42
|
+
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
43
43
|
const pkg = pkgs.find((p) => p.id === targetPackageId);
|
|
44
44
|
const declared = pkg?.config || {};
|
|
45
45
|
const envChecks = [];
|
|
@@ -76,7 +76,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
76
76
|
try {
|
|
77
77
|
const pkgs = await service.discovery.listPackages();
|
|
78
78
|
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
79
|
-
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam,
|
|
79
|
+
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
80
80
|
const pkg = pkgs.find((p) => p.id === targetPackageId);
|
|
81
81
|
const rawDeclared = pkg?.config || {};
|
|
82
82
|
const declared = sanitizeConfigDefinitions(rawDeclared) || {};
|
|
@@ -112,7 +112,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
112
112
|
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
113
113
|
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || body.package || undefined;
|
|
114
114
|
const pkgs = await service.discovery.listPackages();
|
|
115
|
-
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam,
|
|
115
|
+
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
116
116
|
const key = body.key;
|
|
117
117
|
if (!key) {
|
|
118
118
|
return jsonResponse({ ok: false, error: { code: INVALID_ARGUMENT, message: "Config 'key' is required" } }, 400, corsHeaders);
|
|
@@ -141,7 +141,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
141
141
|
const key = decodeURIComponent(configKeyMatch[1]);
|
|
142
142
|
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
143
143
|
const pkgs = await service.discovery.listPackages();
|
|
144
|
-
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam,
|
|
144
|
+
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
145
145
|
const deleted = await service.management.config.delete(targetPackageId, key);
|
|
146
146
|
return jsonResponse({ ok: true, packageId: targetPackageId, key, deleted }, 200, corsHeaders);
|
|
147
147
|
}
|
|
@@ -5,14 +5,14 @@ import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
|
|
|
5
5
|
* 处理环境与依赖诊断接口(GET /api/v2/doctor 与 /doctor)。
|
|
6
6
|
*/
|
|
7
7
|
export async function handleDoctorRoute(ctx) {
|
|
8
|
-
const { req, url, pathname, corsHeaders, projectRoot, customHome, service, options } = ctx;
|
|
8
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome, service, options, activePolicy: policy } = ctx;
|
|
9
9
|
const subpath = getSubPath(pathname);
|
|
10
10
|
if (subpath !== "/doctor" || req.method !== "GET") {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
14
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
15
|
-
if (
|
|
15
|
+
if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
|
|
16
16
|
if (!targetPkg) {
|
|
17
17
|
return jsonResponse({
|
|
18
18
|
ok: false,
|
|
@@ -24,7 +24,7 @@ export async function handleDoctorRoute(ctx) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
if (targetPkg) {
|
|
27
|
-
assertPackageAllowed(targetPkg,
|
|
27
|
+
assertPackageAllowed(targetPkg, policy);
|
|
28
28
|
}
|
|
29
29
|
let packageRoot;
|
|
30
30
|
if (targetPkg) {
|
|
@@ -36,7 +36,7 @@ export async function handleDoctorRoute(ctx) {
|
|
|
36
36
|
cwd: packageRoot || projectRoot || process.cwd(),
|
|
37
37
|
packageIdOrPath: packageRoot || targetPkg,
|
|
38
38
|
customHome,
|
|
39
|
-
packageAllowlist:
|
|
39
|
+
packageAllowlist: policy.packageAllowlist,
|
|
40
40
|
});
|
|
41
41
|
return jsonResponse({ ok: true, report }, 200, corsHeaders);
|
|
42
42
|
}
|
|
@@ -6,12 +6,12 @@ import { jsonResponse } from "./common.js";
|
|
|
6
6
|
* 处理健康检查与就绪状态接口(支持 /api/v2/health 与 /health)。
|
|
7
7
|
*/
|
|
8
8
|
export async function handleHealthRoute(ctx) {
|
|
9
|
-
const { req, pathname, corsHeaders, options, projectRoot } = ctx;
|
|
9
|
+
const { req, pathname, corsHeaders, options, projectRoot, activePolicy: policy } = ctx;
|
|
10
10
|
if (pathname !== "/api/v2/health" &&
|
|
11
11
|
pathname !== "/health") {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
if (!verifyBearerToken(req,
|
|
14
|
+
if (!verifyBearerToken(req, policy.token, options)) {
|
|
15
15
|
return jsonResponse({
|
|
16
16
|
ok: false,
|
|
17
17
|
error: {
|
|
@@ -2,7 +2,7 @@ import { filterWithFallbackInfo } from "../../filter/index.js";
|
|
|
2
2
|
import { INFO_ERROR, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PACKAGES_INFO_ERROR } from "../../errors.js";
|
|
3
3
|
import { ACTIONDOCK_VERSION } from "../../version.js";
|
|
4
4
|
import { sanitizeConfigDefinitions } from "../../storage/index.js";
|
|
5
|
-
import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
|
|
5
|
+
import { assertPackageAllowed, filterPackagesByPolicy, getSubPath, jsonResponse } from "./common.js";
|
|
6
6
|
function sanitizePackage(pkg, exposeDebugInfo) {
|
|
7
7
|
if (!pkg)
|
|
8
8
|
return pkg;
|
|
@@ -22,20 +22,17 @@ function sanitizePackage(pkg, exposeDebugInfo) {
|
|
|
22
22
|
* - GET /api/v2/packages 与 GET /packages -> 委托 service.discovery.listPackages()
|
|
23
23
|
*/
|
|
24
24
|
export async function handleInfoRoute(ctx) {
|
|
25
|
-
const { req, url, pathname, corsHeaders, options, service, projectRoot } = ctx;
|
|
25
|
+
const { req, url, pathname, corsHeaders, options, service, projectRoot, activePolicy: policy } = ctx;
|
|
26
26
|
const subpath = getSubPath(pathname);
|
|
27
27
|
// 1. Packages List: GET /api/v2/packages, /packages
|
|
28
28
|
if ((subpath === "/packages" || pathname === "/api/v2/packages" || pathname === "/packages") && req.method === "GET") {
|
|
29
29
|
try {
|
|
30
30
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
31
31
|
if (targetPkg) {
|
|
32
|
-
assertPackageAllowed(targetPkg,
|
|
32
|
+
assertPackageAllowed(targetPkg, policy);
|
|
33
33
|
}
|
|
34
34
|
const rawPackages = await service.discovery.listPackages();
|
|
35
|
-
|
|
36
|
-
if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
|
|
37
|
-
packages = packages.filter((p) => p.id && options.packageAllowlist.includes(p.id));
|
|
38
|
-
}
|
|
35
|
+
const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
|
|
39
36
|
return jsonResponse({
|
|
40
37
|
ok: true,
|
|
41
38
|
packages,
|
|
@@ -64,13 +61,10 @@ export async function handleInfoRoute(ctx) {
|
|
|
64
61
|
const intent = url.searchParams.get("intent") || undefined;
|
|
65
62
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
66
63
|
if (targetPkg) {
|
|
67
|
-
assertPackageAllowed(targetPkg,
|
|
64
|
+
assertPackageAllowed(targetPkg, policy);
|
|
68
65
|
}
|
|
69
66
|
const rawPackages = await service.info();
|
|
70
|
-
|
|
71
|
-
if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
|
|
72
|
-
packages = packages.filter((p) => p.id && options.packageAllowlist.includes(p.id));
|
|
73
|
-
}
|
|
67
|
+
const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
|
|
74
68
|
if (isTree) {
|
|
75
69
|
return jsonResponse({ ok: true, type: "tree", packages }, 200, corsHeaders);
|
|
76
70
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { filterByIntent } from "../../filter/index.js";
|
|
2
2
|
import { PACKAGE_NOT_ALLOWED, PLAYBOOK_NOT_FOUND, PLAYBOOKS_LIST_ERROR } from "../../errors.js";
|
|
3
|
-
import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
|
|
3
|
+
import { assertPackageAllowed, getSubPath, isPackageAllowedByPolicy, jsonResponse } from "./common.js";
|
|
4
4
|
/**
|
|
5
5
|
* 处理 Playbook 规程相关的 HTTP 路由(列表与 SOP 详情查询)。
|
|
6
6
|
*/
|
|
7
7
|
export async function handlePlaybooksRoutes(ctx) {
|
|
8
|
-
const { req, url, pathname, corsHeaders, service, options } = ctx;
|
|
8
|
+
const { req, url, pathname, corsHeaders, service, options, activePolicy: policy } = ctx;
|
|
9
9
|
const subpath = getSubPath(pathname);
|
|
10
10
|
// 1. Playbooks List: GET /api/v2/playbooks, GET /playbooks
|
|
11
11
|
if (subpath === "/playbooks" && req.method === "GET") {
|
|
@@ -13,14 +13,14 @@ export async function handlePlaybooksRoutes(ctx) {
|
|
|
13
13
|
const intent = url.searchParams.get("intent") || undefined;
|
|
14
14
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
15
15
|
if (targetPkg) {
|
|
16
|
-
assertPackageAllowed(targetPkg,
|
|
16
|
+
assertPackageAllowed(targetPkg, policy);
|
|
17
17
|
}
|
|
18
18
|
let pbs = await service.discovery.listPlaybooks({ intent, package: targetPkg });
|
|
19
19
|
if (targetPkg) {
|
|
20
20
|
pbs = pbs.filter((p) => p.packageId === targetPkg);
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
23
|
-
pbs = pbs.filter((p) => p.packageId
|
|
22
|
+
if (policy.packageAllowlist && Array.isArray(policy.packageAllowlist) && policy.packageAllowlist.length > 0) {
|
|
23
|
+
pbs = pbs.filter((p) => isPackageAllowedByPolicy(p.packageId, policy));
|
|
24
24
|
}
|
|
25
25
|
if (intent) {
|
|
26
26
|
pbs = filterByIntent(pbs, intent, [(p) => p.id, (p) => p.description || "", (p) => p.packageId || "", (p) => (p.actions || []).join(" ")], false);
|
|
@@ -46,7 +46,7 @@ export async function handlePlaybooksRoutes(ctx) {
|
|
|
46
46
|
const packageId = decodeURIComponent(pkgPlaybookMatch[1]);
|
|
47
47
|
const playbookId = decodeURIComponent(pkgPlaybookMatch[2]);
|
|
48
48
|
try {
|
|
49
|
-
assertPackageAllowed(packageId,
|
|
49
|
+
assertPackageAllowed(packageId, policy);
|
|
50
50
|
}
|
|
51
51
|
catch (err) {
|
|
52
52
|
return jsonResponse({
|
|
@@ -79,7 +79,7 @@ export async function handlePlaybooksRoutes(ctx) {
|
|
|
79
79
|
if (playbookId.includes("/")) {
|
|
80
80
|
const pkgFromRef = playbookId.slice(0, playbookId.lastIndexOf("/"));
|
|
81
81
|
try {
|
|
82
|
-
assertPackageAllowed(pkgFromRef,
|
|
82
|
+
assertPackageAllowed(pkgFromRef, policy);
|
|
83
83
|
}
|
|
84
84
|
catch (err) {
|
|
85
85
|
return jsonResponse({
|
|
@@ -101,7 +101,7 @@ export async function handlePlaybooksRoutes(ctx) {
|
|
|
101
101
|
pkgId = matched.packageId;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
assertPackageAllowed(pkgId,
|
|
104
|
+
assertPackageAllowed(pkgId, policy);
|
|
105
105
|
return jsonResponse(pb, 200, corsHeaders);
|
|
106
106
|
}
|
|
107
107
|
catch (err) {
|