@actiondock/core 2.4.1 → 2.5.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.
@@ -118,6 +118,27 @@ export function addProfile(name, entry, customHome) {
118
118
  token: entry.token?.trim() || undefined,
119
119
  tokenEnv: entry.tokenEnv?.trim() || undefined,
120
120
  description: entry.description?.trim() || undefined,
121
+ insecure: entry.insecure !== undefined ? Boolean(entry.insecure) : undefined,
122
+ };
123
+ saveProfiles(profilesConfig, customHome);
124
+ }
125
+ export function updateProfile(name, entry, customHome) {
126
+ const trimmedName = name.trim();
127
+ if (!trimmedName) {
128
+ throw new Error("Profile name cannot be empty");
129
+ }
130
+ const profilesConfig = loadProfiles(customHome);
131
+ const existing = profilesConfig.profiles[trimmedName];
132
+ if (!existing) {
133
+ throw new Error(`Profile '${trimmedName}' not found. Configure it with 'ad profile add ${trimmedName} --server <url>'`);
134
+ }
135
+ const serverUrl = entry.serverUrl ? normalizeServerUrl(entry.serverUrl) : existing.serverUrl;
136
+ profilesConfig.profiles[trimmedName] = {
137
+ serverUrl,
138
+ token: entry.token !== undefined ? (entry.token.trim() || undefined) : existing.token,
139
+ tokenEnv: entry.tokenEnv !== undefined ? (entry.tokenEnv.trim() || undefined) : existing.tokenEnv,
140
+ description: entry.description !== undefined ? (entry.description.trim() || undefined) : existing.description,
141
+ insecure: entry.insecure !== undefined ? Boolean(entry.insecure) : existing.insecure,
121
142
  };
122
143
  saveProfiles(profilesConfig, customHome);
123
144
  }
@@ -215,14 +236,22 @@ export function resolveProfileToken(profileName, entry, explicitToken) {
215
236
  return { token: undefined, source: "none" };
216
237
  }
217
238
  export function resolveTarget(options, customHome) {
239
+ const envInsecure = typeof process !== "undefined" &&
240
+ (process.env?.ACTIONDOCK_INSECURE === "true" || process.env?.ACTIONDOCK_INSECURE === "1");
241
+ const envAllowInsecureHttp = typeof process !== "undefined" &&
242
+ (process.env?.ACTIONDOCK_ALLOW_INSECURE_HTTP === "true" || process.env?.ACTIONDOCK_ALLOW_INSECURE_HTTP === "1");
243
+ const effectiveAllowInsecureHttp = options?.allowInsecureHttp !== undefined ? Boolean(options.allowInsecureHttp) : envAllowInsecureHttp;
218
244
  // 1. Explicit CLI --server flag
219
245
  if (options?.server && options.server.trim()) {
220
246
  const resolvedToken = resolveProfileToken(undefined, undefined, options.token);
247
+ const effectiveInsecure = options.insecure !== undefined ? Boolean(options.insecure) : envInsecure;
221
248
  return {
222
249
  type: "remote",
223
250
  serverUrl: normalizeServerUrl(options.server),
224
251
  token: resolvedToken.token,
225
252
  tokenSource: resolvedToken.source,
253
+ insecure: effectiveInsecure,
254
+ allowInsecureHttp: effectiveAllowInsecureHttp,
226
255
  };
227
256
  }
228
257
  const profilesConfig = loadProfiles(customHome);
@@ -237,22 +266,30 @@ export function resolveTarget(options, customHome) {
237
266
  throw new Error(`Profile '${pName}' not found. Configure it with 'ad profile add ${pName} --server <url>'`);
238
267
  }
239
268
  const resolvedToken = resolveProfileToken(pName, found, options.token);
269
+ const effectiveInsecure = options.insecure !== undefined
270
+ ? Boolean(options.insecure)
271
+ : (envInsecure || Boolean(found.insecure));
240
272
  return {
241
273
  type: "remote",
242
274
  profileName: pName,
243
275
  serverUrl: found.serverUrl,
244
276
  token: resolvedToken.token,
245
277
  tokenSource: resolvedToken.source,
278
+ insecure: effectiveInsecure,
279
+ allowInsecureHttp: effectiveAllowInsecureHttp,
246
280
  };
247
281
  }
