@actiondock/core 2.0.11 → 2.0.12

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.
Files changed (119) hide show
  1. package/dist/build/templates.d.ts +19 -0
  2. package/dist/catalog/action-index.d.ts +9 -0
  3. package/dist/catalog/action-resolver.d.ts +20 -0
  4. package/dist/catalog/location-registry.d.ts +15 -0
  5. package/dist/catalog/package-catalog.d.ts +7 -0
  6. package/dist/catalog/types.d.ts +68 -0
  7. package/dist/doctor/doctor.d.ts +6 -0
  8. package/dist/doctor/types.d.ts +23 -0
  9. package/dist/execution/index.d.ts +2 -0
  10. package/dist/execution/service.d.ts +74 -0
  11. package/dist/execution/types.d.ts +74 -0
  12. package/dist/export/templates.d.ts +33 -0
  13. package/dist/filter/intent.d.ts +33 -0
  14. package/{src/index.ts → dist/index.d.ts} +1 -1
  15. package/dist/index.js +8458 -0
  16. package/dist/profile/client.d.ts +116 -0
  17. package/dist/profile/manager.d.ts +47 -0
  18. package/dist/profile/types.d.ts +61 -0
  19. package/dist/project/closure.d.ts +25 -0
  20. package/{src/project/index.ts → dist/project/index.d.ts} +1 -0
  21. package/dist/project/init.d.ts +27 -0
  22. package/dist/project/loader.d.ts +105 -0
  23. package/dist/project/manifest.d.ts +44 -0
  24. package/dist/project/types.d.ts +131 -0
  25. package/dist/registry/registry.d.ts +24 -0
  26. package/dist/registry/types.d.ts +117 -0
  27. package/dist/runtime/clock.d.ts +21 -0
  28. package/dist/runtime/context.d.ts +75 -0
  29. package/dist/runtime/env.d.ts +54 -0
  30. package/dist/runtime/events.d.ts +61 -0
  31. package/dist/runtime/execution-manager.d.ts +48 -0
  32. package/{src/runtime/index.ts → dist/runtime/index.d.ts} +0 -1
  33. package/dist/runtime/module-loader.d.ts +42 -0
  34. package/dist/runtime/process.d.ts +11 -0
  35. package/dist/runtime/runner.d.ts +186 -0
  36. package/dist/runtime/standalone.d.ts +40 -0
  37. package/dist/schema/validator.d.ts +28 -0
  38. package/dist/server/body.d.ts +40 -0
  39. package/{src/server/index.ts → dist/server/index.d.ts} +0 -1
  40. package/dist/server/routes/actions.d.ts +5 -0
  41. package/dist/server/routes/common.d.ts +36 -0
  42. package/dist/server/routes/config.d.ts +5 -0
  43. package/dist/server/routes/doctor.d.ts +5 -0
  44. package/dist/server/routes/health.d.ts +5 -0
  45. package/dist/server/routes/info.d.ts +5 -0
  46. package/dist/server/routes/playbooks.d.ts +5 -0
  47. package/dist/server/routes/runs.d.ts +5 -0
  48. package/dist/server/routes/state.d.ts +5 -0
  49. package/dist/server/runtime-registry.d.ts +78 -0
  50. package/dist/server/security.d.ts +37 -0
  51. package/dist/server/server.d.ts +15 -0
  52. package/dist/server/types.d.ts +59 -0
  53. package/dist/storage/driver.d.ts +15 -0
  54. package/dist/storage/index.d.ts +44 -0
  55. package/{src/storage/mask.ts → dist/storage/mask.d.ts} +5 -20
  56. package/dist/storage/sqlite.d.ts +66 -0
  57. package/dist/storage/types.d.ts +121 -0
  58. package/dist/utils/index.d.ts +29 -0
  59. package/dist/version.d.ts +4 -0
  60. package/package.json +11 -10
  61. package/src/build/templates.ts +0 -59
  62. package/src/catalog/action-index.ts +0 -92
  63. package/src/catalog/action-resolver.ts +0 -120
  64. package/src/catalog/location-registry.ts +0 -134
  65. package/src/catalog/package-catalog.ts +0 -87
  66. package/src/catalog/types.ts +0 -75
  67. package/src/doctor/doctor.ts +0 -419
  68. package/src/doctor/types.ts +0 -25
  69. package/src/execution/index.ts +0 -2
  70. package/src/execution/service.ts +0 -322
  71. package/src/execution/types.ts +0 -73
  72. package/src/export/templates.ts +0 -590
  73. package/src/filter/intent.ts +0 -154
  74. package/src/profile/client.ts +0 -552
  75. package/src/profile/manager.ts +0 -341
  76. package/src/profile/types.ts +0 -71
  77. package/src/project/init.ts +0 -242
  78. package/src/project/loader.ts +0 -479
  79. package/src/project/manifest.ts +0 -299
  80. package/src/project/types.ts +0 -141
  81. package/src/registry/registry.ts +0 -940
  82. package/src/registry/types.ts +0 -127
  83. package/src/runtime/clock.ts +0 -41
  84. package/src/runtime/context.ts +0 -259
  85. package/src/runtime/env.ts +0 -172
  86. package/src/runtime/events.ts +0 -142
  87. package/src/runtime/execution-manager.ts +0 -74
  88. package/src/runtime/module-loader.ts +0 -67
  89. package/src/runtime/process.ts +0 -245
  90. package/src/runtime/runner.ts +0 -527
  91. package/src/runtime/standalone.ts +0 -429
  92. package/src/schema/validator.ts +0 -61
  93. package/src/server/body.ts +0 -112
  94. package/src/server/routes/actions.ts +0 -262
  95. package/src/server/routes/common.ts +0 -126
  96. package/src/server/routes/config.ts +0 -147
  97. package/src/server/routes/doctor.ts +0 -29
  98. package/src/server/routes/health.ts +0 -40
  99. package/src/server/routes/info.ts +0 -221
  100. package/src/server/routes/playbooks.ts +0 -109
  101. package/src/server/routes/runs.ts +0 -301
  102. package/src/server/routes/state.ts +0 -133
  103. package/src/server/runtime-registry.ts +0 -115
  104. package/src/server/security.ts +0 -115
  105. package/src/server/server.ts +0 -251
  106. package/src/server/types.ts +0 -57
  107. package/src/storage/driver.ts +0 -126
  108. package/src/storage/index.ts +0 -81
  109. package/src/storage/sqlite.ts +0 -714
  110. package/src/storage/types.ts +0 -139
  111. package/src/utils/index.ts +0 -62
  112. /package/{src/build/index.ts → dist/build/index.d.ts} +0 -0
  113. /package/{src/catalog/index.ts → dist/catalog/index.d.ts} +0 -0
  114. /package/{src/doctor/index.ts → dist/doctor/index.d.ts} +0 -0
  115. /package/{src/export/index.ts → dist/export/index.d.ts} +0 -0
  116. /package/{src/filter/index.ts → dist/filter/index.d.ts} +0 -0
  117. /package/{src/profile/index.ts → dist/profile/index.d.ts} +0 -0
  118. /package/{src/registry/index.ts → dist/registry/index.d.ts} +0 -0
  119. /package/{src/server/routes/index.ts → dist/server/routes/index.d.ts} +0 -0
