@actiondock/core 2.19.0 → 2.21.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/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +25 -0
- package/dist/execution/service.js +44 -10
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +20 -1
- package/dist/host/host.js +430 -66
- package/dist/host/types.d.ts +13 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +33 -0
- package/dist/invocation/types.js +11 -1
- package/dist/package/index.d.ts +2 -2
- package/dist/package/runtime.js +48 -15
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +59 -1
- package/dist/process/command-executor.d.ts +35 -0
- package/dist/process/command-executor.js +326 -0
- package/dist/process/index.d.ts +1 -0
- package/dist/process/index.js +1 -0
- package/dist/process/process-driver.d.ts +19 -6
- package/dist/process/process-driver.js +223 -54
- package/dist/process/run-executor.js +101 -47
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +8 -2
- package/dist/project/index.js +8 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +19 -30
- package/dist/project/manifest.js +10 -0
- package/dist/project/package-manager.d.ts +40 -0
- package/dist/project/package-manager.js +224 -0
- package/dist/project/preparation.d.ts +8 -0
- package/dist/project/preparation.js +130 -0
- package/dist/project/transactions.d.ts +5 -1
- package/dist/project/transactions.js +35 -75
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +27 -5
- package/dist/runtime/context.js +98 -26
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/security/config-cipher.d.ts +54 -0
- package/dist/security/config-cipher.js +190 -0
- package/dist/security/config-key.d.ts +35 -0
- package/dist/security/config-key.js +469 -0
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +84 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +4 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +28 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/config-value.d.ts +33 -0
- package/dist/storage/config-value.js +102 -0
- package/dist/storage/file-lock.js +35 -21
- package/dist/storage/index.d.ts +24 -2
- package/dist/storage/index.js +65 -2
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/storage/migrations/index.d.ts +40 -0
- package/dist/storage/migrations/index.js +96 -0
- package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
- package/dist/storage/migrations/v2-to-v3.js +116 -0
- package/dist/storage/sqlite-config.d.ts +62 -3
- package/dist/storage/sqlite-config.js +163 -9
- package/dist/storage/sqlite-driver.d.ts +5 -2
- package/dist/storage/sqlite-driver.js +24 -3
- package/dist/storage/sqlite-schema.d.ts +20 -2
- package/dist/storage/sqlite-schema.js +129 -132
- package/dist/storage/sqlite.d.ts +7 -2
- package/dist/storage/sqlite.js +64 -13
- package/dist/storage/types.d.ts +72 -4
- package/dist/storage/types.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { decodeStateKey } from "@actiondock/sdk";
|
|
2
2
|
import { filterWithFallbackInfo } from "../utils/intent.js";
|
|
3
|
-
import { resolveExecutionHint } from "../errors.js";
|
|
4
|
-
import {
|
|
3
|
+
import { ActionDockError, INVALID_ARGUMENT, resolveExecutionHint } from "../errors.js";
|
|
4
|
+
import { resolveInput, resolveValueInput, formatActionDetail, buildActionDescribePayload, mapInputValidationFailure, } from "../input/index.js";
|
|
5
5
|
import { validateActionInputValue } from "../value-validator.js";
|
|
6
|
+
import { assertConcreteConfigKey } from "../project/config-declarations.js";
|
|
6
7
|
import { parseDuration } from "../utils/index.js";
|
|
7
8
|
import { ExitCode } from "./standalone.js";
|
|
8
9
|
export async function handleList(ctx, subArgs) {
|
|
@@ -148,7 +149,7 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
|
|
|
148
149
|
}
|
|
149
150
|
let input;
|
|
150
151
|
try {
|
|
151
|
-
input = await
|
|
152
|
+
input = await resolveInput({
|
|
152
153
|
input: inputStr,
|
|
153
154
|
inputFile,
|
|
154
155
|
stdinField,
|
|
@@ -273,7 +274,57 @@ function isServiceCapabilityError(err) {
|
|
|
273
274
|
return (code === "CAPABILITY_UNAVAILABLE" ||
|
|
274
275
|
code === "TARGET_CAPABILITY_UNAVAILABLE");
|
|
275
276
|
}
|
|
276
|
-
|
|
277
|
+
function parseSetInput(subArgs, isState) {
|
|
278
|
+
const positional = [];
|
|
279
|
+
const input = {};
|
|
280
|
+
let namespace;
|
|
281
|
+
let ttl;
|
|
282
|
+
for (let i = 1; i < subArgs.length; i++) {
|
|
283
|
+
const arg = subArgs[i];
|
|
284
|
+
if (arg === "--json")
|
|
285
|
+
continue;
|
|
286
|
+
if (arg === "--stdin") {
|
|
287
|
+
if (input.fromStdin)
|
|
288
|
+
throw new ActionDockError(INVALID_ARGUMENT, "Duplicate --stdin option");
|
|
289
|
+
input.fromStdin = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const equals = arg.indexOf("=");
|
|
293
|
+
const option = equals < 0 ? arg : arg.slice(0, equals);
|
|
294
|
+
if (option === "--stdin-field" || (isState && ["-n", "--namespace", "--ttl"].includes(option))) {
|
|
295
|
+
const value = equals < 0 ? subArgs[++i] : arg.slice(equals + 1);
|
|
296
|
+
if (value === undefined) {
|
|
297
|
+
throw new ActionDockError(INVALID_ARGUMENT, `Option '${option}' requires a value`);
|
|
298
|
+
}
|
|
299
|
+
if (option === "--stdin-field") {
|
|
300
|
+
if (input.stdinField !== undefined) {
|
|
301
|
+
throw new ActionDockError(INVALID_ARGUMENT, "Duplicate --stdin-field option");
|
|
302
|
+
}
|
|
303
|
+
input.stdinField = value;
|
|
304
|
+
}
|
|
305
|
+
else if (option === "--ttl") {
|
|
306
|
+
ttl = parseInt(value, 10);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
namespace = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (arg.startsWith("-") && !/^-\d/.test(arg)) {
|
|
313
|
+
throw new ActionDockError(INVALID_ARGUMENT, "Unknown set option");
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
positional.push(arg);
|
|
317
|
+
if (positional.length > 2) {
|
|
318
|
+
throw new ActionDockError(INVALID_ARGUMENT, "Only one complete positional value is allowed");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!positional[0])
|
|
323
|
+
throw new ActionDockError(INVALID_ARGUMENT, "Key is required");
|
|
324
|
+
input.value = positional[1];
|
|
325
|
+
return { key: positional[0], input, namespace, ttl };
|
|
326
|
+
}
|
|
327
|
+
export async function handleConfig(ctx, subArgs, flatArgs, signal, stdin) {
|
|
277
328
|
const sub = subArgs[0] || "list";
|
|
278
329
|
if (!ctx.service.management) {
|
|
279
330
|
ctx.writeErr("Error: management port is not enabled on this service");
|
|
@@ -281,10 +332,15 @@ export async function handleConfig(ctx, subArgs) {
|
|
|
281
332
|
}
|
|
282
333
|
if (sub === "list") {
|
|
283
334
|
const views = await ctx.service.management.config.list(ctx.options.packageId);
|
|
284
|
-
const dict =
|
|
335
|
+
const dict = Object.create(null);
|
|
285
336
|
for (const v of views) {
|
|
286
|
-
if (v.configured
|
|
287
|
-
|
|
337
|
+
if (v.configured) {
|
|
338
|
+
if (v.secret && v.value === undefined) {
|
|
339
|
+
dict[v.key] = "********";
|
|
340
|
+
}
|
|
341
|
+
else if (v.value !== undefined) {
|
|
342
|
+
dict[v.key] = v.value;
|
|
343
|
+
}
|
|
288
344
|
}
|
|
289
345
|
}
|
|
290
346
|
ctx.writeOut(JSON.stringify(dict, null, 2));
|
|
@@ -298,7 +354,15 @@ export async function handleConfig(ctx, subArgs) {
|
|
|
298
354
|
}
|
|
299
355
|
try {
|
|
300
356
|
const item = await ctx.service.management.config.get(ctx.options.packageId, key);
|
|
301
|
-
|
|
357
|
+
if (item.redactedPaths.includes("") || (item.configured && item.value === undefined)) {
|
|
358
|
+
ctx.writeOut("<hidden>");
|
|
359
|
+
}
|
|
360
|
+
else if (item.value === undefined) {
|
|
361
|
+
ctx.writeOut("<not configured>");
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
ctx.writeOut(JSON.stringify(item.value));
|
|
365
|
+
}
|
|
302
366
|
return ExitCode.SUCCESS;
|
|
303
367
|
}
|
|
304
368
|
catch (err) {
|
|
@@ -311,21 +375,27 @@ export async function handleConfig(ctx, subArgs) {
|
|
|
311
375
|
}
|
|
312
376
|
}
|
|
313
377
|
if (sub === "set") {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (!key || rawVal === undefined) {
|
|
317
|
-
ctx.writeErr("Error: key and value required");
|
|
318
|
-
return ExitCode.INVALID_ARGUMENT;
|
|
319
|
-
}
|
|
320
|
-
let parsed = rawVal;
|
|
378
|
+
let parsed;
|
|
379
|
+
let value;
|
|
321
380
|
try {
|
|
322
|
-
parsed =
|
|
381
|
+
parsed = parseSetInput(subArgs, false);
|
|
382
|
+
assertConcreteConfigKey(parsed.key);
|
|
383
|
+
value = await resolveValueInput({
|
|
384
|
+
...parsed.input,
|
|
385
|
+
flatArgs,
|
|
386
|
+
signal,
|
|
387
|
+
stdin,
|
|
388
|
+
policy: { sanitizeInputErrors: true },
|
|
389
|
+
});
|
|
390
|
+
signal?.throwIfAborted();
|
|
323
391
|
}
|
|
324
|
-
catch {
|
|
325
|
-
|
|
392
|
+
catch (err) {
|
|
393
|
+
return ctx.emitError(subArgs.includes("--json"), err?.code || "INVALID_ARGUMENT", err?.message || String(err), ExitCode.INVALID_ARGUMENT, {
|
|
394
|
+
details: err?.details,
|
|
395
|
+
});
|
|
326
396
|
}
|
|
327
|
-
await ctx.service.management.config.set(ctx.options.packageId, key,
|
|
328
|
-
ctx.writeOut(`Config '${key}' updated`);
|
|
397
|
+
await ctx.service.management.config.set(ctx.options.packageId, parsed.key, value);
|
|
398
|
+
ctx.writeOut(`Config '${parsed.key}' updated`);
|
|
329
399
|
return ExitCode.SUCCESS;
|
|
330
400
|
}
|
|
331
401
|
if (sub === "delete") {
|
|
@@ -349,7 +419,7 @@ function decodeStateKeyArg(key) {
|
|
|
349
419
|
return { namespace: "", key };
|
|
350
420
|
}
|
|
351
421
|
}
|
|
352
|
-
export async function handleState(ctx, subArgs) {
|
|
422
|
+
export async function handleState(ctx, subArgs, flatArgs, signal, stdin) {
|
|
353
423
|
const sub = subArgs[0] || "list";
|
|
354
424
|
let namespace;
|
|
355
425
|
let isAll = false;
|
|
@@ -405,36 +475,32 @@ export async function handleState(ctx, subArgs) {
|
|
|
405
475
|
return ExitCode.SUCCESS;
|
|
406
476
|
}
|
|
407
477
|
if (sub === "set") {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (!key || rawVal === undefined) {
|
|
411
|
-
ctx.writeErr("Error: key and value required");
|
|
412
|
-
return ExitCode.INVALID_ARGUMENT;
|
|
413
|
-
}
|
|
414
|
-
let ns = namespace || "";
|
|
415
|
-
let actualKey = key;
|
|
416
|
-
if (namespace === undefined) {
|
|
417
|
-
const decoded = decodeStateKeyArg(key);
|
|
418
|
-
ns = decoded.namespace;
|
|
419
|
-
actualKey = decoded.key;
|
|
420
|
-
}
|
|
421
|
-
let parsed = rawVal;
|
|
478
|
+
let parsed;
|
|
479
|
+
let value;
|
|
422
480
|
try {
|
|
423
|
-
parsed =
|
|
481
|
+
parsed = parseSetInput(subArgs, true);
|
|
482
|
+
value = await resolveValueInput({
|
|
483
|
+
...parsed.input,
|
|
484
|
+
flatArgs,
|
|
485
|
+
signal,
|
|
486
|
+
stdin,
|
|
487
|
+
policy: { sanitizeInputErrors: true },
|
|
488
|
+
});
|
|
489
|
+
signal?.throwIfAborted();
|
|
424
490
|
}
|
|
425
|
-
catch {
|
|
426
|
-
|
|
491
|
+
catch (err) {
|
|
492
|
+
return ctx.emitError(isJson, err?.code || "INVALID_ARGUMENT", err?.message || String(err), ExitCode.INVALID_ARGUMENT, {
|
|
493
|
+
details: err?.details,
|
|
494
|
+
});
|
|
427
495
|
}
|
|
428
|
-
let
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
ttl = parseInt(subArgs[i].slice(6), 10);
|
|
435
|
-
}
|
|
496
|
+
let ns = parsed.namespace || "";
|
|
497
|
+
let actualKey = parsed.key;
|
|
498
|
+
if (parsed.namespace === undefined) {
|
|
499
|
+
const decoded = decodeStateKeyArg(parsed.key);
|
|
500
|
+
ns = decoded.namespace;
|
|
501
|
+
actualKey = decoded.key;
|
|
436
502
|
}
|
|
437
|
-
await ctx.service.management.state.set(ctx.options.packageId, "", actualKey,
|
|
503
|
+
await ctx.service.management.state.set(ctx.options.packageId, "", actualKey, value, { namespace: ns, ttl: parsed.ttl });
|
|
438
504
|
const displayKey = ns ? `${ns}:${actualKey}` : actualKey;
|
|
439
505
|
ctx.writeOut(`State '${displayKey}' updated`);
|
|
440
506
|
return ExitCode.SUCCESS;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseConfigOverrideAssignment } from "../input/config-override.js";
|
|
2
|
+
export { parseConfigOverrideAssignment } from "../input/config-override.js";
|
|
1
3
|
export function parseStandaloneArgs(argv) {
|
|
2
4
|
const separator = argv.indexOf("--");
|
|
3
5
|
const controlArgs = separator >= 0 ? argv.slice(0, separator) : argv;
|
|
@@ -13,17 +15,15 @@ export function parseStandaloneArgs(argv) {
|
|
|
13
15
|
else if (arg.startsWith("--data-dir=")) {
|
|
14
16
|
dataDir = arg.slice(11);
|
|
15
17
|
}
|
|
16
|
-
else if (arg === "--config"
|
|
17
|
-
const pair = controlArgs[++i];
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
configOverrides[k] = v.join("=");
|
|
18
|
+
else if (arg === "--config" || arg === "-c") {
|
|
19
|
+
const pair = controlArgs[++i] ?? "";
|
|
20
|
+
const { key, value } = parseConfigOverrideAssignment(pair);
|
|
21
|
+
Object.defineProperty(configOverrides, key, { value, enumerable: true, writable: true, configurable: true });
|
|
21
22
|
}
|
|
22
23
|
else if (arg.startsWith("--config=")) {
|
|
23
24
|
const pair = arg.slice(9);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
configOverrides[k] = v.join("=");
|
|
25
|
+
const { key, value } = parseConfigOverrideAssignment(pair);
|
|
26
|
+
Object.defineProperty(configOverrides, key, { value, enumerable: true, writable: true, configurable: true });
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
29
|
filteredArgs.push(arg);
|
|
@@ -45,10 +45,16 @@ export function printHelp(writeOut, options) {
|
|
|
45
45
|
writeOut(" <cmd> run <id> [--stdin-field <field>] [-- <assignments...>]");
|
|
46
46
|
writeOut(" Bind raw stdin text to an input field");
|
|
47
47
|
writeOut(" <cmd> config list/get/set/delete Manage package configuration");
|
|
48
|
+
writeOut(" <cmd> config set <key> [<value> | --stdin]");
|
|
49
|
+
writeOut(" <cmd> config set <key> [--stdin-field <field>] [-- <assignments...>]");
|
|
48
50
|
writeOut(" <cmd> state list/get/set/delete Manage shared state store");
|
|
51
|
+
writeOut(" <cmd> state set <key> [<value> | --stdin] [-n <namespace>] [--ttl <seconds>]");
|
|
52
|
+
writeOut(" <cmd> state set <key> [--stdin-field <field>] [-n <namespace>] [--ttl <seconds>] [-- <assignments...>]");
|
|
53
|
+
writeOut(" set 整键替换;完整值、--stdin 与结构化输入互斥");
|
|
54
|
+
writeOut(" 扁平赋值:field=text 或 field:=json;--stdin-field 保留原始正文");
|
|
49
55
|
writeOut("\nGlobal options:");
|
|
50
56
|
writeOut(" --data-dir <path> Custom runtime database directory");
|
|
51
|
-
writeOut(" --config <KEY=val>
|
|
57
|
+
writeOut(" --config <KEY=val|KEY:=json> Temporary config override");
|
|
52
58
|
}
|
|
53
59
|
export function emitError(writeOut, writeErr, isJson, code, message, exitCode, options) {
|
|
54
60
|
if (isJson) {
|
|
@@ -149,10 +149,10 @@ export class StandaloneDispatcher {
|
|
|
149
149
|
code = await handleRun(ctx, subArgs, actionArgs, control?.signal);
|
|
150
150
|
break;
|
|
151
151
|
case "config":
|
|
152
|
-
code = await handleConfig(ctx, subArgs);
|
|
152
|
+
code = await handleConfig(ctx, subArgs, argv.includes("--") ? actionArgs : undefined, control?.signal);
|
|
153
153
|
break;
|
|
154
154
|
case "state":
|
|
155
|
-
code = await handleState(ctx, subArgs);
|
|
155
|
+
code = await handleState(ctx, subArgs, argv.includes("--") ? actionArgs : undefined, control?.signal);
|
|
156
156
|
break;
|
|
157
157
|
default:
|
|
158
158
|
this.writeErr(`Unknown command: '${command}'`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置加密密钥材料。
|
|
3
|
+
*
|
|
4
|
+
* key 为 32 字节 AES-256 原始密钥;id 为密钥的稳定非敏感指纹
|
|
5
|
+
* (由 configKeyId 计算),随密文信封持久化,用于存储域密钥身份校验。
|
|
6
|
+
*/
|
|
7
|
+
export interface ConfigEncryptionKey {
|
|
8
|
+
/** 32 字节 AES-256 原始密钥材料 */
|
|
9
|
+
key: Uint8Array;
|
|
10
|
+
/** 密钥的 SHA-256 十六进制指纹(非敏感,可持久化) */
|
|
11
|
+
id: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 配置加密密钥提供器。
|
|
15
|
+
*
|
|
16
|
+
* create 为 true 时允许在缺失时生成并持久化本地密钥与身份标记;
|
|
17
|
+
* 为 false 时绝不生成任何密钥或标记,缺失即抛出 CONFIG_ENCRYPTION_ERROR。
|
|
18
|
+
* 同一提供器实例在首次成功加载后缓存材料,后续调用不再读取环境或文件。
|
|
19
|
+
*/
|
|
20
|
+
export type ConfigKeyProvider = (create: boolean) => ConfigEncryptionKey;
|
|
21
|
+
/**
|
|
22
|
+
* 计算加密密钥的稳定非敏感指纹。
|
|
23
|
+
*
|
|
24
|
+
* 输出为密钥字节 SHA-256 摘要的 64 位小写十六进制字符串(256 位熵的指纹),
|
|
25
|
+
* 可安全持久化与打印,不会泄露密钥材料本身。
|
|
26
|
+
*/
|
|
27
|
+
export declare function configKeyId(key: Uint8Array): string;
|
|
28
|
+
/**
|
|
29
|
+
* 判定存储值是否为加密信封。
|
|
30
|
+
*
|
|
31
|
+
* 识别任何 adenc: 前缀(含未知版本),调用方必须将命中值交给
|
|
32
|
+
* decryptConfigValue 严格解密;未知版本会在解密时明确报错,绝不降级为明文。
|
|
33
|
+
*/
|
|
34
|
+
export declare function isEncryptedConfigValue(raw: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* 加密单个配置值并返回规范密文信封。
|
|
37
|
+
*
|
|
38
|
+
* 信封格式:adenc:v1:<keyId>:<base64url(iv|ciphertext|authTag)>。
|
|
39
|
+
* 使用 AES-256-GCM 与随机 12 字节初始向量,认证数据无歧义绑定
|
|
40
|
+
* 格式版本、密钥指纹、配置域 scope 与配置键 key。
|
|
41
|
+
* 值必须严格 canonical JSON 可序列化(复用 validateJsonValue 单一事实源),
|
|
42
|
+
* 任何不可精确往返的输入都会抛出 CONFIG_ENCRYPTION_ERROR,错误信息与
|
|
43
|
+
* details 不包含值本身、校验器 reason/path 或密钥材料。
|
|
44
|
+
*/
|
|
45
|
+
export declare function encryptConfigValue(value: unknown, scope: string, key: string, material: ConfigEncryptionKey): string;
|
|
46
|
+
/**
|
|
47
|
+
* 严格解密 encryptConfigValue 产生的密文信封并返回原始值。
|
|
48
|
+
*
|
|
49
|
+
* 拒绝一切不规范输入:未知版本、非规范指纹、非规范或非未填充的
|
|
50
|
+
* base64url、负载截断、密钥身份不符、认证校验失败与非法 JSON 负载,
|
|
51
|
+
* 全部抛出 CONFIG_DECRYPT_FAILED,绝不将密文降级当作明文返回。
|
|
52
|
+
* 错误信息不包含明文片段、密文负载或密钥材料。
|
|
53
|
+
*/
|
|
54
|
+
export declare function decryptConfigValue(ciphertext: string, scope: string, key: string, material: ConfigEncryptionKey): unknown;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { ActionDockError, CONFIG_DECRYPT_FAILED, CONFIG_ENCRYPTION_ERROR } from "../errors.js";
|
|
3
|
+
import { parseJsonWithoutDuplicates } from "../project/digest.js";
|
|
4
|
+
import { validateJsonValue } from "../value-validator.js";
|
|
5
|
+
/** 密文信封统一前缀,任何以该前缀起始的存储值都按密文处理 */
|
|
6
|
+
const ENVELOPE_PREFIX = "adenc:";
|
|
7
|
+
/** 当前支持的密文格式版本 */
|
|
8
|
+
const CURRENT_VERSION = "v1";
|
|
9
|
+
/** AES-256-GCM 密钥字节长度 */
|
|
10
|
+
const KEY_BYTES = 32;
|
|
11
|
+
/** AES-GCM 随机初始向量字节长度 */
|
|
12
|
+
const IV_BYTES = 12;
|
|
13
|
+
/** AES-GCM 认证标签字节长度 */
|
|
14
|
+
const AUTH_TAG_BYTES = 16;
|
|
15
|
+
/** 密钥指纹的规范形式:64 位十六进制字符(SHA-256 摘要) */
|
|
16
|
+
const KEY_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
17
|
+
/** 密文信封整体规范形式:版本段、指纹段与未填充 base64url 负载段 */
|
|
18
|
+
const ENVELOPE_PATTERN = /^adenc:(v[0-9]+):([0-9a-f]{64}):([A-Za-z0-9_-]+)$/;
|
|
19
|
+
/** 未填充 base64url 字符集(RFC 4648 第 5 节) */
|
|
20
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
/** 序列化递归深度上限,与 canonical JSON 校验单一事实源的最大深度保持一致 */
|
|
22
|
+
const MAX_SERIALIZE_DEPTH = 512;
|
|
23
|
+
/**
|
|
24
|
+
* 计算加密密钥的稳定非敏感指纹。
|
|
25
|
+
*
|
|
26
|
+
* 输出为密钥字节 SHA-256 摘要的 64 位小写十六进制字符串(256 位熵的指纹),
|
|
27
|
+
* 可安全持久化与打印,不会泄露密钥材料本身。
|
|
28
|
+
*/
|
|
29
|
+
export function configKeyId(key) {
|
|
30
|
+
if (!(key instanceof Uint8Array) || key.byteLength === 0) {
|
|
31
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Cannot compute config key id: key material must be a non-empty Uint8Array");
|
|
32
|
+
}
|
|
33
|
+
return createHash("sha256").update(key).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 判定存储值是否为加密信封。
|
|
37
|
+
*
|
|
38
|
+
* 识别任何 adenc: 前缀(含未知版本),调用方必须将命中值交给
|
|
39
|
+
* decryptConfigValue 严格解密;未知版本会在解密时明确报错,绝不降级为明文。
|
|
40
|
+
*/
|
|
41
|
+
export function isEncryptedConfigValue(raw) {
|
|
42
|
+
return typeof raw === "string" && raw.startsWith(ENVELOPE_PREFIX);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 构造 AES-GCM 附加认证数据。
|
|
46
|
+
*
|
|
47
|
+
* 以 JSON 数组序列化无歧义绑定格式版本、密钥指纹、配置域与配置键,
|
|
48
|
+
* 防止 scope 或 key 含分隔符时的拼接歧义,阻止跨域跨键的密文挪用。
|
|
49
|
+
*/
|
|
50
|
+
function buildAdditionalAuthenticatedData(version, id, scope, key) {
|
|
51
|
+
return Buffer.from(JSON.stringify([`${ENVELOPE_PREFIX}${version}`, id, scope, key]), "utf8");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 校验并规范化密钥材料。
|
|
55
|
+
*
|
|
56
|
+
* 要求 key 恰为 32 字节,id 为规范指纹且与 key 实际指纹一致,
|
|
57
|
+
* 身份不符时立即拒绝,防止用错误身份写出无法解密的密文。
|
|
58
|
+
*/
|
|
59
|
+
function normalizeMaterial(material) {
|
|
60
|
+
if (material === null || typeof material !== "object") {
|
|
61
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Invalid config encryption material: expected ConfigEncryptionKey object");
|
|
62
|
+
}
|
|
63
|
+
const { key, id } = material;
|
|
64
|
+
if (!(key instanceof Uint8Array) || key.byteLength !== KEY_BYTES) {
|
|
65
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, `Invalid config encryption key: expected ${KEY_BYTES} bytes, got ${key instanceof Uint8Array ? key.byteLength : typeof key}`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof id !== "string" || !KEY_ID_PATTERN.test(id)) {
|
|
68
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Invalid config encryption key id: expected 64 lowercase hex characters");
|
|
69
|
+
}
|
|
70
|
+
if (configKeyId(key) !== id) {
|
|
71
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Config encryption key id does not match key material fingerprint");
|
|
72
|
+
}
|
|
73
|
+
return { keyBytes: Buffer.from(key.buffer, key.byteOffset, key.byteLength), id };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 校验配置域与配置键参数。
|
|
77
|
+
*/
|
|
78
|
+
function assertBindingFields(scope, key) {
|
|
79
|
+
if (typeof scope !== "string" || scope.length === 0) {
|
|
80
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Config encryption scope must be a non-empty string");
|
|
81
|
+
}
|
|
82
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
83
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, "Config encryption key name must be a non-empty string");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 严格校验并序列化配置值。
|
|
88
|
+
*
|
|
89
|
+
* 复用 validateJsonValue 作为 canonical JSON 规则的单一事实源:拒绝
|
|
90
|
+
* undefined、函数、符号、BigInt、非有限数值、访问器属性(仅检查描述符,
|
|
91
|
+
* 绝不执行 getter)、非枚举属性、Symbol 键、稀疏数组、非 plain 对象
|
|
92
|
+
* (Date、Map 等)、循环引用与超深嵌套,保证密文解密后能够精确还原。
|
|
93
|
+
* 校验失败只携带固定文案与机器可读的校验错误码,不携带 validator 的
|
|
94
|
+
* reason/path(两者可能嵌入用户对象抛出的异常内容),杜绝秘密外带。
|
|
95
|
+
*/
|
|
96
|
+
function serializeConfigValue(value, scope, key) {
|
|
97
|
+
const validation = validateJsonValue(value, { maxDepth: MAX_SERIALIZE_DEPTH });
|
|
98
|
+
if (!validation.valid) {
|
|
99
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, `Config value for key '${key}' in scope '${scope}' is not strictly JSON serializable`, { scope, key, code: validation.code });
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new ActionDockError(CONFIG_ENCRYPTION_ERROR, `Config value for key '${key}' in scope '${scope}' is not strictly JSON serializable`, { scope, key });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 加密单个配置值并返回规范密文信封。
|
|
110
|
+
*
|
|
111
|
+
* 信封格式:adenc:v1:<keyId>:<base64url(iv|ciphertext|authTag)>。
|
|
112
|
+
* 使用 AES-256-GCM 与随机 12 字节初始向量,认证数据无歧义绑定
|
|
113
|
+
* 格式版本、密钥指纹、配置域 scope 与配置键 key。
|
|
114
|
+
* 值必须严格 canonical JSON 可序列化(复用 validateJsonValue 单一事实源),
|
|
115
|
+
* 任何不可精确往返的输入都会抛出 CONFIG_ENCRYPTION_ERROR,错误信息与
|
|
116
|
+
* details 不包含值本身、校验器 reason/path 或密钥材料。
|
|
117
|
+
*/
|
|
118
|
+
export function encryptConfigValue(value, scope, key, material) {
|
|
119
|
+
assertBindingFields(scope, key);
|
|
120
|
+
const { keyBytes, id } = normalizeMaterial(material);
|
|
121
|
+
const plaintext = serializeConfigValue(value, scope, key);
|
|
122
|
+
const iv = randomBytes(IV_BYTES);
|
|
123
|
+
const cipher = createCipheriv("aes-256-gcm", keyBytes, iv, { authTagLength: AUTH_TAG_BYTES });
|
|
124
|
+
cipher.setAAD(buildAdditionalAuthenticatedData(CURRENT_VERSION, id, scope, key));
|
|
125
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
126
|
+
const authTag = cipher.getAuthTag();
|
|
127
|
+
const payload = Buffer.concat([iv, ciphertext, authTag]).toString("base64url");
|
|
128
|
+
return `${ENVELOPE_PREFIX}${CURRENT_VERSION}:${id}:${payload}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 严格解密 encryptConfigValue 产生的密文信封并返回原始值。
|
|
132
|
+
*
|
|
133
|
+
* 拒绝一切不规范输入:未知版本、非规范指纹、非规范或非未填充的
|
|
134
|
+
* base64url、负载截断、密钥身份不符、认证校验失败与非法 JSON 负载,
|
|
135
|
+
* 全部抛出 CONFIG_DECRYPT_FAILED,绝不将密文降级当作明文返回。
|
|
136
|
+
* 错误信息不包含明文片段、密文负载或密钥材料。
|
|
137
|
+
*/
|
|
138
|
+
export function decryptConfigValue(ciphertext, scope, key, material) {
|
|
139
|
+
assertBindingFields(scope, key);
|
|
140
|
+
const { keyBytes, id } = normalizeMaterial(material);
|
|
141
|
+
if (typeof ciphertext !== "string" || !ciphertext.startsWith(ENVELOPE_PREFIX)) {
|
|
142
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Stored config value for key '${key}' in scope '${scope}' is not an encrypted envelope`, { scope, key });
|
|
143
|
+
}
|
|
144
|
+
const match = ciphertext.match(ENVELOPE_PATTERN);
|
|
145
|
+
if (!match) {
|
|
146
|
+
const rest = ciphertext.slice(ENVELOPE_PREFIX.length);
|
|
147
|
+
const colon = rest.indexOf(":");
|
|
148
|
+
const version = colon === -1 ? rest : rest.slice(0, colon);
|
|
149
|
+
if (/^v[0-9]+$/.test(version) && version !== CURRENT_VERSION) {
|
|
150
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope version '${version}' is not supported (expected '${CURRENT_VERSION}')`, { scope, key, version });
|
|
151
|
+
}
|
|
152
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope for key '${key}' in scope '${scope}' is malformed`, { scope, key });
|
|
153
|
+
}
|
|
154
|
+
const [, version, envelopeId, payload] = match;
|
|
155
|
+
if (version !== CURRENT_VERSION) {
|
|
156
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope version '${version}' is not supported (expected '${CURRENT_VERSION}')`, { scope, key, version });
|
|
157
|
+
}
|
|
158
|
+
if (envelopeId !== id) {
|
|
159
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope for key '${key}' in scope '${scope}' was written with a different key identity`, { scope, key, envelopeKeyId: envelopeId });
|
|
160
|
+
}
|
|
161
|
+
if (!BASE64URL_PATTERN.test(payload)) {
|
|
162
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope payload for key '${key}' in scope '${scope}' is not canonical base64url`, { scope, key });
|
|
163
|
+
}
|
|
164
|
+
const raw = Buffer.from(payload, "base64url");
|
|
165
|
+
if (raw.byteLength < IV_BYTES + AUTH_TAG_BYTES + 1) {
|
|
166
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope payload for key '${key}' in scope '${scope}' is truncated`, { scope, key });
|
|
167
|
+
}
|
|
168
|
+
if (raw.toString("base64url") !== payload) {
|
|
169
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Encrypted config envelope payload for key '${key}' in scope '${scope}' is not canonical base64url`, { scope, key });
|
|
170
|
+
}
|
|
171
|
+
const iv = raw.subarray(0, IV_BYTES);
|
|
172
|
+
const authTag = raw.subarray(raw.byteLength - AUTH_TAG_BYTES);
|
|
173
|
+
const encrypted = raw.subarray(IV_BYTES, raw.byteLength - AUTH_TAG_BYTES);
|
|
174
|
+
let plaintext;
|
|
175
|
+
try {
|
|
176
|
+
const decipher = createDecipheriv("aes-256-gcm", keyBytes, iv, { authTagLength: AUTH_TAG_BYTES });
|
|
177
|
+
decipher.setAAD(buildAdditionalAuthenticatedData(version, envelopeId, scope, key));
|
|
178
|
+
decipher.setAuthTag(authTag);
|
|
179
|
+
plaintext = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Decryption of config value for key '${key}' in scope '${scope}' failed authentication`, { scope, key });
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return parseJsonWithoutDuplicates(plaintext.toString("utf8"));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new ActionDockError(CONFIG_DECRYPT_FAILED, `Decrypted config payload for key '${key}' in scope '${scope}' is not valid JSON`, { scope, key });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ConfigKeyProvider } from "./config-cipher.js";
|
|
2
|
+
/**
|
|
3
|
+
* 配置加密密钥提供器构造选项。
|
|
4
|
+
*/
|
|
5
|
+
export interface ConfigKeyProviderOptions {
|
|
6
|
+
/** 覆盖用户主目录定位(与 getActionDockHome 的 customHome 语义一致) */
|
|
7
|
+
customHome?: string;
|
|
8
|
+
/** 数据目录定位:密钥落盘为 <dataDir>/keys/config.key,优先于主目录定位 */
|
|
9
|
+
dataDir?: string;
|
|
10
|
+
/** 显式注入密钥材料:纯内存使用,不读取环境变量,也不访问或创建任何真实目录 */
|
|
11
|
+
key?: Uint8Array;
|
|
12
|
+
/** 环境变量快照来源(缺省读取 process.env),至少须支持 ACTIONDOCK_ENCRYPTION_KEY */
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 创建配置加密密钥提供器。
|
|
17
|
+
*
|
|
18
|
+
* 来源优先级:
|
|
19
|
+
* - 显式注入 key:纯内存材料,绝不读取环境变量或真实用户目录,也无需钉住持久身份;
|
|
20
|
+
* - 环境变量 ACTIONDOCK_ENCRYPTION_KEY:必须为恰好 64 位十六进制字符,
|
|
21
|
+
* 只要被设置(含空串或空白)就必须严格合法,非法取值立即报错,
|
|
22
|
+
* 绝不静默回退到本地密钥;
|
|
23
|
+
* - 本地密钥文件:dataDir 有值时为 <dataDir>/keys/config.key,
|
|
24
|
+
* 否则为 getActionDockHome(customHome)/.actiondock/keys/config.key。
|
|
25
|
+
*
|
|
26
|
+
* 构造时不发生任何环境读取、文件访问或目录创建;全部定位与加载在
|
|
27
|
+
* 首次调用时完成并缓存,此后同一提供器实例不再感知环境变化(宿主快照)。
|
|
28
|
+
* 若首次调用以 create 为 false 加载了未钉住身份的材料,后续 create 为 true
|
|
29
|
+
* 的调用会先原子创建或校验身份标记再返回缓存材料,保证允许写入前
|
|
30
|
+
* 持久域身份一定已经落盘。
|
|
31
|
+
*
|
|
32
|
+
* 身份保护:标记存在而密钥材料丢失时直接报错,绝不为同一存储域重新
|
|
33
|
+
* 生成新密钥;提供密钥与标记指纹不符时同样拒绝。
|
|
34
|
+
*/
|
|
35
|
+
export declare function createConfigKeyProvider(options?: ConfigKeyProviderOptions): ConfigKeyProvider;
|