248
282
  // 3. Environment variable ACTIONDOCK_SERVER_URL
249
283
  if (process.env.ACTIONDOCK_SERVER_URL && process.env.ACTIONDOCK_SERVER_URL.trim()) {
250
284
  const resolvedToken = resolveProfileToken(undefined, undefined, options?.token);
285
+ const effectiveInsecure = options?.insecure !== undefined ? Boolean(options.insecure) : envInsecure;
251
286
  return {
252
287
  type: "remote",
253
288
  serverUrl: normalizeServerUrl(process.env.ACTIONDOCK_SERVER_URL),
254
289
  token: resolvedToken.token,
255
290
  tokenSource: resolvedToken.source,
291
+ insecure: effectiveInsecure,
292
+ allowInsecureHttp: effectiveAllowInsecureHttp,
256
293
  };
257
294
  }
258
295
  // 4. Environment variable ACTIONDOCK_PROFILE
@@ -266,12 +303,17 @@ export function resolveTarget(options, customHome) {
266
303
  throw new Error(`Profile '${pName}' (from ACTIONDOCK_PROFILE) not found. Configure it with 'ad profile add ${pName} --server <url>'`);
267
304
  }
268
305
  const resolvedToken = resolveProfileToken(pName, found, options?.token);
306
+ const effectiveInsecure = options?.insecure !== undefined
307
+ ? Boolean(options.insecure)
308
+ : (envInsecure || Boolean(found.insecure));
269
309
  return {
270
310
  type: "remote",
271
311
  profileName: pName,
272
312
  serverUrl: found.serverUrl,
273
313
  token: resolvedToken.token,
274
314
  tokenSource: resolvedToken.source,
315
+ insecure: effectiveInsecure,
316
+ allowInsecureHttp: effectiveAllowInsecureHttp,
275
317
  };
276
318
  }
277
319
  // 5. Current Profile in config
@@ -280,12 +322,17 @@ export function resolveTarget(options, customHome) {
280
322
  const found = profilesConfig.profiles[current];
281
323
  if (found && found.serverUrl && found.serverUrl !== "local") {
282
324
  const resolvedToken = resolveProfileToken(current, found, options?.token);
325
+ const effectiveInsecure = options?.insecure !== undefined
326
+ ? Boolean(options.insecure)
327
+ : (envInsecure || Boolean(found.insecure));
283
328
  return {
284
329
  type: "remote",
285
330
  profileName: current,
286
331
  serverUrl: found.serverUrl,
287
332
  token: resolvedToken.token,
288
333
  tokenSource: resolvedToken.source,
334
+ insecure: effectiveInsecure,
335
+ allowInsecureHttp: effectiveAllowInsecureHttp,
289
336
  };
290
337
  }
291
338
  }
@@ -13,6 +13,8 @@ export interface ProfileEntry {
13
13
  tokenEnv?: string;
14
14
  /** 该机器/环境的描述信息 */
15
15
  description?: string;
16
+ /** 是否跳过 TLS 证书合法性校验(用于局域网内网自签证书) */
17
+ insecure?: boolean;
16
18
  }
17
19
  /**
18
20
  * 全局 profiles.json 配置文件结构契约。
@@ -41,6 +43,10 @@ export interface ResolvedTarget {
41
43
  token?: string;
42
44
  /** Token 数据来源 */
43
45
  tokenSource?: TokenResolutionSource;
46
+ /** 是否跳过 TLS 证书合法性校验 */
47
+ insecure?: boolean;
48
+ /** 是否允许向非回环地址发送明文 HTTP 请求 */
49
+ allowInsecureHttp?: boolean;
44
50
  }
