@actiondock/core 2.5.1 → 2.7.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/dist/app/app.d.ts +14 -1
- package/dist/app/app.js +88 -222
- package/dist/app/static-index.d.ts +39 -0
- package/dist/app/static-index.js +164 -0
- package/dist/app/types.d.ts +5 -0
- package/dist/catalog/action-index.js +6 -4
- package/dist/catalog/package-catalog.js +2 -1
- package/dist/doctor/checks/index.d.ts +5 -0
- package/dist/doctor/checks/index.js +5 -0
- package/dist/doctor/checks/project-locate.d.ts +6 -0
- package/dist/doctor/checks/project-locate.js +36 -0
- package/dist/doctor/checks/project.d.ts +6 -0
- package/dist/doctor/checks/project.js +383 -0
- package/dist/doctor/checks/registry.d.ts +13 -0
- package/dist/doctor/checks/registry.js +207 -0
- package/dist/doctor/checks/runtime.d.ts +13 -0
- package/dist/doctor/checks/runtime.js +90 -0
- package/dist/doctor/checks/storage.d.ts +5 -0
- package/dist/doctor/checks/storage.js +34 -0
- package/dist/doctor/context.d.ts +35 -0
- package/dist/doctor/context.js +11 -0
- package/dist/doctor/doctor.d.ts +5 -0
- package/dist/doctor/doctor.js +33 -602
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +22 -0
- package/dist/execution/service.js +14 -9
- package/dist/export/templates.js +42 -30
- package/dist/host/host.d.ts +33 -0
- package/dist/host/host.js +222 -362
- package/dist/host/routing.d.ts +92 -0
- package/dist/host/routing.js +238 -0
- package/dist/host/types.d.ts +8 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/input/advice.d.ts +100 -0
- package/dist/input/advice.js +767 -0
- package/dist/input/describe.d.ts +106 -0
- package/dist/input/describe.js +176 -0
- package/dist/input/file-input.d.ts +54 -0
- package/dist/input/file-input.js +124 -0
- package/dist/input/flat-decode.d.ts +11 -0
- package/dist/input/flat-decode.js +13 -0
- package/dist/input/flat-errors.d.ts +72 -0
- package/dist/input/flat-errors.js +87 -0
- package/dist/input/flat-materializer.d.ts +29 -0
- package/dist/input/flat-materializer.js +179 -0
- package/dist/input/flat-parser.d.ts +50 -0
- package/dist/input/flat-parser.js +169 -0
- package/dist/input/flat-predicates.d.ts +34 -0
- package/dist/input/flat-predicates.js +50 -0
- package/dist/input/index.d.ts +14 -0
- package/dist/input/index.js +14 -0
- package/dist/input/input-resolver.d.ts +63 -0
- package/dist/input/input-resolver.js +254 -0
- package/dist/input/json-depth-scanner.d.ts +17 -0
- package/dist/input/json-depth-scanner.js +49 -0
- package/dist/input/metadata.d.ts +99 -0
- package/dist/input/metadata.js +99 -0
- package/dist/input/stdin-input.d.ts +26 -0
- package/dist/input/stdin-input.js +125 -0
- package/dist/input/utf8.d.ts +19 -0
- package/dist/input/utf8.js +36 -0
- package/dist/input/validation-mapper.d.ts +27 -0
- package/dist/input/validation-mapper.js +144 -0
- package/dist/ipc/host.js +38 -0
- package/dist/json/index.d.ts +1 -0
- package/dist/json/index.js +1 -0
- package/dist/json/value-validator.d.ts +88 -0
- package/dist/json/value-validator.js +330 -0
- package/dist/platform/types.d.ts +7 -0
- package/dist/process/control-arbiter.d.ts +107 -0
- package/dist/process/control-arbiter.js +251 -0
- package/dist/process/diagnostics-sink.d.ts +26 -0
- package/dist/process/diagnostics-sink.js +35 -0
- package/dist/process/index.d.ts +7 -0
- package/dist/process/index.js +7 -0
- package/dist/process/input-dispatcher.d.ts +88 -0
- package/dist/process/input-dispatcher.js +240 -0
- package/dist/process/managed-record.d.ts +78 -0
- package/dist/process/managed-record.js +1 -0
- package/dist/process/process-manager.d.ts +15 -56
- package/dist/process/process-manager.js +119 -707
- package/dist/process/reservation-table.d.ts +39 -0
- package/dist/process/reservation-table.js +80 -0
- package/dist/process/run-executor.d.ts +16 -0
- package/dist/process/run-executor.js +155 -0
- package/dist/process/terminal-output-cache.d.ts +66 -0
- package/dist/process/terminal-output-cache.js +132 -0
- package/dist/profile/client-actions.d.ts +56 -0
- package/dist/profile/client-actions.js +215 -0
- package/dist/profile/client-config.d.ts +20 -0
- package/dist/profile/client-config.js +51 -0
- package/dist/profile/client-health.d.ts +14 -0
- package/dist/profile/client-health.js +69 -0
- package/dist/profile/client-playbooks.d.ts +19 -0
- package/dist/profile/client-playbooks.js +36 -0
- package/dist/profile/client-query.d.ts +30 -0
- package/dist/profile/client-query.js +70 -0
- package/dist/profile/client-runs.d.ts +42 -0
- package/dist/profile/client-runs.js +58 -0
- package/dist/profile/client-state.d.ts +56 -0
- package/dist/profile/client-state.js +83 -0
- package/dist/profile/client-transport.d.ts +61 -0
- package/dist/profile/client-transport.js +156 -0
- package/dist/profile/client.d.ts +23 -142
- package/dist/profile/client.js +28 -588
- package/dist/profile/manager.js +1 -1
- package/dist/project/closure.d.ts +4 -4
- package/dist/project/closure.js +11 -5
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +0 -25
- package/dist/project/loader.js +1 -154
- package/dist/project/transactions.d.ts +12 -14
- package/dist/project/transactions.js +60 -836
- package/dist/registry/registry.js +6 -4
- package/dist/registry/resolve.js +1 -1
- package/dist/runtime/action-registry.d.ts +45 -0
- package/dist/runtime/action-registry.js +108 -0
- package/dist/runtime/clock.d.ts +5 -17
- package/dist/runtime/clock.js +5 -15
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.js +17 -56
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/package-runner-factory.d.ts +82 -0
- package/dist/runtime/package-runner-factory.js +186 -0
- package/dist/runtime/run-persistence.d.ts +85 -0
- package/dist/runtime/run-persistence.js +111 -0
- package/dist/runtime/runner.d.ts +25 -45
- package/dist/runtime/runner.js +130 -354
- package/dist/runtime/standalone.d.ts +25 -2
- package/dist/runtime/standalone.js +305 -87
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/validator.d.ts +10 -0
- package/dist/schema/validator.js +48 -54
- package/dist/server/dispatcher.d.ts +8 -3
- package/dist/server/dispatcher.js +16 -40
- package/dist/server/routes/runs.js +6 -4
- package/dist/server/security.d.ts +1 -7
- package/dist/server/security.js +2 -13
- package/dist/storage/clock.d.ts +25 -0
- package/dist/storage/clock.js +23 -0
- package/dist/storage/data-dir-lock.d.ts +3 -36
- package/dist/storage/data-dir-lock.js +48 -881
- package/dist/storage/index.d.ts +8 -1
- package/dist/storage/index.js +8 -2
- package/dist/storage/lazy.js +1 -0
- package/dist/storage/lock-core.d.ts +162 -0
- package/dist/storage/lock-core.js +885 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +81 -89
- package/dist/storage/types.d.ts +27 -5
- package/dist/storage/types.js +21 -0
- package/dist/storage/utils.d.ts +22 -0
- package/dist/storage/utils.js +34 -0
- package/dist/target/local.d.ts +1 -0
- package/dist/target/local.js +23 -17
- package/dist/target/remote-errors.d.ts +21 -0
- package/dist/target/remote-errors.js +51 -0
- package/dist/target/remote-polling.d.ts +35 -0
- package/dist/target/remote-polling.js +104 -0
- package/dist/target/remote.d.ts +4 -0
- package/dist/target/remote.js +108 -301
- package/dist/target/sse-parser.d.ts +35 -0
- package/dist/target/sse-parser.js +163 -0
- package/dist/target/target.js +2 -0
- package/dist/target/types.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/net.d.ts +10 -0
- package/dist/utils/net.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { inputPathConflict, flatInputLimitExceeded, invalidFlatArgument, } from "./flat-errors.js";
|
|
2
|
+
import { isForbiddenActionInputPropertyName } from "./flat-predicates.js";
|
|
3
|
+
import { DEFAULT_MAX_MATERIALIZED_SIZE_BYTES } from "./flat-parser.js";
|
|
4
|
+
import { validateJsonValue } from "../json/value-validator.js";
|
|
5
|
+
/**
|
|
6
|
+
* 节点状态枚举(四态节点模型)。
|
|
7
|
+
*/
|
|
8
|
+
export const NodeState = {
|
|
9
|
+
UNSET: "UNSET",
|
|
10
|
+
OBJECT: "OBJECT",
|
|
11
|
+
ARRAY: "ARRAY",
|
|
12
|
+
VALUE: "VALUE",
|
|
13
|
+
};
|
|
14
|
+
function createNode(pathStr, state = NodeState.UNSET) {
|
|
15
|
+
return {
|
|
16
|
+
state,
|
|
17
|
+
pathStr,
|
|
18
|
+
objectChildren: Object.create(null),
|
|
19
|
+
arrayChildren: new Map(),
|
|
20
|
+
value: undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function materializeNode(node) {
|
|
24
|
+
switch (node.state) {
|
|
25
|
+
case NodeState.VALUE:
|
|
26
|
+
return node.value;
|
|
27
|
+
case NodeState.OBJECT: {
|
|
28
|
+
const result = {};
|
|
29
|
+
const keys = Object.keys(node.objectChildren).sort();
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
result[key] = materializeNode(node.objectChildren[key]);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
case NodeState.ARRAY: {
|
|
36
|
+
const count = node.arrayChildren.size;
|
|
37
|
+
if (count === 0) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const result = [];
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
const child = node.arrayChildren.get(i);
|
|
43
|
+
if (!child) {
|
|
44
|
+
throw inputPathConflict(`Sparse array detected at "${node.pathStr}": missing index ${i}`, { path: node.pathStr, missingIndex: i, reason: "SPARSE_ARRAY" });
|
|
45
|
+
}
|
|
46
|
+
result.push(materializeNode(child));
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
case NodeState.UNSET:
|
|
51
|
+
default:
|
|
52
|
+
throw inputPathConflict(`Incomplete intermediate node at "${node.pathStr}"`, { path: node.pathStr, reason: "INCOMPLETE_CONTAINER" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 将扁平赋值表达式列表物化为标准 JSON 值对象。
|
|
57
|
+
*
|
|
58
|
+
* 安全规范:
|
|
59
|
+
* - 错误信息与 details 中严禁回显原始 raw、rawValue 或完整 token,仅保留 path、operator、valueLength。
|
|
60
|
+
*
|
|
61
|
+
* @param assignments 扁平赋值表达式列表
|
|
62
|
+
* @param options 物化配置选项
|
|
63
|
+
* @returns 物化后的 JSON 对象
|
|
64
|
+
*/
|
|
65
|
+
export function materializeFlatInput(assignments, options) {
|
|
66
|
+
const root = createNode("", NodeState.OBJECT);
|
|
67
|
+
for (const assignment of assignments) {
|
|
68
|
+
const { path, value, operator, rawValue } = assignment;
|
|
69
|
+
const valueLength = Buffer.byteLength(rawValue, "utf8");
|
|
70
|
+
if (path.length === 0) {
|
|
71
|
+
throw inputPathConflict(`Cannot assign value directly to root object`, { operator, valueLength, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
72
|
+
}
|
|
73
|
+
let current = root;
|
|
74
|
+
for (let i = 0; i < path.length; i++) {
|
|
75
|
+
const seg = path[i];
|
|
76
|
+
const isLast = i === path.length - 1;
|
|
77
|
+
const currentPathStr = path.slice(0, i + 1).join(".");
|
|
78
|
+
if (typeof seg === "string") {
|
|
79
|
+
if (isForbiddenActionInputPropertyName(seg)) {
|
|
80
|
+
throw invalidFlatArgument(`Forbidden property "${seg}" in path: "${currentPathStr}"`, { path: currentPathStr, segment: seg, reason: "FORBIDDEN_PROPERTY" });
|
|
81
|
+
}
|
|
82
|
+
if (current.state !== NodeState.OBJECT) {
|
|
83
|
+
throw inputPathConflict(`Path conflict at "${current.pathStr}": expected object container but found ${current.state.toLowerCase()}`, { path: currentPathStr, operator, valueLength, reason: "OBJECT_ARRAY_CONFLICT" });
|
|
84
|
+
}
|
|
85
|
+
let child = Object.hasOwn(current.objectChildren, seg)
|
|
86
|
+
? current.objectChildren[seg]
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!child) {
|
|
89
|
+
child = createNode(currentPathStr);
|
|
90
|
+
current.objectChildren[seg] = child;
|
|
91
|
+
}
|
|
92
|
+
if (isLast) {
|
|
93
|
+
if (child.state === NodeState.UNSET) {
|
|
94
|
+
child.state = NodeState.VALUE;
|
|
95
|
+
child.value = value;
|
|
96
|
+
}
|
|
97
|
+
else if (child.state === NodeState.VALUE) {
|
|
98
|
+
throw inputPathConflict(`Duplicate assignment to leaf path "${currentPathStr}"`, { path: currentPathStr, operator, valueLength, reason: "DUPLICATE_ASSIGNMENT" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": cannot assign value to existing container (${child.state.toLowerCase()})`, { path: currentPathStr, operator, valueLength, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const nextSeg = path[i + 1];
|
|
106
|
+
const nextExpectedState = typeof nextSeg === "number" ? NodeState.ARRAY : NodeState.OBJECT;
|
|
107
|
+
if (child.state === NodeState.UNSET) {
|
|
108
|
+
child.state = nextExpectedState;
|
|
109
|
+
}
|
|
110
|
+
else if (child.state === NodeState.VALUE) {
|
|
111
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": cannot access property "${nextSeg}" on existing value`, { path: currentPathStr, operator, valueLength, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
112
|
+
}
|
|
113
|
+
else if (child.state !== nextExpectedState) {
|
|
114
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": expected ${nextExpectedState.toLowerCase()} but found ${child.state.toLowerCase()}`, { path: currentPathStr, operator, valueLength, reason: "OBJECT_ARRAY_CONFLICT" });
|
|
115
|
+
}
|
|
116
|
+
current = child;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// seg is a number
|
|
121
|
+
if (current.state !== NodeState.ARRAY) {
|
|
122
|
+
throw inputPathConflict(`Path conflict at "${current.pathStr || "root"}": expected array container but found ${current.state.toLowerCase()}`, {
|
|
123
|
+
path: currentPathStr,
|
|
124
|
+
operator,
|
|
125
|
+
valueLength,
|
|
126
|
+
reason: current.pathStr === "" ? "ROOT_INDEX_NOT_ALLOWED" : "OBJECT_ARRAY_CONFLICT",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
let child = current.arrayChildren.get(seg);
|
|
130
|
+
if (!child) {
|
|
131
|
+
child = createNode(currentPathStr);
|
|
132
|
+
current.arrayChildren.set(seg, child);
|
|
133
|
+
}
|
|
134
|
+
if (isLast) {
|
|
135
|
+
if (child.state === NodeState.UNSET) {
|
|
136
|
+
child.state = NodeState.VALUE;
|
|
137
|
+
child.value = value;
|
|
138
|
+
}
|
|
139
|
+
else if (child.state === NodeState.VALUE) {
|
|
140
|
+
throw inputPathConflict(`Duplicate assignment to leaf path "${currentPathStr}"`, { path: currentPathStr, operator, valueLength, reason: "DUPLICATE_ASSIGNMENT" });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": cannot assign value to existing container (${child.state.toLowerCase()})`, { path: currentPathStr, operator, valueLength, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const nextSeg = path[i + 1];
|
|
148
|
+
const nextExpectedState = typeof nextSeg === "number" ? NodeState.ARRAY : NodeState.OBJECT;
|
|
149
|
+
if (child.state === NodeState.UNSET) {
|
|
150
|
+
child.state = nextExpectedState;
|
|
151
|
+
}
|
|
152
|
+
else if (child.state === NodeState.VALUE) {
|
|
153
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": cannot access index/property "${nextSeg}" on existing value`, { path: currentPathStr, operator, valueLength, reason: "LEAF_CONTAINER_CONFLICT" });
|
|
154
|
+
}
|
|
155
|
+
else if (child.state !== nextExpectedState) {
|
|
156
|
+
throw inputPathConflict(`Path conflict at "${currentPathStr}": expected ${nextExpectedState.toLowerCase()} but found ${child.state.toLowerCase()}`, { path: currentPathStr, operator, valueLength, reason: "OBJECT_ARRAY_CONFLICT" });
|
|
157
|
+
}
|
|
158
|
+
current = child;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const result = materializeNode(root);
|
|
164
|
+
const check = validateJsonValue(result);
|
|
165
|
+
if (!check.valid) {
|
|
166
|
+
throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
|
|
167
|
+
reason: check.code === "MAX_JSON_DEPTH"
|
|
168
|
+
? "MAX_MATERIALIZED_JSON_DEPTH"
|
|
169
|
+
: "INVALID_JSON_VALUE",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
|
|
173
|
+
const serialized = JSON.stringify(result);
|
|
174
|
+
const byteLength = Buffer.byteLength(serialized, "utf8");
|
|
175
|
+
if (byteLength > maxMaterializedSizeBytes) {
|
|
176
|
+
throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { JsonValue } from "@actiondock/sdk";
|
|
2
|
+
/** 最大赋值表达式总数 */
|
|
3
|
+
export declare const DEFAULT_MAX_ASSIGNMENTS = 1000;
|
|
4
|
+
/** 最大路径段深度 */
|
|
5
|
+
export declare const DEFAULT_MAX_PATH_DEPTH = 32;
|
|
6
|
+
/** 原始路径字符串最大字节数 */
|
|
7
|
+
export declare const DEFAULT_MAX_PATH_LENGTH = 1024;
|
|
8
|
+
/** 单个属性名最大字节数 */
|
|
9
|
+
export declare const DEFAULT_MAX_PROPERTY_KEY_LENGTH = 128;
|
|
10
|
+
/** 原始值字符串最大字节数 (1 MiB) */
|
|
11
|
+
export declare const DEFAULT_MAX_RAW_VALUE_LENGTH: number;
|
|
12
|
+
/** JSON 字面量最大字节数 (1 MiB) */
|
|
13
|
+
export declare const DEFAULT_MAX_JSON_LITERAL_LENGTH: number;
|
|
14
|
+
/** 原始输入总字节数上限 (10 MiB) */
|
|
15
|
+
export declare const DEFAULT_MAX_TOTAL_RAW_BYTES: number;
|
|
16
|
+
/** 数组索引数值上限 */
|
|
17
|
+
export declare const DEFAULT_MAX_ARRAY_INDEX = 10000;
|
|
18
|
+
/** 物化后总大小字节上限 (10 MiB) */
|
|
19
|
+
export declare const DEFAULT_MAX_MATERIALIZED_SIZE_BYTES: number;
|
|
20
|
+
/**
|
|
21
|
+
* 扁平赋值表达式结构。
|
|
22
|
+
*/
|
|
23
|
+
export interface FlatAssignment {
|
|
24
|
+
raw: string;
|
|
25
|
+
path: Array<string | number>;
|
|
26
|
+
operator: "=" | ":=";
|
|
27
|
+
rawValue: string;
|
|
28
|
+
value: JsonValue;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 扁平解析器配置选项。
|
|
32
|
+
*/
|
|
33
|
+
export interface FlatParserOptions {
|
|
34
|
+
maxAssignments?: number;
|
|
35
|
+
maxPathDepth?: number;
|
|
36
|
+
maxPathLength?: number;
|
|
37
|
+
maxPropertyKeyLength?: number;
|
|
38
|
+
maxRawValueLength?: number;
|
|
39
|
+
maxJsonLiteralLength?: number;
|
|
40
|
+
maxArrayIndex?: number;
|
|
41
|
+
maxTotalRawBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析扁平赋值表达式列表。
|
|
45
|
+
*
|
|
46
|
+
* @param tokens 待解析的表达式字符串列表
|
|
47
|
+
* @param options 解析安全阈值配置选项
|
|
48
|
+
* @returns 解析后的结构化赋值列表
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseFlatAssignments(tokens: string[], options?: FlatParserOptions): FlatAssignment[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { invalidFlatArgument, invalidJsonLiteral, flatInputLimitExceeded, } from "./flat-errors.js";
|
|
2
|
+
import { isFlatPathPropertyName, isForbiddenActionInputPropertyName, } from "./flat-predicates.js";
|
|
3
|
+
import { validateJsonValue } from "../json/value-validator.js";
|
|
4
|
+
/** 最大赋值表达式总数 */
|
|
5
|
+
export const DEFAULT_MAX_ASSIGNMENTS = 1000;
|
|
6
|
+
/** 最大路径段深度 */
|
|
7
|
+
export const DEFAULT_MAX_PATH_DEPTH = 32;
|
|
8
|
+
/** 原始路径字符串最大字节数 */
|
|
9
|
+
export const DEFAULT_MAX_PATH_LENGTH = 1024;
|
|
10
|
+
/** 单个属性名最大字节数 */
|
|
11
|
+
export const DEFAULT_MAX_PROPERTY_KEY_LENGTH = 128;
|
|
12
|
+
/** 原始值字符串最大字节数 (1 MiB) */
|
|
13
|
+
export const DEFAULT_MAX_RAW_VALUE_LENGTH = 1024 * 1024;
|
|
14
|
+
/** JSON 字面量最大字节数 (1 MiB) */
|
|
15
|
+
export const DEFAULT_MAX_JSON_LITERAL_LENGTH = 1024 * 1024;
|
|
16
|
+
/** 原始输入总字节数上限 (10 MiB) */
|
|
17
|
+
export const DEFAULT_MAX_TOTAL_RAW_BYTES = 10 * 1024 * 1024;
|
|
18
|
+
/** 数组索引数值上限 */
|
|
19
|
+
export const DEFAULT_MAX_ARRAY_INDEX = 10_000;
|
|
20
|
+
/** 物化后总大小字节上限 (10 MiB) */
|
|
21
|
+
export const DEFAULT_MAX_MATERIALIZED_SIZE_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
const ARRAY_INDEX_REGEX = /^(0|[1-9][0-9]*)$/;
|
|
23
|
+
/**
|
|
24
|
+
* 解析单个扁平赋值表达式。
|
|
25
|
+
*
|
|
26
|
+
* 安全规范:
|
|
27
|
+
* - 错误信息与 details 中严禁回显未经脱敏的原始 rawValue 或 token,防止敏感凭据泄露至日志。
|
|
28
|
+
* - 资源限制统一使用 UTF-8 字节计算。
|
|
29
|
+
*/
|
|
30
|
+
function parseSingleFlatAssignment(token, options) {
|
|
31
|
+
const maxPathDepth = options?.maxPathDepth ?? DEFAULT_MAX_PATH_DEPTH;
|
|
32
|
+
const maxPathLength = options?.maxPathLength ?? DEFAULT_MAX_PATH_LENGTH;
|
|
33
|
+
const maxPropertyKeyLength = options?.maxPropertyKeyLength ?? DEFAULT_MAX_PROPERTY_KEY_LENGTH;
|
|
34
|
+
const maxRawValueLength = options?.maxRawValueLength ?? DEFAULT_MAX_RAW_VALUE_LENGTH;
|
|
35
|
+
const maxJsonLiteralLength = options?.maxJsonLiteralLength ?? DEFAULT_MAX_JSON_LITERAL_LENGTH;
|
|
36
|
+
const maxArrayIndex = options?.maxArrayIndex ?? DEFAULT_MAX_ARRAY_INDEX;
|
|
37
|
+
const colonEqIndex = token.indexOf(":=");
|
|
38
|
+
const eqIndex = token.indexOf("=");
|
|
39
|
+
let opIndex = -1;
|
|
40
|
+
let operator = "=";
|
|
41
|
+
if (colonEqIndex !== -1 && eqIndex !== -1) {
|
|
42
|
+
if (colonEqIndex < eqIndex) {
|
|
43
|
+
opIndex = colonEqIndex;
|
|
44
|
+
operator = ":=";
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
opIndex = eqIndex;
|
|
48
|
+
operator = "=";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (colonEqIndex !== -1) {
|
|
52
|
+
opIndex = colonEqIndex;
|
|
53
|
+
operator = ":=";
|
|
54
|
+
}
|
|
55
|
+
else if (eqIndex !== -1) {
|
|
56
|
+
opIndex = eqIndex;
|
|
57
|
+
operator = "=";
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw invalidFlatArgument(`Missing assignment operator in flat argument (length: ${token.length})`, { length: token.length, reason: "MISSING_OPERATOR" });
|
|
61
|
+
}
|
|
62
|
+
const rawPath = token.slice(0, opIndex);
|
|
63
|
+
const rawValue = token.slice(opIndex + operator.length);
|
|
64
|
+
const rawValueBytes = Buffer.byteLength(rawValue, "utf8");
|
|
65
|
+
if (rawPath.length === 0) {
|
|
66
|
+
throw invalidFlatArgument(`Empty path in flat assignment with operator '${operator}'`, { operator, valueLength: rawValueBytes, reason: "EMPTY_PATH" });
|
|
67
|
+
}
|
|
68
|
+
const pathByteLength = Buffer.byteLength(rawPath, "utf8");
|
|
69
|
+
if (pathByteLength > maxPathLength) {
|
|
70
|
+
throw flatInputLimitExceeded(`Path length (${pathByteLength} bytes) exceeds limit (${maxPathLength} bytes)`, { path: rawPath, length: pathByteLength, maxPathLength, reason: "MAX_PATH_BYTES" });
|
|
71
|
+
}
|
|
72
|
+
if (rawPath.startsWith(".") ||
|
|
73
|
+
rawPath.endsWith(".") ||
|
|
74
|
+
rawPath.includes("..")) {
|
|
75
|
+
throw invalidFlatArgument(`Invalid dot notation in path: "${rawPath}"`, { path: rawPath, reason: "INVALID_DOT_NOTATION" });
|
|
76
|
+
}
|
|
77
|
+
const rawSegments = rawPath.split(".");
|
|
78
|
+
if (rawSegments.length > maxPathDepth) {
|
|
79
|
+
throw flatInputLimitExceeded(`Path depth (${rawSegments.length}) exceeds limit (${maxPathDepth}) in path: "${rawPath}"`, { path: rawPath, depth: rawSegments.length, maxPathDepth, reason: "MAX_PATH_DEPTH" });
|
|
80
|
+
}
|
|
81
|
+
const path = [];
|
|
82
|
+
for (const seg of rawSegments) {
|
|
83
|
+
if (seg.length === 0) {
|
|
84
|
+
throw invalidFlatArgument(`Empty path segment in path: "${rawPath}"`, { path: rawPath, reason: "INVALID_SEGMENT" });
|
|
85
|
+
}
|
|
86
|
+
if (isForbiddenActionInputPropertyName(seg)) {
|
|
87
|
+
throw invalidFlatArgument(`Forbidden property "${seg}" in path: "${rawPath}"`, { path: rawPath, segment: seg, reason: "FORBIDDEN_PROPERTY" });
|
|
88
|
+
}
|
|
89
|
+
const segByteLength = Buffer.byteLength(seg, "utf8");
|
|
90
|
+
if (segByteLength > maxPropertyKeyLength) {
|
|
91
|
+
throw flatInputLimitExceeded(`Path segment length (${segByteLength} bytes) exceeds limit (${maxPropertyKeyLength} bytes) in path: "${rawPath}"`, { path: rawPath, segment: seg, length: segByteLength, maxPropertyKeyLength, reason: "MAX_PROPERTY_KEY_BYTES" });
|
|
92
|
+
}
|
|
93
|
+
if (ARRAY_INDEX_REGEX.test(seg)) {
|
|
94
|
+
const index = Number(seg);
|
|
95
|
+
if (index > maxArrayIndex) {
|
|
96
|
+
throw flatInputLimitExceeded(`Array index (${index}) exceeds limit (${maxArrayIndex}) in path: "${rawPath}"`, { path: rawPath, segment: seg, index, maxArrayIndex, reason: "MAX_ARRAY_INDEX" });
|
|
97
|
+
}
|
|
98
|
+
path.push(index);
|
|
99
|
+
}
|
|
100
|
+
else if (isFlatPathPropertyName(seg)) {
|
|
101
|
+
path.push(seg);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw invalidFlatArgument(`Invalid path segment "${seg}" in path: "${rawPath}"`, { path: rawPath, segment: seg, reason: "INVALID_SEGMENT" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (rawValueBytes > maxRawValueLength) {
|
|
108
|
+
throw flatInputLimitExceeded(`Raw value length (${rawValueBytes} bytes) exceeds limit (${maxRawValueLength} bytes) for path "${rawPath}"`, { path: rawPath, operator, valueLength: rawValueBytes, maxRawValueLength, reason: "MAX_RAW_VALUE_BYTES" });
|
|
109
|
+
}
|
|
110
|
+
let value;
|
|
111
|
+
if (operator === "=") {
|
|
112
|
+
value = rawValue;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (rawValueBytes > maxJsonLiteralLength) {
|
|
116
|
+
throw flatInputLimitExceeded(`JSON literal length (${rawValueBytes} bytes) exceeds limit (${maxJsonLiteralLength} bytes) for path "${rawPath}"`, { path: rawPath, operator: ":=", valueLength: rawValueBytes, maxJsonLiteralLength, reason: "MAX_JSON_LITERAL_BYTES" });
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(rawValue);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw invalidJsonLiteral(`Failed to parse JSON literal for path "${rawPath}" (length: ${rawValueBytes} bytes)`, { path: rawPath, operator: ":=", valueLength: rawValueBytes, reason: "SYNTAX_ERROR" });
|
|
124
|
+
}
|
|
125
|
+
const check = validateJsonValue(parsed);
|
|
126
|
+
if (!check.valid) {
|
|
127
|
+
throw invalidJsonLiteral(`Invalid JSON literal for path "${rawPath}": ${check.reason}`, {
|
|
128
|
+
path: rawPath,
|
|
129
|
+
operator: ":=",
|
|
130
|
+
valueLength: rawValueBytes,
|
|
131
|
+
reason: check.code === "NON_FINITE_NUMBER"
|
|
132
|
+
? "NON_FINITE_NUMBER"
|
|
133
|
+
: check.code === "MAX_JSON_DEPTH"
|
|
134
|
+
? "MAX_JSON_DEPTH"
|
|
135
|
+
: "INVALID_JSON_VALUE",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
value = parsed;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
raw: token,
|
|
142
|
+
path,
|
|
143
|
+
operator,
|
|
144
|
+
rawValue,
|
|
145
|
+
value,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 解析扁平赋值表达式列表。
|
|
150
|
+
*
|
|
151
|
+
* @param tokens 待解析的表达式字符串列表
|
|
152
|
+
* @param options 解析安全阈值配置选项
|
|
153
|
+
* @returns 解析后的结构化赋值列表
|
|
154
|
+
*/
|
|
155
|
+
export function parseFlatAssignments(tokens, options) {
|
|
156
|
+
const maxAssignments = options?.maxAssignments ?? DEFAULT_MAX_ASSIGNMENTS;
|
|
157
|
+
if (tokens.length > maxAssignments) {
|
|
158
|
+
throw flatInputLimitExceeded(`Total assignments (${tokens.length}) exceeds limit (${maxAssignments})`, { count: tokens.length, maxAssignments, reason: "MAX_ASSIGNMENTS" });
|
|
159
|
+
}
|
|
160
|
+
const maxTotalRawBytes = options?.maxTotalRawBytes ?? DEFAULT_MAX_TOTAL_RAW_BYTES;
|
|
161
|
+
let totalRawBytes = 0;
|
|
162
|
+
for (const token of tokens) {
|
|
163
|
+
totalRawBytes += Buffer.byteLength(token, "utf8");
|
|
164
|
+
if (totalRawBytes > maxTotalRawBytes) {
|
|
165
|
+
throw flatInputLimitExceeded(`Total raw input bytes (${totalRawBytes}) exceeds limit (${maxTotalRawBytes})`, { totalBytes: totalRawBytes, maxTotalRawBytes, reason: "MAX_TOTAL_RAW_BYTES" });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return tokens.map((token) => parseSingleFlatAssignment(token, options));
|
|
169
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 属性名与路径谓词定义。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 提供扁平入参路径属性名与全局禁止属性名的校验谓词。
|
|
6
|
+
* - 作为输入解析器与编码顾问共享的属性安全判定单一事实源。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 全局禁止的 Action 输入属性名只读列表。
|
|
10
|
+
*/
|
|
11
|
+
export declare const FORBIDDEN_ACTION_INPUT_PROPERTIES: readonly ["__proto__", "constructor", "prototype"];
|
|
12
|
+
/**
|
|
13
|
+
* 校验属性名是否符合扁平路径段命名规范。
|
|
14
|
+
* 规范:必须以英文字母或下划线开头,仅包含英文字母、数字、下划线与中划线。
|
|
15
|
+
*
|
|
16
|
+
* @param key 待校验的属性名
|
|
17
|
+
* @returns 是否符合扁平路径段规范
|
|
18
|
+
*/
|
|
19
|
+
export declare function isFlatPathPropertyName(key: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 判定属性名是否属于全局禁止的 Action 输入危险属性名。
|
|
22
|
+
* 禁止属性:__proto__、constructor、prototype。
|
|
23
|
+
*
|
|
24
|
+
* @param key 待判定的属性名
|
|
25
|
+
* @returns 是否属于禁止属性
|
|
26
|
+
*/
|
|
27
|
+
export declare function isForbiddenActionInputPropertyName(key: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 判定属性名是否既符合扁平路径段规范且不属于禁止属性。
|
|
30
|
+
*
|
|
31
|
+
* @param key 待判定的属性名
|
|
32
|
+
* @returns 是否为扁平安全的属性名
|
|
33
|
+
*/
|
|
34
|
+
export declare function isFlatSafePropertyName(key: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 属性名与路径谓词定义。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 提供扁平入参路径属性名与全局禁止属性名的校验谓词。
|
|
6
|
+
* - 作为输入解析器与编码顾问共享的属性安全判定单一事实源。
|
|
7
|
+
*/
|
|
8
|
+
const FLAT_PATH_PROPERTY_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
9
|
+
const FORBIDDEN_PROPERTY_SET = new Set([
|
|
10
|
+
"__proto__",
|
|
11
|
+
"constructor",
|
|
12
|
+
"prototype",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* 全局禁止的 Action 输入属性名只读列表。
|
|
16
|
+
*/
|
|
17
|
+
export const FORBIDDEN_ACTION_INPUT_PROPERTIES = Object.freeze([
|
|
18
|
+
"__proto__",
|
|
19
|
+
"constructor",
|
|
20
|
+
"prototype",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* 校验属性名是否符合扁平路径段命名规范。
|
|
24
|
+
* 规范:必须以英文字母或下划线开头,仅包含英文字母、数字、下划线与中划线。
|
|
25
|
+
*
|
|
26
|
+
* @param key 待校验的属性名
|
|
27
|
+
* @returns 是否符合扁平路径段规范
|
|
28
|
+
*/
|
|
29
|
+
export function isFlatPathPropertyName(key) {
|
|
30
|
+
return FLAT_PATH_PROPERTY_NAME_REGEX.test(key);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 判定属性名是否属于全局禁止的 Action 输入危险属性名。
|
|
34
|
+
* 禁止属性:__proto__、constructor、prototype。
|
|
35
|
+
*
|
|
36
|
+
* @param key 待判定的属性名
|
|
37
|
+
* @returns 是否属于禁止属性
|
|
38
|
+
*/
|
|
39
|
+
export function isForbiddenActionInputPropertyName(key) {
|
|
40
|
+
return FORBIDDEN_PROPERTY_SET.has(key);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 判定属性名是否既符合扁平路径段规范且不属于禁止属性。
|
|
44
|
+
*
|
|
45
|
+
* @param key 待判定的属性名
|
|
46
|
+
* @returns 是否为扁平安全的属性名
|
|
47
|
+
*/
|
|
48
|
+
export function isFlatSafePropertyName(key) {
|
|
49
|
+
return isFlatPathPropertyName(key) && !isForbiddenActionInputPropertyName(key);
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./flat-errors.js";
|
|
2
|
+
export * from "./flat-predicates.js";
|
|
3
|
+
export * from "./validation-mapper.js";
|
|
4
|
+
export * from "./flat-parser.js";
|
|
5
|
+
export * from "./flat-materializer.js";
|
|
6
|
+
export * from "./flat-decode.js";
|
|
7
|
+
export * from "./json-depth-scanner.js";
|
|
8
|
+
export * from "./utf8.js";
|
|
9
|
+
export * from "./file-input.js";
|
|
10
|
+
export * from "./stdin-input.js";
|
|
11
|
+
export * from "./input-resolver.js";
|
|
12
|
+
export * from "./advice.js";
|
|
13
|
+
export * from "./metadata.js";
|
|
14
|
+
export * from "./describe.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./flat-errors.js";
|
|
2
|
+
export * from "./flat-predicates.js";
|
|
3
|
+
export * from "./validation-mapper.js";
|
|
4
|
+
export * from "./flat-parser.js";
|
|
5
|
+
export * from "./flat-materializer.js";
|
|
6
|
+
export * from "./flat-decode.js";
|
|
7
|
+
export * from "./json-depth-scanner.js";
|
|
8
|
+
export * from "./utf8.js";
|
|
9
|
+
export * from "./file-input.js";
|
|
10
|
+
export * from "./stdin-input.js";
|
|
11
|
+
export * from "./input-resolver.js";
|
|
12
|
+
export * from "./advice.js";
|
|
13
|
+
export * from "./metadata.js";
|
|
14
|
+
export * from "./describe.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { JsonValue } from "@actiondock/sdk";
|
|
2
|
+
import type { FlatParserOptions } from "./flat-parser.js";
|
|
3
|
+
import type { FlatMaterializerOptions } from "./flat-materializer.js";
|
|
4
|
+
export { stripBom } from "./utf8.js";
|
|
5
|
+
/**
|
|
6
|
+
* 输入解析策略配置。
|
|
7
|
+
*/
|
|
8
|
+
export interface InputResolutionPolicy {
|
|
9
|
+
/** 最大允许输入字节数(默认 10MB) */
|
|
10
|
+
maxInputBytes?: number;
|
|
11
|
+
/** 最大允许 JSON 嵌套深度(默认 256) */
|
|
12
|
+
maxJsonDepth?: number;
|
|
13
|
+
/** 是否启用严格 UTF-8 校验(默认 true) */
|
|
14
|
+
strictUtf8?: boolean;
|
|
15
|
+
/** 是否对面向 Agent 的错误信息进行脱敏(默认 false) */
|
|
16
|
+
sanitizeInputErrors?: boolean;
|
|
17
|
+
/** 是否仅允许字节流输入(默认 false) */
|
|
18
|
+
byteStreamOnly?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从标准输入流中完整读取全部数据并转换为 UTF-8 字符串(保持历史接口兼容)。
|
|
22
|
+
*
|
|
23
|
+
* @param stream 标准输入可读流,默认为 process.stdin
|
|
24
|
+
*/
|
|
25
|
+
export declare function readStdin(stream?: NodeJS.ReadableStream): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Action 执行输入解析选项。
|
|
28
|
+
*/
|
|
29
|
+
export interface ResolveActionInputOptions {
|
|
30
|
+
input?: string;
|
|
31
|
+
inputFile?: string;
|
|
32
|
+
flatArgs?: string[];
|
|
33
|
+
stdin?: NodeJS.ReadableStream;
|
|
34
|
+
flatOptions?: FlatParserOptions & FlatMaterializerOptions;
|
|
35
|
+
policy?: InputResolutionPolicy;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 解析完整 JSON 文档字符串。
|
|
40
|
+
* 若解析失败抛出 INVALID_JSON 异常。
|
|
41
|
+
*
|
|
42
|
+
* @param text 待解析文本
|
|
43
|
+
* @param source 输入源描述(如 --input、文件路径或 stdin)
|
|
44
|
+
* @param policy 解析策略选项
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseJson(text: string, source: string, policy?: InputResolutionPolicy): JsonValue;
|
|
47
|
+
/**
|
|
48
|
+
* 统一解析 Action 执行入参。
|
|
49
|
+
*
|
|
50
|
+
* 输入模式仲裁规则(Input Mode Arbitration):
|
|
51
|
+
* - effectiveFlat: flatArgs 存在且 length > 0
|
|
52
|
+
* - hasInlineJson: options.input !== undefined(即使为 "")
|
|
53
|
+
* - hasFileJson: options.inputFile !== undefined(包含 "-")
|
|
54
|
+
* - 最多一个为 true;若超过一个,抛出 INPUT_CONFLICT(reason: "MULTIPLE_INPUT_MODES")。
|
|
55
|
+
* - 若三个均为 false,返回默认 {}。
|
|
56
|
+
* - flatArgs = [] 视为未指定。
|
|
57
|
+
* - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
|
|
58
|
+
* - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
|
|
59
|
+
*
|
|
60
|
+
* @param options 输入解析选项
|
|
61
|
+
* @returns 解析后的 JSON 对象
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveActionInput(options: ResolveActionInputOptions): Promise<JsonValue>;
|