@@ -1,429 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import type { ActionDefinition } from "@actiondock/sdk";
3
- import { filterWithFallbackInfo } from "../filter";
4
- import type { ConfigItemDefinition } from "../project/types";
5
- import { createStorage } from "../storage";
6
- import { parseDuration } from "../utils";
7
- import { ActionRunner } from "./runner";
8
-
9
- /**
10
- * 独立编译可执行文件运行时的初始化选项。
11
- */
12
- export interface StandaloneRuntimeOptions {
13
- /** 所属 Package ID */
14
- packageId: string;
15
- /** 版本号 */
16
- version: string;
17
- /** 描述信息 */
18
- description?: string;
19
- /** 声明的配置依赖定义 */
20
- config?: Record<string, ConfigItemDefinition>;
21
- /** 打包内置的 Action 动作定义列表 */
22
- actions: ActionDefinition[];
23
- }
24
-
25
- /**
26
- * 独立二进制可执行文件运行时(Standalone Runtime)。
27
- *
28
- * 职责:
29
- * 1. 作为由 `ad build` 编译生成的单文件独立可执行文件(Standalone Binary)的运行时入口。
30
- * 2. 保证与开发态(`ad run` / `ad action`)在输出信封、配置优先级、状态存储等方面的 100% 行为一致性。
31
- * 3. 自带轻量 CLI 分发器,支持 `list`, `describe`, `run`, `config`, `state`, `version`, `help` 子命令。
32
- */
33
- export class StandaloneRuntime {
34
- private packageId: string;
35
- private version: string;
36
- private description?: string;
37
- private configDefs?: Record<string, ConfigItemDefinition>;
38
- private actionsMap: Map<string, ActionDefinition>;
39
-
40
- constructor(options: StandaloneRuntimeOptions) {
41
- this.packageId = options.packageId;
42
- this.version = options.version;
43
- this.description = options.description;
44
- this.configDefs = options.config;
45
- this.actionsMap = new Map(options.actions.map((a) => [a.id, a]));
46
- }
47
-
48
- /**
49
- * 解析命令行参数并执行对应的独立二进制子命令。
50
- *
51
- * @param argv 命令行参数数组(通常为 process.argv.slice(2))
52
- */
53
- async run(argv: string[]): Promise<void> {
54
- const args = [...argv];
55
- let dataDir: string | undefined;
56
- const configOverrides: Record<string, unknown> = {};
57
-
58
- // 提取全局参数(--data-dir 与 --config)
59
- const filteredArgs: string[] = [];
60
- for (let i = 0; i < args.length; i++) {
61
- const arg = args[i];
62
- if (arg === "--data-dir" && i + 1 < args.length) {
63
- dataDir = args[++i];
64
- } else if (arg.startsWith("--data-dir=")) {
65
- dataDir = arg.split("=")[1];
66
- } else if (arg === "--config" && i + 1 < args.length) {
67
- const pair = args[++i];
68
- const [k, ...v] = pair.split("=");
69
- if (k) configOverrides[k] = v.join("=");
70
- } else if (arg.startsWith("--config=")) {
71
- const pair = arg.slice(9);
72
- const [k, ...v] = pair.split("=");
73
- if (k) configOverrides[k] = v.join("=");
74
- } else {
75
- filteredArgs.push(arg);
76
- }
77
- }
78
-
79
- const command = filteredArgs[0] || "help";
80
- const subArgs = filteredArgs.slice(1);
81
-
82
- const storage = createStorage(this.packageId, { dataDir });
83
- const runner = new ActionRunner({
84
- packageId: this.packageId,
85
- storage,
86
- configOverrides,
87
- projectConfig: {
88
- id: this.packageId,
89
- name: this.packageId,
90
- version: this.version,
91
- description: this.description,
92
- config: this.configDefs,
93
- },
94
- actions: this.actionsMap,
95
- });
96
-
97
- try {
98
- switch (command) {
99
- case "list": {
100
- const json = subArgs.includes("--json");
101
- const noFallback = subArgs.includes("--no-fallback");
102
- let intent: string | undefined;
103
- const positionalPatterns: string[] = [];
104
-
105
- for (let i = 0; i < subArgs.length; i++) {
106
- const arg = subArgs[i];
107
- if (arg === "--intent" || arg === "-i") {
108
- if (i + 1 < subArgs.length) intent = subArgs[++i];
109
- } else if (arg.startsWith("--intent=")) {
110
- intent = arg.slice(9);
111
- } else if (arg.startsWith("-i=")) {
112
- intent = arg.slice(3);
113
- } else if (!arg.startsWith("-")) {
114
- positionalPatterns.push(arg);
115
- }
116
- }
117
-
118
- const effectiveIntent =
119
- intent ||
120
- (positionalPatterns.length > 0
121
- ? positionalPatterns.join("|")
122
- : undefined);
123
-
124
- const list = runner.listActions().map((a) => ({
125
- id: a.id,
126
- description: a.description || "",
127
- }));
128
-
129
- const filterRes = filterWithFallbackInfo(
130
- list,
131
- effectiveIntent,
132
- [(a) => a.id, (a) => a.description],
133
- !noFallback
134
- );
135
-
136
- if (json) {
137
- console.log(JSON.stringify(filterRes.items, null, 2));
138
- } else {
139
- console.log(`Actions in ${this.packageId} (v${this.version}):\n`);
140
- for (const a of filterRes.items) {
141
- console.log(` ${a.id.padEnd(28)} ${a.description}`);
142
- }
143
- }
144
- break;
145
- }
146
-
147
-
148
- case "describe":
149
- case "show": {
150
- const id = subArgs.find((a) => !a.startsWith("-"));
151
- const json = subArgs.includes("--json");
152
- if (!id) {
153
- console.error("Error: Action ID is required for describe");
154
- process.exit(1);
155
- }
156
- const action = runner.getAction(id);
157
- if (!action) {
158
- console.error(`Error: Action '${id}' not found`);
159
- process.exit(1);
160
- }
161
- if (json) {
162
- console.log(
163
- JSON.stringify(
164
- {
165
- id: action.id,
166
- description: action.description,
167
- inputSchema: action.inputSchema,
168
- outputSchema: action.outputSchema,
169
- },
170
- null,
171
- 2
172
- )
173
- );
174
- } else {
175
- console.log(`Action: ${action.id}`);
176
- if (action.description) console.log(`Description: ${action.description}`);
177
- if (action.inputSchema) {
178
- console.log("\nInput Schema:");
179
- console.log(JSON.stringify(action.inputSchema, null, 2));
180
- }
181
- if (action.outputSchema) {
182
- console.log("\nOutput Schema:");
183
- console.log(JSON.stringify(action.outputSchema, null, 2));
184
- }
185
- }
186
- break;
187
- }
188
-
189
- case "run": {
190
- const id = subArgs.find((a) => !a.startsWith("-"));
191
- if (!id) {
192
- console.error("Error: Action ID is required for run");
193
- process.exit(1);
194
- }
195
-
196
- let input: unknown = {};
197
- let timeoutMs: number | undefined;
198
-
199
- for (let i = 0; i < subArgs.length; i++) {
200
- if (subArgs[i] === "--async") {
201
- console.error(
202
- "Error: Async execution is not supported in standalone single-execution binaries."
203
- );
204
- process.exit(1);
205
- } else if (subArgs[i] === "--timeout" && i + 1 < subArgs.length) {
206
- const raw = subArgs[++i];
207
- timeoutMs = parseDuration(raw);
208
- } else if (subArgs[i].startsWith("--timeout=")) {
209
- timeoutMs = parseDuration(subArgs[i].slice(10));
210
- } else if (subArgs[i] === "--input" && i + 1 < subArgs.length) {
211
- try {
212
- input = JSON.parse(subArgs[++i]);
213
- } catch (e: any) {
214
- console.error(`Error parsing --input JSON: ${e.message}`);
215
- process.exit(1);
216
- }
217
- } else if (subArgs[i].startsWith("--input=")) {
218
- try {
219
- input = JSON.parse(subArgs[i].slice(8));
220
- } catch (e: any) {
221
- console.error(`Error parsing --input JSON: ${e.message}`);
222
- process.exit(1);
223
- }
224
- } else if (subArgs[i] === "--input-file" && i + 1 < subArgs.length) {
225
- try {
226
- input = JSON.parse(readFileSync(subArgs[++i], "utf-8"));
227
- } catch (e: any) {
228
- console.error(`Error reading --input-file: ${e.message}`);
229
- process.exit(1);
230
- }
231
- }
232
- }
233
-
234
- const controller = new AbortController();
235
- const sigintHandler = () => {
236
- controller.abort(new Error("Interrupted by SIGINT"));
237
- };
238
- process.once("SIGINT", sigintHandler);
239
-
240
- try {
241
- const result = await runner.execute(id, input, {
242
- signal: controller.signal,
243
- timeoutMs,
244
- });
245
- // Standard stdout JSON envelope
246
- console.log(JSON.stringify(result, null, 2));
247
- if (!result.ok) {
248
- process.exit(1);
249
- }
250
- } finally {
251
- process.removeListener("SIGINT", sigintHandler);
252
- }
253
- break;
254
- }
255
-
256
- case "config": {
257
- const sub = subArgs[0] || "list";
258
- if (sub === "list") {
259
- const all = storage.listConfig();
260
- console.log(JSON.stringify(all, null, 2));
261
- } else if (sub === "get") {
262
- const key = subArgs[1];
263
- if (!key) {
264
- console.error("Error: config key required");
265
- process.exit(1);
266
- }
267
- const val = storage.getConfig(key);
268
- console.log(val !== undefined ? JSON.stringify(val) : "undefined");
269
- } else if (sub === "set") {
270
- const key = subArgs[1];
271
- const rawVal = subArgs[2];
272
- if (!key || rawVal === undefined) {
273
- console.error("Error: key and value required");
274
- process.exit(1);
275
- }
276
- let parsed: unknown = rawVal;
277
- try {
278
- parsed = JSON.parse(rawVal);
279
- } catch {
280
- parsed = rawVal;
281
- }
282
- storage.setConfig(key, parsed);
283
- console.log(`Config '${key}' updated`);
284
- } else if (sub === "delete") {
285
- const key = subArgs[1];
286
- if (!key) {
287
- console.error("Error: config key required");
288
- process.exit(1);
289
- }
290
- storage.deleteConfig(key);
291
- console.log(`Config '${key}' deleted`);
292
- }
293
- break;
294
- }
295
-
296
- case "state": {
297
- const sub = subArgs[0] || "list";
298
- let namespace: string | undefined;
299
- let isAll = false;
300
- let isJson = false;
301
-
302
- for (let i = 1; i < subArgs.length; i++) {
303
- if ((subArgs[i] === "-n" || subArgs[i] === "--namespace") && i + 1 < subArgs.length) {
304
- namespace = subArgs[++i];
305
- } else if (subArgs[i].startsWith("--namespace=")) {
306
- namespace = subArgs[i].slice(12);
307
- } else if (subArgs[i] === "-a" || subArgs[i] === "--all") {
308
- isAll = true;
309
- } else if (subArgs[i] === "--json") {
310
- isJson = true;
311
- }
312
- }
313
-
314
- if (sub === "list") {
315
- const prefix = subArgs[1] && !subArgs[1].startsWith("-") ? subArgs[1] : "";
316
- const keys = await storage.listStateKeys(namespace !== undefined ? namespace : null, prefix);
317
- console.log(JSON.stringify(keys, null, 2));
318
- } else if (sub === "get") {
319
- const key = subArgs[1] && !subArgs[1].startsWith("-") ? subArgs[1] : subArgs[2];
320
- if (!key) {
321
- console.error("Error: state key required");
322
- process.exit(1);
323
- }
324
- let val: unknown;
325
- if (namespace !== undefined) {
326
- val = await storage.getState(namespace, key);
327
- } else {
328
- const entry = await storage.findState(key);
329
- val = entry?.value;
330
- }
331
- if (isJson) {
332
- console.log(JSON.stringify({ key, value: val }, null, 2));
333
- } else {
334
- console.log(val !== undefined ? JSON.stringify(val) : "undefined");
335
- }
336
- } else if (sub === "set") {
337
- const key = subArgs[1];
338
- const rawVal = subArgs[2];
339
- if (!key || rawVal === undefined) {
340
- console.error("Error: key and value required");
341
- process.exit(1);
342
- }
343
-
344
- let ns = namespace || "";
345
- let actualKey = key;
346
- if (namespace === undefined && key.includes(":")) {
347
- const colonIdx = key.indexOf(":");
348
- ns = key.slice(0, colonIdx);
349
- actualKey = key.slice(colonIdx + 1);
350
- }
351
-
352
- let parsed: unknown = rawVal;
353
- try {
354
- parsed = JSON.parse(rawVal);
355
- } catch {
356
- parsed = rawVal;
357
- }
358
-
359
- let ttl: number | undefined;
360
- for (let i = 3; i < subArgs.length; i++) {
361
- if (subArgs[i] === "--ttl" && i + 1 < subArgs.length) {
362
- ttl = parseInt(subArgs[++i], 10);
363
- } else if (subArgs[i].startsWith("--ttl=")) {
364
- ttl = parseInt(subArgs[i].slice(6), 10);
365
- }
366
- }
367
-
368
- await storage.setState(ns, actualKey, parsed, ttl);
369
- const displayKey = ns ? `${ns}:${actualKey}` : actualKey;
370
- console.log(`State '${displayKey}' updated`);
371
- } else if (sub === "delete" || sub === "rm") {
372
- const key = subArgs[1] && !subArgs[1].startsWith("-") ? subArgs[1] : subArgs[2];
373
- if (!key) {
374
- console.error("Error: state key required");
375
- process.exit(1);
376
- }
377
- const deleted = await storage.deleteStateSmart(key, namespace);
378
- if (deleted) {
379
- console.log(`State '${key}' deleted`);
380
- } else {
381
- console.error(`Error: State key '${key}' not found`);
382
- process.exit(1);
383
- }
384
- } else if (sub === "clear" || sub === "clean") {
385
- const prefix = subArgs[1] && !subArgs[1].startsWith("-") ? subArgs[1] : "";
386
- const count = await storage.clearState({
387
- namespace,
388
- all: isAll,
389
- prefix: prefix || undefined,
390
- });
391
- console.log(`Cleared ${count} state entry(s)`);
392
- }
393
- break;
394
- }
395
-
396
- case "version":
397
- case "-v":
398
- case "--version": {
399
- console.log(`${this.packageId} v${this.version}`);
400
- break;
401
- }
402
-
403
- case "help":
404
- default: {
405
- console.log(`${this.packageId} (v${this.version})`);
406
- if (this.description) console.log(`${this.description}\n`);
407
- console.log("Usage:");
408
- console.log(" <cmd> list [--json] List available actions");
409
- console.log(" <cmd> describe <id> [--json] Show action details and schemas");
410
- console.log(" <cmd> run <id> [--input '<json>'] Execute action with JSON input");
411
- console.log(" <cmd> config list/get/set/delete Manage package configuration");
412
- console.log(" <cmd> state list/get/set/delete Manage shared state store");
413
- console.log("\nGlobal options:");
414
- console.log(" --data-dir <path> Custom runtime database directory");
415
- console.log(" --config <KEY=val> Temporary config override");
416
- break;
417
- }
418
- }
419
- } finally {
420
- storage.close();
421
- }
422
- }
423
- }
424
-
425
- export function createStandaloneRuntime(
426
- options: StandaloneRuntimeOptions
427
- ): StandaloneRuntime {
428
- return new StandaloneRuntime(options);
429
- }
@@ -1,61 +0,0 @@
1
- import Ajv from "ajv";
2
- import addFormats from "ajv-formats";
3
- import type { JsonSchema } from "@actiondock/sdk";
4
-
5
- // 初始化全局 Ajv 实例,预载所有标准 format 格式校验器
6
- const ajv = new Ajv({
7
- allErrors: true,
8
- strict: false,
9
- coerceTypes: false,
10
- });
11
- addFormats(ajv);
12
-
13
- /**
14
- * JSON Schema 校验结果对象。
15
- */
16
- export interface ValidationResult {
17
- /** 数据是否完全符合 Schema 规范 */
18
- valid: boolean;
19
- /** 校验失败时的具体错误详情列表 */
20
- errors?: string[];
21
- }
22
-
23
- /**
24
- * 校验指定数据是否符合给定的 JSON Schema 契约。
25
- *
26
- * @param schema 期望匹配的 JSON Schema 对象(未提供或为空对象时默认通过)
27
- * @param data 待校验的原始数据
28
- * @returns 包含 valid 状态与错误信息列表的 ValidationResult
29
- */
30
- export function validateSchema(
31
- schema: JsonSchema | undefined,
32
- data: unknown
33
- ): ValidationResult {
34
- if (!schema || typeof schema !== "object" || Object.keys(schema).length === 0) {
35
- return { valid: true };
36
- }
37
-
38
- try {
39
- const validate = ajv.compile(schema);
40
- const valid = validate(data);
41
-
42
- if (valid) {
43
- return { valid: true };
44
- }
45
-
46
- const errors = (validate.errors || []).map((err) => {
47
- const path = err.instancePath ? `at '${err.instancePath}' ` : "";
48
- return `${path}${err.message || "failed validation"}`.trim();
49
- });
50
-
51
- return {
52
- valid: false,
53
- errors: errors.length > 0 ? errors : ["Schema validation failed"],
54
- };
55
- } catch (err: any) {
56
- return {
57
- valid: false,
58
- errors: [`Schema compilation error: ${err.message || String(err)}`],
59
- };
60
- }
61
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * 默认单次 HTTP 请求体最大字节上限(1 MiB)。
3
- */
4
- export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
5
-
6
- /**
7
- * 请求体超出最大允许体积时抛出的异常(对应 HTTP 413 Payload Too Large)。
8
- */
9
- export class RequestTooLargeError extends Error {
10
- public code = "REQUEST_TOO_LARGE";
11
- constructor(message = "Request body exceeds maximum allowed size") {
12
- super(message);
13
- this.name = "RequestTooLargeError";
14
- }
15
- }
16
-
17
- /**
18
- * 请求体非合法 JSON 格式时抛出的异常(对应 HTTP 400 Bad Request)。
19
- */
20
- export class InvalidJsonError extends Error {
21
- public code = "INVALID_JSON";
22
- constructor(message = "Failed to parse request body as JSON") {
23
- super(message);
24
- this.name = "InvalidJsonError";
25
- }
26
- }
27
-
28
- /**
29
- * 请求体安全读取选项。
30
- */
31
- export interface ReadJsonBodyOptions {
32
- /** 最大允许的字节数(默认 1 MiB) */
33
- maxBytes?: number;
34
- }
35
-
36
- /**
37
- * 流式安全地读取 HTTP 请求体并解析为 JSON 对象。
38
- *
39
- * 防护机制:
40
- * 1. 快速拒绝(Fast-path):若请求头中存在 `Content-Length` 且超过 `maxBytes`,立即抛出 RequestTooLargeError,不进行内存分配。
41
- * 2. 流式计数器(Chunk Streaming):在读取 ReadableStream 过程中实时累计字节数,中途超出上限即刻截断并释放流锁,防止大文件 DoS 与内存耗尽(OOM)。
42
- * 3. 安全解码:处理空请求体与 UTF-8 字符集解码。
43
- *
44
- * @param req 传入的 Request 对象
45
- * @param options 读取配置选项
46
- * @returns 解析后的 JSON 对象
47
- * @throws {RequestTooLargeError} 若请求体体积超限
48
- * @throws {InvalidJsonError} 若请求体非合法 JSON
49
- */
50
- export async function readJsonBody<T = any>(
51
- req: Request,
52
- options: ReadJsonBodyOptions = {}
53
- ): Promise<T> {
54
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BODY_BYTES;
55
-
56
- // 1. 快速检查 Content-Length 头部
57
- const contentLengthHeader = req.headers.get("content-length");
58
- if (contentLengthHeader) {
59
- const parsedLength = parseInt(contentLengthHeader, 10);
60
- if (!isNaN(parsedLength) && parsedLength > maxBytes) {
61
- throw new RequestTooLargeError();
62
- }
63
- }
64
-
65
- // 2. 流式读取并实时统计字节数
66
- if (!req.body) {
67
- return {} as T;
68
- }
69
-
70
- const reader = req.body.getReader();
71
- const chunks: Uint8Array[] = [];
72
- let totalBytes = 0;
73
-
74
- try {
75
- while (true) {
76
- const { done, value } = await reader.read();
77
- if (done) break;
78
- if (value) {
79
- totalBytes += value.byteLength;
80
- if (totalBytes > maxBytes) {
81
- throw new RequestTooLargeError();
82
- }
83
- chunks.push(value);
84
- }
85
- }
86
- } finally {
87
- reader.releaseLock();
88
- }
89
-
90
- if (chunks.length === 0 || totalBytes === 0) {
91
- return {} as T;
92
- }
93
-
94
- // 3. 拼接字节数组并进行 UTF-8 解码
95
- const merged = new Uint8Array(totalBytes);
96
- let offset = 0;
97
- for (const chunk of chunks) {
98
- merged.set(chunk, offset);
99
- offset += chunk.byteLength;
100
- }
101
-
102
- const text = new TextDecoder("utf-8").decode(merged);
103
- if (!text.trim()) {
104
- return {} as T;
105
- }
106
-
107
- try {
108
- return JSON.parse(text) as T;
109
- } catch (err: any) {
110
- throw new InvalidJsonError(`Failed to parse request body: ${err.message}`);
111
- }
112
- }