45
51
  /**
46
52
  * 远端服务器健康探测与时延检测结果。
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
86
86
  node: ">=24.12.0",
87
87
  },
88
88
  dependencies: {
89
- "@actiondock/sdk": "^2.4.1",
89
+ "@actiondock/sdk": "^2.5.0",
90
90
  },
91
91
  devDependencies: {
92
- "@actiondock/testing": "^2.4.1",
92
+ "@actiondock/testing": "^2.5.0",
93
93
  "@types/node": "^22.13.0",
94
94
  "tsx": "^4.19.0",
95
95
  "typescript": "^5.7.0",
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 获取底层 HTTP 调度器(如 Undici Agent 或自定义 Dispatcher)的提供者函数契约。
3
+ */
4
+ export type FetchDispatcherProvider = () => unknown;
5
+ /**
6
+ * 注册全局用于忽略服务端证书校验的调度器提供者。
7
+ */
8
+ export declare function setInsecureDispatcherProvider(provider?: FetchDispatcherProvider): void;
9
+ /**
10
+ * 获取当前已注册的调度器提供者函数。
11
+ */
12
+ export declare function getInsecureDispatcherProvider(): FetchDispatcherProvider | undefined;
13
+ /**
14
+ * 获取用于忽略服务端证书校验的调度器实例。
15
+ * 纯粹解耦设计:
16
+ * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 注册);
17
+ * 2. 检查全局符号注入的提供者;
18
+ * 3. 若在 Node 环境下且前述未注入,安全动态加载兜底连接池,绝不让 core 包产生静态硬编译依赖。
19
+ */
20
+ export declare function getInsecureDispatcher(): unknown;
21
+ /**
22
+ * 显式强制重置并销毁调度器连接池(仅用于单元测试重置或进程退出)。
23
+ */
24
+ export declare function closeInsecureDispatcher(): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { createRequire } from "node:module";
2
+ let insecureDispatcherProvider;
3
+ let fallbackInsecureAgent;
4
+ /**
5
+ * 注册全局用于忽略服务端证书校验的调度器提供者。
6
+ */
7
+ export function setInsecureDispatcherProvider(provider) {
8
+ insecureDispatcherProvider = provider;
9
+ }
10
+ /**
11
+ * 获取当前已注册的调度器提供者函数。
12
+ */
13
+ export function getInsecureDispatcherProvider() {
14
+ return insecureDispatcherProvider;
15
+ }
16
+ /**
17
+ * 获取用于忽略服务端证书校验的调度器实例。
18
+ * 纯粹解耦设计:
19
+ * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 注册);
20
+ * 2. 检查全局符号注入的提供者;
21
+ * 3. 若在 Node 环境下且前述未注入,安全动态加载兜底连接池,绝不让 core 包产生静态硬编译依赖。
22
+ */
23
+ export function getInsecureDispatcher() {
24
+ if (insecureDispatcherProvider) {
25
+ return insecureDispatcherProvider();
26
+ }
27
+ const globalProvider = globalThis[Symbol.for("actiondock.insecureDispatcherProvider")];
28
+ if (typeof globalProvider === "function") {
29
+ return globalProvider();
30
+ }
31
+ if (typeof process !== "undefined" && process.versions?.node) {
32
+ try {
33
+ const req = createRequire(import.meta.url);
34
+ const runtimeNode = req("@actiondock/runtime-node");
35
+ if (typeof runtimeNode?.getInsecureDispatcher === "function") {
36
+ return runtimeNode.getInsecureDispatcher();
37
+ }
38
+ }
39
+ catch {
40
+ // 忽略未安装 @actiondock/runtime-node 的情况
41
+ }
42
+ if (!fallbackInsecureAgent || fallbackInsecureAgent.closed || fallbackInsecureAgent.destroyed) {
43
+ try {
44
+ const req = createRequire(import.meta.url);
45
+ const undici = req("undici");
46
+ if (undici?.Agent) {
47
+ fallbackInsecureAgent = new undici.Agent({
48
+ connect: {
49
+ rejectUnauthorized: false,
50
+ },
51
+ bodyTimeout: 0,
52
+ });
53
+ }
54
+ }
55
+ catch {
56
+ // 忽略非 Node 环境或未安装 undici 的异常
57
+ }
58
+ }
59
+ return fallbackInsecureAgent;
60
+ }
61
+ return undefined;
62
+ }
63
+ /**
64
+ * 显式强制重置并销毁调度器连接池(仅用于单元测试重置或进程退出)。
65
+ */
66
+ export async function closeInsecureDispatcher() {
67
+ const globalClose = globalThis[Symbol.for("actiondock.closeInsecureDispatcher")];
68
+ if (typeof globalClose === "function") {
69
+ try {
70
+ await globalClose();
71
+ }
72
+ catch {
73
+ // 忽略关闭异常
74
+ }
75
+ }
76
+ if (fallbackInsecureAgent) {
77
+ const agent = fallbackInsecureAgent;
78
+ fallbackInsecureAgent = undefined;
79
+ try {
80
+ await agent.close();
81
+ }
82
+ catch {
83
+ // 忽略关闭异常
84
+ }
85
+ }
86
+ }
@@ -3,3 +3,4 @@ export * from "./security.js";
3
3
  export * from "./body.js";
