@actiondock/core 2.8.0 → 2.9.1
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 +21 -1
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +2 -0
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +10 -0
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/ipc/host.js +1 -0
- package/dist/ipc/service.js +3 -0
- package/dist/package/runtime.d.ts +3 -0
- package/dist/package/runtime.js +8 -0
- package/dist/package/types.d.ts +4 -0
- package/dist/platform/node.js +1 -0
- package/dist/platform/types.d.ts +3 -1
- package/dist/profile/client-runs.d.ts +10 -1
- package/dist/profile/client-runs.js +14 -1
- package/dist/profile/client.d.ts +1 -1
- package/dist/profile/client.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 +111 -30
- 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 +215 -30
- package/dist/server/types.d.ts +60 -2
- package/dist/service/local.js +3 -0
- package/dist/service/remote.js +16 -1
- package/dist/service/types.d.ts +4 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +6 -1
- package/dist/storage/sqlite.d.ts +18 -1
- package/dist/storage/sqlite.js +159 -2
- package/dist/storage/types.d.ts +29 -0
- package/dist/storage/types.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
2
|
+
import { ActionDockError, ACTION_FORBIDDEN, PACKAGE_NOT_ALLOWED } from "../errors.js";
|
|
3
|
+
import { safeEqual } from "./security.js";
|
|
4
|
+
export { extractBearerToken } from "./security.js";
|
|
5
|
+
/**
|
|
6
|
+
* 动作未列入白名单拒绝访问异常。
|
|
7
|
+
*/
|
|
8
|
+
export class ActionForbiddenError extends ActionDockError {
|
|
9
|
+
status = 403;
|
|
10
|
+
statusCode = 403;
|
|
11
|
+
constructor(actionRef) {
|
|
12
|
+
super(ACTION_FORBIDDEN, `Action '${actionRef}' is not in the allowed action list`);
|
|
13
|
+
this.name = "ActionForbiddenError";
|
|
14
|
+
Object.setPrototypeOf(this, ActionForbiddenError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 包未列入白名单拒绝访问异常。
|
|
19
|
+
*/
|
|
20
|
+
export class PackageNotAllowedError extends ActionDockError {
|
|
21
|
+
status = 403;
|
|
22
|
+
statusCode = 403;
|
|
23
|
+
constructor(packageId) {
|
|
24
|
+
super(PACKAGE_NOT_ALLOWED, `Package '${packageId}' is not in the allowed package list`);
|
|
25
|
+
this.name = "PackageNotAllowedError";
|
|
26
|
+
Object.setPrototypeOf(this, PackageNotAllowedError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 根据客户端携带的 Token 智能匹配已注册的视图。
|
|
31
|
+
* 使用 safeEqual 进行恒定时间比较以防范时序攻击。
|
|
32
|
+
*
|
|
33
|
+
* @param token 客户端提供的 Bearer Token
|
|
34
|
+
* @param views 可遍历的视图集合
|
|
35
|
+
* @returns 匹配成功的视图对象,未命中返回 undefined
|
|
36
|
+
*/
|
|
37
|
+
export function matchViewByToken(token, views) {
|
|
38
|
+
const trimmed = token.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
let matched = undefined;
|
|
43
|
+
for (const view of views) {
|
|
44
|
+
const candidateToken = view.policy.token?.trim();
|
|
45
|
+
if (candidateToken) {
|
|
46
|
+
const isMatch = safeEqual(trimmed, candidateToken);
|
|
47
|
+
if (isMatch && !matched) {
|
|
48
|
+
matched = view;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return matched;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 归一化解析服务端配置的所有视图集合。
|
|
56
|
+
* 保证默认视图与显式自定义视图具有统一的数据结构。
|
|
57
|
+
*
|
|
58
|
+
* @param options 服务端配置选项
|
|
59
|
+
* @returns 默认视图与全部视图 Map 映射
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeServerViews(options) {
|
|
62
|
+
const views = new Map();
|
|
63
|
+
// 1. 初始化基于全局配置的默认视图
|
|
64
|
+
const defaultRawOptions = {
|
|
65
|
+
name: "default",
|
|
66
|
+
token: options.token,
|
|
67
|
+
packageAllowlist: options.packageAllowlist,
|
|
68
|
+
actionAllowlist: options.actionAllowlist,
|
|
69
|
+
enableManagement: options.enableManagement,
|
|
70
|
+
enableMcp: options.enableMcp !== false,
|
|
71
|
+
mcpHandler: options.mcpHandler,
|
|
72
|
+
};
|
|
73
|
+
const customViews = new Map();
|
|
74
|
+
// 2. 解析 options.views 自定义视图
|
|
75
|
+
if (options.views) {
|
|
76
|
+
if (Array.isArray(options.views)) {
|
|
77
|
+
for (let i = 0; i < options.views.length; i++) {
|
|
78
|
+
const item = options.views[i];
|
|
79
|
+
if (!item || typeof item !== "object")
|
|
80
|
+
continue;
|
|
81
|
+
const name = item.name?.trim() || `view_${i}`;
|
|
82
|
+
if (!name || name === "." || name === "..") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (name === "default") {
|
|
86
|
+
Object.assign(defaultRawOptions, item);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
customViews.set(name, {
|
|
90
|
+
name,
|
|
91
|
+
policy: {
|
|
92
|
+
viewName: name,
|
|
93
|
+
token: item.token,
|
|
94
|
+
packageAllowlist: item.packageAllowlist,
|
|
95
|
+
actionAllowlist: item.actionAllowlist,
|
|
96
|
+
enableManagement: item.enableManagement,
|
|
97
|
+
},
|
|
98
|
+
enableMcp: item.enableMcp !== false,
|
|
99
|
+
rawOptions: item,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (typeof options.views === "object") {
|
|
105
|
+
for (const [key, item] of Object.entries(options.views)) {
|
|
106
|
+
if (!item || typeof item !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
const name = item.name?.trim() || key.trim();
|
|
109
|
+
if (!name || name === "." || name === "..") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (name === "default") {
|
|
113
|
+
Object.assign(defaultRawOptions, item);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
customViews.set(name, {
|
|
117
|
+
name,
|
|
118
|
+
policy: {
|
|
119
|
+
viewName: name,
|
|
120
|
+
token: item.token,
|
|
121
|
+
packageAllowlist: item.packageAllowlist,
|
|
122
|
+
actionAllowlist: item.actionAllowlist,
|
|
123
|
+
enableManagement: item.enableManagement,
|
|
124
|
+
},
|
|
125
|
+
enableMcp: item.enableMcp !== false,
|
|
126
|
+
rawOptions: item,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const defaultView = {
|
|
133
|
+
name: "default",
|
|
134
|
+
policy: {
|
|
135
|
+
viewName: "default",
|
|
136
|
+
token: defaultRawOptions.token,
|
|
137
|
+
packageAllowlist: defaultRawOptions.packageAllowlist,
|
|
138
|
+
actionAllowlist: defaultRawOptions.actionAllowlist,
|
|
139
|
+
enableManagement: defaultRawOptions.enableManagement,
|
|
140
|
+
},
|
|
141
|
+
enableMcp: defaultRawOptions.enableMcp !== false,
|
|
142
|
+
rawOptions: defaultRawOptions,
|
|
143
|
+
};
|
|
144
|
+
// 默认视图置于首位,随后装载自定义视图
|
|
145
|
+
views.set("default", defaultView);
|
|
146
|
+
for (const [k, v] of customViews) {
|
|
147
|
+
views.set(k, v);
|
|
148
|
+
}
|
|
149
|
+
return { defaultView, views };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 校验目标动作是否在 actionAllowlist 允许白名单中。
|
|
153
|
+
* 支持短名 actionId 与全限定名 packageId/actionId 匹配:
|
|
154
|
+
* - 白名单项若含 '/'(如 pkg/act),则必须 packageId 与 actionId 同时精确匹配。
|
|
155
|
+
* - 白名单项若不含 '/'(如 act),则只需 actionId 匹配(不限包)。
|
|
156
|
+
*
|
|
157
|
+
* @param action 目标动作对象或引用字符串
|
|
158
|
+
* @param allowlist 允许动作白名单列表
|
|
159
|
+
* @returns 是否允许访问
|
|
160
|
+
*/
|
|
161
|
+
export function isActionAllowed(action, allowlist) {
|
|
162
|
+
if (!allowlist || !Array.isArray(allowlist) || allowlist.length === 0) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (!action) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
let target;
|
|
169
|
+
try {
|
|
170
|
+
if (typeof action === "string") {
|
|
171
|
+
target = parseActionRef(action);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const rawId = action.actionId || action.id;
|
|
175
|
+
if (!rawId)
|
|
176
|
+
return false;
|
|
177
|
+
if (action.packageId) {
|
|
178
|
+
if (rawId.includes("/")) {
|
|
179
|
+
target = parseActionRef(rawId);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
target = parseActionRef({
|
|
183
|
+
packageId: action.packageId,
|
|
184
|
+
actionId: rawId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
target = parseActionRef(rawId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
for (const item of allowlist) {
|
|
197
|
+
if (typeof item !== "string")
|
|
198
|
+
continue;
|
|
199
|
+
const trimmed = item.trim();
|
|
200
|
+
if (!trimmed)
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
const rule = parseActionRef(trimmed);
|
|
204
|
+
if (rule.packageId) {
|
|
205
|
+
if (rule.packageId === target.packageId && rule.actionId === target.actionId) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
if (rule.actionId === target.actionId) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 根据生效策略统一判定目标动作是否允许访问。
|
|
223
|
+
* 同时综合校验策略中的 packageAllowlist 与 actionAllowlist 约束。
|
|
224
|
+
*
|
|
225
|
+
* @param action 目标动作对象或引用字符串
|
|
226
|
+
* @param policy 当前生效的服务端策略
|
|
227
|
+
* @returns 是否允许访问
|
|
228
|
+
*/
|
|
229
|
+
export function isActionAllowedByPolicy(action, policy) {
|
|
230
|
+
if (!policy) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (!action) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
let pkgId;
|
|
237
|
+
let actId;
|
|
238
|
+
try {
|
|
239
|
+
if (typeof action === "string") {
|
|
240
|
+
const parsed = parseActionRef(action);
|
|
241
|
+
pkgId = parsed.packageId;
|
|
242
|
+
actId = parsed.actionId;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
pkgId = action.packageId;
|
|
246
|
+
const rawId = action.actionId || action.id;
|
|
247
|
+
if (!rawId)
|
|
248
|
+
return false;
|
|
249
|
+
if (rawId.includes("/")) {
|
|
250
|
+
const parsed = parseActionRef(rawId);
|
|
251
|
+
pkgId = parsed.packageId || pkgId;
|
|
252
|
+
actId = parsed.actionId;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
actId = rawId;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
// 1. 若配置了 packageAllowlist,校验所属包白名单
|
|
263
|
+
if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
|
|
264
|
+
if (pkgId) {
|
|
265
|
+
if (!policy.packageAllowlist.includes(pkgId)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// 未指明所属包时,仅当其动作标识明确列入 actionAllowlist 时才允许放行
|
|
271
|
+
if (!policy.actionAllowlist ||
|
|
272
|
+
policy.actionAllowlist.length === 0 ||
|
|
273
|
+
!isActionAllowed({ actionId: actId }, policy.actionAllowlist)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// 2. 若配置了 actionAllowlist,校验动作白名单
|
|
279
|
+
if (policy.actionAllowlist && policy.actionAllowlist.length > 0) {
|
|
280
|
+
if (!isActionAllowed({ packageId: pkgId, actionId: actId }, policy.actionAllowlist)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 根据生效策略统一判定目标包标识是否允许访问。
|
|
288
|
+
*
|
|
289
|
+
* @param packageId 目标包 ID
|
|
290
|
+
* @param policy 当前生效的服务端策略
|
|
291
|
+
* @returns 是否允许访问
|
|
292
|
+
*/
|
|
293
|
+
export function isPackageAllowedByPolicy(packageId, policy) {
|
|
294
|
+
if (!policy?.packageAllowlist || policy.packageAllowlist.length === 0) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (!packageId) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return policy.packageAllowlist.includes(packageId);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 根据生效策略统一判定配置与状态管理路由是否开启。
|
|
304
|
+
*
|
|
305
|
+
* @param policy 当前生效的服务端策略
|
|
306
|
+
* @returns 是否允许管理操作
|
|
307
|
+
*/
|
|
308
|
+
export function isManagementAllowedByPolicy(policy) {
|
|
309
|
+
return policy?.enableManagement === true;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 根据生效策略对动作列表进行统一过滤。
|
|
313
|
+
*
|
|
314
|
+
* @param actions 待过滤的动作列表
|
|
315
|
+
* @param policy 当前生效的服务端策略
|
|
316
|
+
* @returns 过滤后允许访问的动作列表
|
|
317
|
+
*/
|
|
318
|
+
export function filterActionsByPolicy(actions, policy) {
|
|
319
|
+
if (!policy) {
|
|
320
|
+
return actions;
|
|
321
|
+
}
|
|
322
|
+
const hasPkg = policy.packageAllowlist && policy.packageAllowlist.length > 0;
|
|
323
|
+
const hasAct = policy.actionAllowlist && policy.actionAllowlist.length > 0;
|
|
324
|
+
if (!hasPkg && !hasAct) {
|
|
325
|
+
return actions;
|
|
326
|
+
}
|
|
327
|
+
return actions.filter((a) => isActionAllowedByPolicy({ packageId: a.packageId, actionId: a.actionId || a.id }, policy));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 根据生效策略对包列表进行统一过滤。
|
|
331
|
+
*
|
|
332
|
+
* @param packages 待过滤的包列表
|
|
333
|
+
* @param policy 当前生效的服务端策略
|
|
334
|
+
* @returns 过滤后允许访问的包列表
|
|
335
|
+
*/
|
|
336
|
+
export function filterPackagesByPolicy(packages, policy) {
|
|
337
|
+
if (!policy?.packageAllowlist || policy.packageAllowlist.length === 0) {
|
|
338
|
+
return packages;
|
|
339
|
+
}
|
|
340
|
+
return packages.filter((p) => p.id && policy.packageAllowlist.includes(p.id));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 校验目标动作是否属于允许白名单,不符合则抛出 403 异常。
|
|
344
|
+
*
|
|
345
|
+
* @param action 目标动作对象或引用
|
|
346
|
+
* @param policy 当前生效的服务端策略
|
|
347
|
+
*/
|
|
348
|
+
export function assertActionAllowedByPolicy(action, policy) {
|
|
349
|
+
if (!isActionAllowedByPolicy(action, policy)) {
|
|
350
|
+
const actRef = typeof action === "string"
|
|
351
|
+
? action
|
|
352
|
+
: action.packageId
|
|
353
|
+
? `${action.packageId}/${action.actionId || action.id}`
|
|
354
|
+
: action.actionId || action.id || "unknown";
|
|
355
|
+
throw new ActionForbiddenError(actRef);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 校验目标包是否属于允许白名单,不符合则抛出 403 异常。
|
|
360
|
+
*
|
|
361
|
+
* @param packageId 目标包 ID
|
|
362
|
+
* @param policy 当前生效的服务端策略
|
|
363
|
+
*/
|
|
364
|
+
export function assertPackageAllowedByPolicy(packageId, policy) {
|
|
365
|
+
if (!isPackageAllowedByPolicy(packageId, policy)) {
|
|
366
|
+
throw new PackageNotAllowedError(packageId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -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;
|