@actiondock/core 2.20.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/errors.d.ts +11 -1
- package/dist/errors.js +23 -0
- package/dist/execution/service.js +36 -5
- package/dist/host/host.d.ts +11 -0
- package/dist/host/host.js +258 -39
- package/dist/host/types.d.ts +10 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/invocation/types.d.ts +31 -0
- package/dist/invocation/types.js +10 -1
- package/dist/package/index.d.ts +1 -1
- package/dist/package/runtime.js +18 -4
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +11 -0
- 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/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +3 -0
- package/dist/project/index.js +3 -0
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +15 -30
- 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/runtime/context.d.ts +21 -2
- package/dist/runtime/context.js +42 -17
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- 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/routes/config.js +20 -1
- package/dist/service/factory.js +3 -0
- package/dist/service/types.d.ts +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/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,5 +1,6 @@
|
|
|
1
1
|
import type { ActionContext, ActionInvokeOptions, ActionRef, Config, Logger, ProcessAPI, ProgressReporter, StateStore } from "@actiondock/sdk";
|
|
2
2
|
import type { ProjectConfig } from "../project/types.js";
|
|
3
|
+
import type { ConfigItemDefinition } from "../project/types.js";
|
|
3
4
|
import type { RuntimeStorage } from "../storage/types.js";
|
|
4
5
|
import { ProcessManager, type ProcessOwner } from "../process/index.js";
|
|
5
6
|
import type { ConfigSource } from "../service/types.js";
|
|
@@ -22,11 +23,15 @@ export declare class RuntimeConfig implements Config {
|
|
|
22
23
|
get<T = unknown>(key: string): T | undefined;
|
|
23
24
|
get<T = unknown>(key: string, defaultValue: T): T;
|
|
24
25
|
has(key: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* 含历史保护声明的配置解析结果视图。
|
|
28
|
+
*/
|
|
29
|
+
describeWithProtection<T = unknown>(key: string): ConfigResolutionWithProtection<T>;
|
|
25
30
|
/**
|
|
26
31
|
* 结构化解析指定配置项的来源与取值视图。
|
|
27
32
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
33
|
+
* 委托 describeWithProtection 五层优先级链单一事实源,剥出既有公开契约视图,
|
|
34
|
+
* 避免双实现漂移;历史保护声明仅由管理视图消费。
|
|
30
35
|
* source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
|
|
31
36
|
* null/false/0/空串均为真实值;整链未命中时 source 为 null。
|
|
32
37
|
*/
|
|
@@ -35,6 +40,20 @@ export declare class RuntimeConfig implements Config {
|
|
|
35
40
|
value: T | undefined;
|
|
36
41
|
};
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* 含历史保护声明的配置解析结果视图。
|
|
45
|
+
*/
|
|
46
|
+
export interface ConfigResolutionWithProtection<T = unknown> {
|
|
47
|
+
/** 实际生效值来源;整链未命中为 null */
|
|
48
|
+
source: ConfigSource | null;
|
|
49
|
+
/** 实际生效值 */
|
|
50
|
+
value: T | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* 值来源存储持久化的历史保护声明(仅 package/global 来源可能存在)。
|
|
53
|
+
* 与值取自同一次原子读取,避免并发删除造成值与规则错配。
|
|
54
|
+
*/
|
|
55
|
+
protection?: readonly ConfigItemDefinition[];
|
|
56
|
+
}
|
|
38
57
|
/**
|
|
39
58
|
* 生产级状态存储实现,将状态持久化到 SQLite 中,完整支持命名空间分段隔离与 TTL。
|
|
40
59
|
*
|
package/dist/runtime/context.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { assertConcreteConfigKey, assertValidConfigDeclarations, resolveConfigDeclaration, validateConfigValue } from "../project/config-declarations.js";
|
|
2
|
+
import { readConfigWithProtection } from "../storage/index.js";
|
|
2
3
|
import { MemoryProcessDriver, ProcessManager } from "../process/index.js";
|
|
3
4
|
import { createDefaultProcessOwner } from "../invocation/types.js";
|
|
4
5
|
import { resolveEnvValue } from "./env.js";
|
|
5
|
-
import { ActionDockError, INVALID_ACTION_REF, INVALID_ARGUMENT, INVALID_STATE } from "../errors.js";
|
|
6
|
+
import { ActionDockError, isConfigEncryptionError, INVALID_ACTION_REF, INVALID_ARGUMENT, INVALID_STATE, } from "../errors.js";
|
|
6
7
|
/**
|
|
7
8
|
* 生产级配置解析器实现。
|
|
8
9
|
* 严格践行 5 层配置解析优先级链:
|
|
@@ -41,14 +42,9 @@ export class RuntimeConfig {
|
|
|
41
42
|
return !(resolved.source === null && resolved.value === undefined);
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
|
|
47
|
-
* 避免重复实现解析链导致行为漂移。
|
|
48
|
-
* source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
|
|
49
|
-
* null/false/0/空串均为真实值;整链未命中时 source 为 null。
|
|
45
|
+
* 含历史保护声明的配置解析结果视图。
|
|
50
46
|
*/
|
|
51
|
-
|
|
47
|
+
describeWithProtection(key) {
|
|
52
48
|
assertConcreteConfigKey(key);
|
|
53
49
|
// 1. 运行时固定覆盖与单次调用覆盖(值为 undefined 视为未命中,继续解析链)
|
|
54
50
|
if (this.overrides.has(key)) {
|
|
@@ -57,25 +53,42 @@ export class RuntimeConfig {
|
|
|
57
53
|
return { source: "override", value: overrideValue };
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
|
-
// 2.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
// 2. 包级持久化存储:原子快照配对读取值与历史保护声明;
|
|
57
|
+
// 加密错误直接透传,绝不降级继续解析链(旧版库已在打开时自动迁移)
|
|
58
|
+
const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
|
|
59
|
+
const packageSnapshot = readConfigWithProtection(this.storage, key);
|
|
60
|
+
if (packageSnapshot !== undefined) {
|
|
61
|
+
return {
|
|
62
|
+
source: "package",
|
|
63
|
+
value: packageSnapshot.value,
|
|
64
|
+
...(packageSnapshot.protection !== undefined
|
|
65
|
+
? { protection: packageSnapshot.protection }
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
64
68
|
}
|
|
65
|
-
// 3.
|
|
69
|
+
// 3. 全局持久化存储(一般读取异常降级继续解析链并告警;
|
|
70
|
+
// 加密错误严禁回退,必须原样透传)
|
|
66
71
|
if (this.globalStorage) {
|
|
67
72
|
try {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
return {
|
|
73
|
+
const globalSnapshot = readConfigWithProtection(this.globalStorage, key);
|
|
74
|
+
if (globalSnapshot !== undefined) {
|
|
75
|
+
return {
|
|
76
|
+
source: "global",
|
|
77
|
+
value: globalSnapshot.value,
|
|
78
|
+
...(globalSnapshot.protection !== undefined
|
|
79
|
+
? { protection: globalSnapshot.protection }
|
|
80
|
+
: {}),
|
|
81
|
+
};
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
catch (err) {
|
|
85
|
+
if (isConfigEncryptionError(err)) {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
74
88
|
console.warn(`[actiondock] global config read failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
// 4. 环境变量
|
|
78
|
-
const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
|
|
79
92
|
const itemDef = resolvedDecl?.item;
|
|
80
93
|
const envResolved = resolveEnvValue(key, itemDef, this.projectConfig?.id);
|
|
81
94
|
if (envResolved !== undefined) {
|
|
@@ -87,6 +100,18 @@ export class RuntimeConfig {
|
|
|
87
100
|
}
|
|
88
101
|
return { source: null, value: undefined };
|
|
89
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* 结构化解析指定配置项的来源与取值视图。
|
|
105
|
+
*
|
|
106
|
+
* 委托 describeWithProtection 五层优先级链单一事实源,剥出既有公开契约视图,
|
|
107
|
+
* 避免双实现漂移;历史保护声明仅由管理视图消费。
|
|
108
|
+
* source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
|
|
109
|
+
* null/false/0/空串均为真实值;整链未命中时 source 为 null。
|
|
110
|
+
*/
|
|
111
|
+
describe(key) {
|
|
112
|
+
const resolved = this.describeWithProtection(key);
|
|
113
|
+
return { source: resolved.source, value: resolved.value };
|
|
114
|
+
}
|
|
90
115
|
}
|
|
91
116
|
/**
|
|
92
117
|
* 生产级状态存储实现,将状态持久化到 SQLite 中,完整支持命名空间分段隔离与 TTL。
|
|
@@ -63,7 +63,7 @@ export interface RunFinalizer {
|
|
|
63
63
|
/** 竞态监听句柄注销函数列表(finalize 时统一清理,覆盖成功路径) */
|
|
64
64
|
raceListenerRemovers: Array<() => void>;
|
|
65
65
|
/** 绑定取消控制器并启动超时定时器 */
|
|
66
|
-
startTimeout(controller: AbortController, timeoutMs: number): void;
|
|
66
|
+
startTimeout(controller: AbortController, timeoutMs: number, displayTimeoutMs?: number): void;
|
|
67
67
|
/** 写入终态(幂等,自动清理超时定时器与全部监听句柄) */
|
|
68
68
|
finalize(status: TerminalRunStatus, output?: unknown, error?: RuntimeError): void;
|
|
69
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionDockError, RUN_PERSISTENCE_FAILED, RUN_REPOSITORY_UNAVAILABLE } from "../errors.js";
|
|
1
|
+
import { ActionDockError, ACTION_TIMEOUT, RUN_PERSISTENCE_FAILED, RUN_REPOSITORY_UNAVAILABLE } from "../errors.js";
|
|
2
2
|
import { snapshotRunOrigin } from "./run-origin.js";
|
|
3
3
|
/**
|
|
4
4
|
* 构建运行初始记录(RunRecord 组装单一事实源)。
|
|
@@ -79,11 +79,12 @@ export function createRunFinalizer(storage, runId) {
|
|
|
79
79
|
signal: undefined,
|
|
80
80
|
onAbort: undefined,
|
|
81
81
|
raceListenerRemovers: [],
|
|
82
|
-
startTimeout: (controller, timeoutMs) => {
|
|
82
|
+
startTimeout: (controller, timeoutMs, displayTimeoutMs) => {
|
|
83
83
|
finalizer.controller = controller;
|
|
84
|
+
const display = displayTimeoutMs ?? timeoutMs;
|
|
84
85
|
finalizer.timeoutTimer = setTimeout(() => {
|
|
85
86
|
finalizer.isTimeout = true;
|
|
86
|
-
finalizer.controller?.abort(new
|
|
87
|
+
finalizer.controller?.abort(new ActionDockError(ACTION_TIMEOUT, `Action exceeded timeout of ${display}ms`, { timeoutMs: display }));
|
|
87
88
|
}, timeoutMs);
|
|
88
89
|
},
|
|
89
90
|
finalize: (status, output, error) => {
|
|
@@ -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;
|