4
4
  export * from "./routes/index.js";
5
5
  export * from "./server.js";
6
+ export * from "./dispatcher.js";
@@ -3,3 +3,4 @@ export * from "./security.js";
3
3
  export * from "./body.js";
4
4
  export * from "./routes/index.js";
5
5
  export * from "./server.js";
6
+ export * from "./dispatcher.js";
@@ -1,4 +1,4 @@
1
- import type { ActionDockServerInstance, CoreHttpServerInstance, ServerOptions } from "./types.js";
1
+ import type { ActionDockServerInstance, CoreHttpServerInstance, ServerOptions, ServerTlsOptions } from "./types.js";
2
2
  /**
3
3
  * 规范化主机地址用于拼接 URL。
4
4
  * 若 host 包含冒号 : 且不以 [ 开头,则添加中括号包裹(IPv6 标准格式)。
@@ -7,7 +7,7 @@ export declare function formatHostForUrl(host: string): string;
7
7
  /**
8
8
  * 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
9
9
  */
10
- export declare function launchHttpServer(port: number, host: string, fetchHandler: (req: Request) => Promise<Response>): Promise<CoreHttpServerInstance>;
10
+ export declare function launchHttpServer(port: number, host: string, fetchHandler: (req: Request) => Promise<Response>, tls?: ServerTlsOptions): Promise<CoreHttpServerInstance>;
11
11
  /**
12
12
  * 启动 ActionDock 2.0 原生轻量级 HTTP 服务端。
13
13
  * 作为 ActionDockHost 与 ActionDockTarget 的薄适配层,负责中间件流转、认证拦截与路由分发。
@@ -1,4 +1,5 @@
1
1
  import { createServer as createNodeHttpServer } from "node:http";
2
+ import { readFileSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { Readable } from "node:stream";
4
5
  import { pipeline } from "node:stream/promises";
@@ -21,8 +22,8 @@ export function formatHostForUrl(host) {
21
22
  /**
22
23
  * 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
23
24
  */
24
- export async function launchHttpServer(port, host, fetchHandler) {
25
- const srv = createNodeHttpServer(async (req, res) => {
25
+ export async function launchHttpServer(port, host, fetchHandler, tls) {
26
+ const requestListener = async (req, res) => {
26
27
  const ac = new AbortController();
27
28
  const onReqClose = () => {
28
29
  if (!req.complete) {
@@ -84,7 +85,33 @@ export async function launchHttpServer(port, host, fetchHandler) {
84
85
  req.removeListener("close", onReqClose);
85
86
  res.removeListener("close", onResClose);
86
87
  }
87
- });
88
+ };
89
+ let srv;
90
+ if (tls) {
91
+ let cert = tls.cert;
92
+ if (!cert && tls.certPath) {
93
+ cert = readFileSync(tls.certPath);
94
+ }
95
+ let key = tls.key;
96
+ if (!key && tls.keyPath) {
97
+ key = readFileSync(tls.keyPath);
98
+ }
99
+ const httpsOptions = {
100
+ cert,
101
+ key,
102
+ };
103
+ if (tls.ca) {
104
+ httpsOptions.ca = tls.ca;
105
+ }
106
+ if (tls.passphrase) {
107
+ httpsOptions.passphrase = tls.passphrase;
108
+ }
109
+ const { createServer: createHttpsServer } = await import("node:https");
110
+ srv = createHttpsServer(httpsOptions, requestListener);
111
+ }
112
+ else {
113
+ srv = createNodeHttpServer(requestListener);
114
+ }
88
115
  const instance = {
89
116
  port,
90
117
  stop: async () => {
@@ -313,11 +340,12 @@ export async function startActionDockServer(options = {}) {
313
340
  },
314
341
  }, 404, corsHeaders);
315
342
  };
316
- const server = await launchHttpServer(port, host, fetchHandler);
343
+ const server = await launchHttpServer(port, host, fetchHandler, options.tls);
317
344
  if (server.ready) {
318
345
  await server.ready;
319
346
  }
320
347
  const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
348
+ const protocol = options.tls ? "https" : "http";
321
349
  const instance = {
322
350
  get port() {
323
351
  return server.port ?? port;
@@ -328,13 +356,14 @@ export async function startActionDockServer(options = {}) {
328
356
  host: hostInstance,
329
357
  target: targetInstance,
330
358
  get url() {
331
- return `http://${formatHostForUrl(actualHost)}:${this.port}`;
359
+ return `${protocol}://${formatHostForUrl(actualHost)}:${this.port}`;
332
360
  },
333
361
  ready: Promise.resolve(),
334
362
  stop: async (stopOptions) => {
363
+ await server.stop(true);
335
364
  if (targetInstance) {
336
365
  try {
337
- await targetInstance.close();
366
+ await targetInstance.close(stopOptions?.graceMs !== undefined ? { timeoutMs: stopOptions.graceMs } : undefined);
338
367
  }
339
368
  catch {
340
369
  // 忽略关闭异常
@@ -348,7 +377,6 @@ export async function startActionDockServer(options = {}) {
348
377
  // 忽略宿主关闭异常
349
378
  }
350
379
  }
351
- await server.stop(true);
352
380
  },
353
381
  };
354
382
  return instance;
@@ -5,10 +5,25 @@ export interface CoreHttpServerInstance {
5
5
  stop: (closeActiveConnections?: boolean) => void | Promise<void>;
6
6
  ready?: Promise<void>;
7
7
  }
8
+ export interface ServerTlsOptions {
9
+ /** TLS 证书内容(PEM 格式字符串或 Buffer) */
10
+ cert?: string | Buffer;
11
+ /** TLS 私钥内容(PEM 格式字符串或 Buffer) */
12
+ key?: string | Buffer;
13
+ /** TLS 证书文件绝对或相对路径 */
14
+ certPath?: string;
15
+ /** TLS 私钥文件绝对或相对路径 */
16
+ keyPath?: string;
17
+ /** CA 根证书或证书链内容/路径 */
18
+ ca?: string | Buffer | Array<string | Buffer>;
19
+ /** 私钥密码口令(若私钥被密码加密) */
20
+ passphrase?: string;
21
+ }
8
22
  export type CoreHttpServerFactory = (options: {
9
23
  port: number;
10
24
  host: string;
11
25
  fetch: (req: Request) => Promise<Response>;
26
+ tls?: ServerTlsOptions;
12
27
  }) => CoreHttpServerInstance | Promise<CoreHttpServerInstance>;
13
28
  /**
14
29
  * 启动 ActionDock HTTP Runner 服务端的配置选项。
@@ -56,6 +71,8 @@ export interface ServerOptions {
56
71
  enableManagement?: boolean;
57
72
  /** 是否扫描并加载外部链接包(默认在未指定 projectRoot 时为 true,指定时为 false) */
58
73
  scanLinkedPackages?: boolean;
74
+ /** 服务端 TLS/HTTPS 安全传输选项 */
75
+ tls?: ServerTlsOptions;
59
76
  }
60
77
  /**
61
78
  * 已启动的 ActionDock HTTP Runner 实例句柄。
@@ -11,6 +11,8 @@ export declare function streamRemoteEvents(serverUrl: string, runId: string, tok
11
11
  signal?: AbortSignal;
12
12
  maxQueueSize?: number;
13
13
  allowInsecureHttp?: boolean;
14
+ insecure?: boolean;
15
+ dispatcher?: unknown;
14
16
  }): AsyncIterable<ExecutionEvent>;
15
17
  /**
16
18
  * 远程 ActionDockTarget 门面实现。
@@ -22,7 +24,11 @@ export declare class RemoteActionDockTarget implements ActionDockTarget {
22
24
  readonly timeoutMs?: number;
23
25
  readonly baseTimeoutMs: number;
24
26
  readonly allowInsecureHttp?: boolean;
27
+ readonly insecure?: boolean;
28
+ readonly dispatcher?: unknown;
29
+ private isClosed;
25
30
  constructor(options: RemoteTargetOptions);
31
+ private assertNotClosed;
26
32
  info(): Promise<TargetInfo>;
27
33
  listPackages(): Promise<PackageInfo[]>;
28
34
  listActions(options?: ListActionsOptions): Promise<ActionSummary[]>;