@h-ai/deploy 1.0.0-beta.22 → 1.0.0-beta.23

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/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
2
- import { createReadStream, readFileSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, openSync, constants, fstatSync, readSync, closeSync, fchmodSync, writeFileSync, fsyncSync, renameSync, unlinkSync, chmodSync, mkdtempSync, copyFileSync, rmSync, createWriteStream, statSync } from 'fs';
2
+ import { createReadStream, readFileSync, existsSync, lstatSync, mkdirSync, readdirSync, openSync, constants, fstatSync, readSync, closeSync, fchmodSync, writeFileSync, fsyncSync, renameSync, unlinkSync, chmodSync, mkdtempSync, copyFileSync, rmSync, realpathSync, createWriteStream, statSync } from 'fs';
3
3
  import { resolve, extname, join, dirname, basename, relative, sep } from 'path';
4
4
  import { z } from 'zod';
5
5
  import { core, err, ok, CoreConfigSchema, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
6
- import { spawn, execFileSync } from 'child_process';
6
+ import { execFileSync, spawn } from 'child_process';
7
7
  import process from 'process';
8
8
  import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
9
9
  import { Buffer, constants as constants$1 } from 'buffer';
@@ -22,17 +22,14 @@ import { isIP } from 'net';
22
22
  // src/deploy-artifact.ts
23
23
  var DeploymentArtifactSchema = z.object({
24
24
  /** 清单格式版本,与应用版本和部署环境无关。 */
25
- version: z.union([
26
- z.literal(4),
27
- z.literal(5)
28
- ]),
25
+ version: z.literal(5),
29
26
  /** 主运行服务:独立后端为 serv,独立浏览器站点(静态或 SSR)为 web。 */
30
27
  primaryService: z.enum(["serv", "web"]),
31
28
  /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
32
29
  appVersion: z.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
33
30
  /** 本次构建标识;同一应用版本允许重复构建。 */
34
31
  buildId: z.string().max(128).regex(/^[a-z0-9][\w.+-]*$/i),
35
- /** 构建启动时观察到的 Git 来源;历史清单可以省略。 */
32
+ /** 构建启动时观察到的 Git 来源。 */
36
33
  source: z.object({
37
34
  /** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
38
35
  commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
@@ -43,8 +40,8 @@ var DeploymentArtifactSchema = z.object({
43
40
  partial: z.boolean().default(false),
44
41
  /** 是否包含浏览器端;仅构建 serv 时也保留完整工程的 Web 拓扑。 */
45
42
  hasWeb: z.boolean().default(false),
46
- /** Web构建的公开挂载目录,无末尾斜杠;省略或空字符串保持域名根目录。增量制品须保留完整拓扑的同一值。 */
47
- webBasePath: z.string().refine((value) => value === "" || /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === "..")).optional(),
43
+ /** Web 构建的公开挂载目录,无末尾斜杠;空字符串表示域名根目录。增量制品须保留完整拓扑的同一值。 */
44
+ webBasePath: z.string().refine((value) => value === "" || /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === "..")),
48
45
  /** 工程显式配置的后端端口,部分构建未包含 serv 时仍保留。 */
49
46
  servContainerPort: z.number().int().min(1).max(65535).optional(),
50
47
  /** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
@@ -59,12 +56,12 @@ var DeploymentArtifactSchema = z.object({
59
56
  imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
60
57
  /** tar 内容的 SHA-256,用于部署前校验。 */
61
58
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
62
- /** 引擎实际构建的镜像 OS/架构;历史清单可以省略。 */
59
+ /** 引擎实际构建的镜像 OS/架构。 */
63
60
  platform: z.string().regex(/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/).optional(),
64
61
  /** 容器内部监听端口;服务器公开端口由部署选择。 */
65
62
  containerPort: z.number().int().min(1).max(65535).optional()
66
63
  })).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
67
- }).refine((artifact) => artifact.version === 5 ? !!artifact.webBasePath : !artifact.webBasePath).refine((artifact) => !artifact.webBasePath || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.primaryService !== "web" || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.name === artifact.primaryService || image.name === "web" && artifact.hasWeb)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === artifact.primaryService)).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
64
+ }).refine((artifact) => !artifact.webBasePath || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.primaryService !== "web" || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.name === artifact.primaryService || image.name === "web" && artifact.hasWeb)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === artifact.primaryService)).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
68
65
  async function artifactChecksum(file) {
69
66
  const hash = createHash("sha256");
70
67
  for await (const chunk of createReadStream(file))
@@ -189,9 +186,7 @@ var en_US_default = {
189
186
  deploy_uploadPasswordCancelled: "Password input cancelled; no server connection was made.",
190
187
  deploy_uploadPasswordInvalid: "SSH passwords must contain 1\u20131023 UTF-8 bytes without line breaks.",
191
188
  deploy_managedSecretsInvalid: "Invalid generated middleware credential candidates. Rebuild the deployment bundle; server credentials will not be rotated automatically.",
192
- deploy_secretImportFailed: "Legacy credential import was rejected during {phase}; check private input permissions, project/volume identity, live credentials and the deployment lock. No services were updated; inspect the protected state file if commit is unconfirmed",
193
189
  deploy_projectNameRequired: "The root package.json.name is missing or cannot be normalized into an image and deployment resource identity.",
194
- deploy_projectNameOverrideRemoved: "Deployment identity comes from the root package.json.name; remove the obsolete projectName override.",
195
190
  deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle.",
196
191
  deploy_httpsPortsInvalid: "HTTP and HTTPS ports must differ; Let\u2019s Encrypt HTTP-01 requires host port 80.",
197
192
  deploy_httpsCertificateInvalid: "Invalid HTTPS certificate. Check readable PEM files, matching unencrypted private key, validity dates and domain, then repackage.",
@@ -334,9 +329,7 @@ var ja_JP_default = {
334
329
  deploy_uploadPasswordCancelled: "\u30D1\u30B9\u30EF\u30FC\u30C9\u5165\u529B\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u306F\u672A\u63A5\u7D9A\u3067\u3059\u3002",
335
330
  deploy_uploadPasswordInvalid: "SSH \u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u6539\u884C\u3092\u542B\u307E\u306A\u3044 1\u20131023 UTF-8 \u30D0\u30A4\u30C8\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
336
331
  deploy_managedSecretsInvalid: "\u81EA\u5EFA\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u306E\u5019\u88DC\u8A8D\u8A3C\u60C5\u5831\u304C\u7121\u52B9\u3067\u3059\u3002\u914D\u7F6E\u30D1\u30C3\u30B1\u30FC\u30B8\u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D0\u30FC\u306E\u8A8D\u8A3C\u60C5\u5831\u306F\u81EA\u52D5\u5909\u66F4\u3055\u308C\u307E\u305B\u3093\u3002",
337
- deploy_secretImportFailed: "\u65E7\u8A8D\u8A3C\u60C5\u5831\u306E\u30A4\u30F3\u30DD\u30FC\u30C8\u306F {phase} \u6BB5\u968E\u3067\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002\u5165\u529B\u6A29\u9650\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3068\u30DC\u30EA\u30E5\u30FC\u30E0\u3001\u5B9F\u969B\u306E\u8A8D\u8A3C\u60C5\u5831\u3001\u30C7\u30D7\u30ED\u30A4\u30ED\u30C3\u30AF\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D3\u30B9\u306F\u66F4\u65B0\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u78BA\u5B9A\u304C\u4E0D\u660E\u306A\u5834\u5408\u306F\u4FDD\u8B77\u3055\u308C\u305F\u72B6\u614B\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
338
332
  deploy_projectNameRequired: "\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304C\u306A\u3044\u304B\u6B63\u898F\u5316\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30A4\u30E1\u30FC\u30B8\u3068\u30C7\u30D7\u30ED\u30A4\u30EA\u30BD\u30FC\u30B9\u306E\u8B58\u5225\u540D\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002",
339
- deploy_projectNameOverrideRemoved: "\u30C7\u30D7\u30ED\u30A4\u8B58\u5225\u540D\u306F\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304B\u3089\u53D6\u5F97\u3057\u307E\u3059\u3002\u65E7 projectName \u4E0A\u66F8\u304D\u5F15\u6570\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
340
333
  deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
341
334
  deploy_httpsPortsInvalid: "HTTP \u3068 HTTPS \u306E\u30DD\u30FC\u30C8\u306F\u7570\u306A\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002Let\u2019s Encrypt HTTP-01 \u306B\u306F\u30DB\u30B9\u30C8\u306E\u30DD\u30FC\u30C8 80 \u304C\u5FC5\u8981\u3067\u3059\u3002",
342
335
  deploy_httpsCertificateInvalid: "HTTPS \u8A3C\u660E\u66F8\u304C\u7121\u52B9\u3067\u3059\u3002PEM \u30D5\u30A1\u30A4\u30EB\u3001\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u79D8\u5BC6\u9375\u3068\u306E\u4E00\u81F4\u3001\u6709\u52B9\u671F\u9593\u3001\u30C9\u30E1\u30A4\u30F3\u3092\u78BA\u8A8D\u3057\u3001\u518D\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
@@ -479,9 +472,7 @@ var zh_CN_default = {
479
472
  deploy_uploadPasswordCancelled: "\u5DF2\u53D6\u6D88\u5BC6\u7801\u8F93\u5165\uFF0C\u5C1A\u672A\u8FDE\u63A5\u670D\u52A1\u5668\u3002",
480
473
  deploy_uploadPasswordInvalid: "SSH \u5BC6\u7801\u987B\u4E3A 1\u20131023 \u4E2A UTF-8 \u5B57\u8282\uFF0C\u4E14\u4E0D\u80FD\u5305\u542B\u6362\u884C\u3002",
481
474
  deploy_managedSecretsInvalid: "\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u5019\u9009\u51ED\u636E\u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u90E8\u7F72\u5305\uFF1B\u670D\u52A1\u5668\u51ED\u636E\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u3002",
482
- deploy_secretImportFailed: "\u65E7\u51ED\u636E\u5BFC\u5165\u5728 {phase} \u9636\u6BB5\u88AB\u62D2\u7EDD\uFF1B\u8BF7\u6838\u5BF9\u79C1\u6709\u8F93\u5165\u6743\u9650\u3001\u9879\u76EE/\u5377\u8EAB\u4EFD\u3001\u5B9E\u9645\u670D\u52A1\u51ED\u636E\u53CA\u90E8\u7F72\u9501\uFF0C\u672A\u6267\u884C\u670D\u52A1\u66F4\u65B0\uFF1B\u63D0\u4EA4\u7ED3\u679C\u4E0D\u660E\u65F6\u8BF7\u6838\u5BF9\u53D7\u4FDD\u62A4\u7684\u72B6\u6001\u6587\u4EF6",
483
475
  deploy_projectNameRequired: "\u5DE5\u7A0B\u6839 package.json.name \u7F3A\u5931\u6216\u65E0\u6CD5\u5F52\u4E00\u5316\uFF0C\u4E0D\u80FD\u751F\u6210\u955C\u50CF\u4E0E\u90E8\u7F72\u8D44\u6E90\u8EAB\u4EFD\u3002",
484
- deploy_projectNameOverrideRemoved: "\u90E8\u7F72\u9879\u76EE\u540D\u7EDF\u4E00\u6765\u81EA\u5DE5\u7A0B\u6839 package.json.name\uFF1B\u8BF7\u79FB\u9664\u65E7 projectName \u8986\u76D6\u53C2\u6570\u3002",
485
476
  deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002",
486
477
  deploy_httpsPortsInvalid: "HTTP \u4E0E HTTPS \u7AEF\u53E3\u5FC5\u987B\u4E0D\u540C\uFF1BLet\u2019s Encrypt \u7684 HTTP-01 \u5165\u53E3\u5FC5\u987B\u4F7F\u7528\u5BBF\u4E3B\u673A 80 \u7AEF\u53E3\u3002",
487
478
  deploy_httpsCertificateInvalid: "HTTPS \u8BC1\u4E66\u65E0\u6548\uFF1A\u8BF7\u6838\u5BF9 PEM \u6587\u4EF6\u53EF\u8BFB\u3001\u8BC1\u4E66\u4E0E\u672A\u52A0\u5BC6\u79C1\u94A5\u5339\u914D\u3001\u8BC1\u4E66\u6709\u6548\u671F\u53CA\u57DF\u540D\uFF0C\u7136\u540E\u91CD\u65B0\u6253\u5305\u3002",
@@ -6261,9 +6252,9 @@ function existingFile(path) {
6261
6252
  throw error;
6262
6253
  }
6263
6254
  }
6264
- function writeLocalFile(path, content, privateFile2 = false) {
6255
+ function writeLocalFile(path, content, privateFile = false) {
6265
6256
  const previous = existingFile(path);
6266
- const mode = privateFile2 ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
6257
+ const mode = privateFile ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
6267
6258
  const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
6268
6259
  let fd;
6269
6260
  let owned;
@@ -6470,7 +6461,7 @@ function isSecret(name, value) {
6470
6461
  }
6471
6462
  function generatedVariables(values) {
6472
6463
  const generated = {};
6473
- const groups3 = /* @__PURE__ */ new Map();
6464
+ const groups2 = /* @__PURE__ */ new Map();
6474
6465
  const structures = /* @__PURE__ */ new Set();
6475
6466
  for (const [name, value] of Object.entries(values)) {
6476
6467
  if (value === void 0)
@@ -6482,9 +6473,9 @@ function generatedVariables(values) {
6482
6473
  const module = /^HAI_([A-Z0-9]+)(?:_|$)/.exec(name)?.[1]?.toLowerCase();
6483
6474
  if (!module || !shapes[module])
6484
6475
  continue;
6485
- const group = groups3.get(module) ?? {};
6476
+ const group = groups2.get(module) ?? {};
6486
6477
  group[name] = text;
6487
- groups3.set(module, group);
6478
+ groups2.set(module, group);
6488
6479
  try {
6489
6480
  const parsed = JSON.parse(text);
6490
6481
  if (parsed !== null && typeof parsed === "object")
@@ -6492,7 +6483,7 @@ function generatedVariables(values) {
6492
6483
  } catch {
6493
6484
  }
6494
6485
  }
6495
- for (const [module, group] of groups3) {
6486
+ for (const [module, group] of groups2) {
6496
6487
  const structuredNames = Object.keys(group).filter((name) => structures.has(name));
6497
6488
  if (!structuredNames.length)
6498
6489
  continue;
@@ -6539,7 +6530,7 @@ function generatedVariables(values) {
6539
6530
  }
6540
6531
  return generated;
6541
6532
  }
6542
- function updateFile(path, values, privateFile2) {
6533
+ function updateFile(path, values, privateFile) {
6543
6534
  let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
6544
6535
  for (const [name, value] of Object.entries(values)) {
6545
6536
  if (!/^[A-Z][A-Z0-9_]*$/.test(name))
@@ -6563,15 +6554,15 @@ ${line}
6563
6554
  content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
6564
6555
  }
6565
6556
  }
6566
- writeLocalFile(path, content, privateFile2);
6557
+ writeLocalFile(path, content, privateFile);
6567
6558
  }
6568
- function removeOverrides(path, names, privateFile2 = true) {
6559
+ function removeOverrides(path, names, privateFile = true) {
6569
6560
  if (!existsSync(path) || names.length === 0)
6570
6561
  return;
6571
6562
  let content = readLocalFile(path).toString("utf8");
6572
6563
  for (const name of names)
6573
6564
  content = content.replace(new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
6574
- writeLocalFile(path, content, privateFile2);
6565
+ writeLocalFile(path, content, privateFile);
6575
6566
  }
6576
6567
  function saveDeploymentEnvironment(environment, values, options = {}) {
6577
6568
  try {
@@ -7134,47 +7125,6 @@ async function scanApp(inputDir, options) {
7134
7125
  }
7135
7126
  }
7136
7127
 
7137
- // src/container/deploy-compose-identity.ts
7138
- function createComposeContainerLookupScript() {
7139
- const labelFormat = [
7140
- "com.docker.compose.project",
7141
- "com.docker.compose.service",
7142
- "io.podman.compose.project",
7143
- "io.podman.compose.service"
7144
- ].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
7145
- return String.raw`compose_container_ids() (
7146
- if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
7147
- lookup_runtime=$1
7148
- lookup_project=$2
7149
- lookup_service=$3
7150
- lookup_selector=$4
7151
- case "$lookup_runtime" in docker|podman) ;; *) printf 'Invalid Compose lookup runtime.\n' >&2; exit 1;; esac
7152
- case "$lookup_project" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose project identity.\n' >&2; exit 1;; esac
7153
- case "$lookup_service" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose service identity.\n' >&2; exit 1;; esac
7154
- case "$lookup_selector" in -q|-aq) ;; *) printf 'Invalid Compose container selector.\n' >&2; exit 1;; esac
7155
- lookup_ids=''
7156
- for lookup_namespace in com.docker.compose io.podman.compose; do
7157
- lookup_found=$("$lookup_runtime" ps "$lookup_selector" --filter "label=$lookup_namespace.project=$lookup_project" --filter "label=$lookup_namespace.service=$lookup_service") || { printf 'Cannot query Compose containers.\n' >&2; exit 1; }
7158
- printf '%s\n' "$lookup_found" | LC_ALL=C awk 'NF {if(NF!=1 || $0!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1}' || { printf 'Invalid Compose container identifiers.\n' >&2; exit 1; }
7159
- lookup_ids=$(printf '%s\n%s\n' "$lookup_ids" "$lookup_found" | LC_ALL=C awk 'NF && !seen[$0]++ {print}') || exit 1
7160
- done
7161
- for lookup_id in $lookup_ids; do
7162
- lookup_labels=$("$lookup_runtime" inspect --format '${labelFormat}' "$lookup_id") || { printf 'Cannot inspect Compose container labels.\n' >&2; exit 1; }
7163
- printf '%s\n' "$lookup_labels" | LC_ALL=C awk -F'|' -v project="$lookup_project" -v service="$lookup_service" '
7164
- NR!=1 || NF!=4 {exit 1}
7165
- {
7166
- docker=($1!="" && $1!="<no value>") || ($2!="" && $2!="<no value>")
7167
- podman=($3!="" && $3!="<no value>") || ($4!="" && $4!="<no value>")
7168
- if((!docker && !podman) || (docker && ($1!=project || $2!=service)) || (podman && ($3!=project || $4!=service))) exit 1
7169
- }
7170
- END {if(NR!=1) exit 1}
7171
- ' || { printf 'Container labels do not confirm the requested Compose project and service.\n' >&2; exit 1; }
7172
- done
7173
- if [ -n "$lookup_ids" ]; then printf '%s\n' "$lookup_ids"; fi
7174
- )
7175
- `;
7176
- }
7177
-
7178
7128
  // src/container/deploy-json.ts
7179
7129
  function createDeploymentJsonTool() {
7180
7130
  return String.raw`function fail(message) {
@@ -7499,7 +7449,7 @@ deployment_metadata_init() {
7499
7449
  deployment_metadata_file() {
7500
7450
  metadata_directory=$1
7501
7451
  metadata_name=$2
7502
- case "$metadata_name" in layout|health-paths|image-list|dependencies|managed-project|managed-bindings|managed-volumes|managed-app-keys|unmanaged-app-keys|state-backends|storage-routing|web-base-path|node-runtime|proxy-service|https) ;; *) printf 'Unknown deployment metadata field.\n' >&2; return 1;; esac
7452
+ case "$metadata_name" in layout|health-paths|image-list|dependencies|managed-bindings|managed-volumes|managed-app-keys|unmanaged-app-keys|state-backends|storage-routing|web-base-path|node-runtime|proxy-service|https) ;; *) printf 'Unknown deployment metadata field.\n' >&2; return 1;; esac
7503
7453
  test -f "$metadata_directory/manifest.json" && test ! -L "$metadata_directory/manifest.json" || return 1
7504
7454
  metadata_identity=$(printf '%s' "$metadata_directory" | sha256sum | awk '{print $1}')
7505
7455
  metadata_cached="$metadata_work/$metadata_identity-$metadata_name"
@@ -7596,8 +7546,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7596
7546
  }
7597
7547
  }
7598
7548
  return {
7599
- "managed-project": `hai-${projectName}
7600
- `,
7601
7549
  "managed-candidates.env": candidates.length ? `${candidates.join("\n")}
7602
7550
  ` : "",
7603
7551
  "managed-bindings": bindings.size ? `${[...bindings].join("\n")}
@@ -7607,29 +7555,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7607
7555
  "resolve-managed-secrets.sh": createManagedSecretScript()
7608
7556
  };
7609
7557
  }
7610
- function createManagedSecretValidationScript() {
7611
- return String.raw`valid_values() {
7612
- version=$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$1")
7613
- if [ "$version" = 2 ]; then
7614
- LC_ALL=C awk -F= '
7615
- NF {
7616
- if(NF!=2 || seen[$1]++) exit 1
7617
- if($1=="FORMAT_VERSION") {if($2!="2") exit 1; next}
7618
- if($1=="PROJECT") {if($2!~/^[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; project=1; next}
7619
- if($1~/^VOLUME_[0-9]+$/) {if($2!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; next}
7620
- if($1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[0-9a-f]+$/ || length($2)%2 || length($2)>8192) exit 1
7621
- for(i=1;i<=length($2);i+=2) {b=substr($2,i,2); if(b~/^[01]/ || b=="7f") exit 1}
7622
- }
7623
- END {if(!project) exit 1}
7624
- ' "$1"
7625
- return
7626
- fi
7627
- LC_ALL=C awk -F= '
7628
- NF {if(NF!=2 || $1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[A-Za-z0-9_-]+$/ || length($2)!=32 || seen[$1]++) exit 1}
7629
- ' "$1"
7630
- }
7631
- `;
7632
- }
7633
7558
  function createManagedSecretScript() {
7634
7559
  return String.raw`#!/bin/sh
7635
7560
  # 由 apply.sh 在项目部署锁内调用;长期凭据只保存在服务器,不依赖打包机。
@@ -7644,12 +7569,15 @@ test -d "$root" && test ! -L "$root" && test -d "$release/.hai" && test ! -L "$r
7644
7569
  @@MANIFEST_FUNCTIONS@@
7645
7570
  deployment_metadata_init
7646
7571
  trap deployment_metadata_cleanup EXIT
7647
- managed_project=$(deployment_metadata_file "$release" managed-project)
7648
7572
  managed_bindings=$(deployment_metadata_file "$release" managed-bindings)
7649
7573
  managed_volumes=$(deployment_metadata_file "$release" managed-volumes)
7650
7574
  test -s "$candidate_file" || exit 0
7651
7575
  fail() { printf '%s\n' "$*" >&2; exit 1; }
7652
- @@MANAGED_VALUES_VALIDATOR@@
7576
+ valid_values() {
7577
+ LC_ALL=C awk -F= '
7578
+ NF {if(NF!=2 || $1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[A-Za-z0-9_-]+$/ || length($2)!=32 || seen[$1]++) exit 1}
7579
+ ' "$1"
7580
+ }
7653
7581
  value_of() { awk -F= -v key="$2" '$1==key {print $2}' "$1"; }
7654
7582
  test ! -L "$state" || fail "Invalid managed middleware credential state: $state"
7655
7583
  valid_values "$candidate_file" || fail 'Invalid managed middleware credential candidates; rebuild the deployment bundle.'
@@ -7660,22 +7588,6 @@ if [ -f "$state" ]; then
7660
7588
  else
7661
7589
  : > "$next"
7662
7590
  fi
7663
- format=$(value_of "$next" FORMAT_VERSION)
7664
- if [ "$format" = 2 ]; then
7665
- test "$(value_of "$next" PROJECT)" = "$(cat "$managed_project")" || fail 'Managed middleware credential project mismatch.'
7666
- known=$("$runtime" volume ls -q) || fail 'Cannot check persistent volumes; managed middleware credentials were not changed.'
7667
- while IFS=' ' read -r volume_group volume; do
7668
- test -n "$volume" || continue
7669
- binding="$volume_group:$volume"
7670
- if ! awk -F= -v binding="$binding" '$1~/^VOLUME_[0-9]+$/ && $2==binding {found=1} END {exit !found}' "$next"; then
7671
- if printf '%s\n' "$known" | grep -Fxq "$volume"; then
7672
- fail 'Existing persistent volume is not bound to the imported credential state. Import its verified identity first.'
7673
- fi
7674
- index=$(awk -F= '$1~/^VOLUME_[0-9]+$/ {n=substr($1,8)+0; if(n>max)max=n} END {print max+1}' "$next")
7675
- printf 'VOLUME_%s=%s\n' "$index" "$binding" >> "$next"
7676
- fi
7677
- done < "$managed_volumes"
7678
- fi
7679
7591
  volumes_checked=false
7680
7592
  known_volumes=''
7681
7593
  while IFS='=' read -r name candidate; do
@@ -7692,64 +7604,24 @@ while IFS='=' read -r name candidate; do
7692
7604
  fail "Missing $name in $state for existing persistent volume $volume. Restore the server credential state before retrying."
7693
7605
  fi
7694
7606
  done < "$managed_volumes"
7695
- value=$candidate
7696
- if [ "$format" = 2 ]; then value=$(printf '%s' "$candidate" | od -An -tx1 | tr -d ' \n'); fi
7697
- printf '%s=%s\n' "$name" "$value" >> "$next"
7607
+ printf '%s=%s\n' "$name" "$candidate" >> "$next"
7698
7608
  fi
7699
7609
  done < "$candidate_file"
7700
7610
  while IFS='=' read -r name candidate; do
7701
7611
  test -n "$name" || continue
7702
7612
  value=$(value_of "$next" "$name")
7703
7613
  for file in config/.env.runtime .hai/middleware-runtime.env; do
7704
- compose_environment=false
7705
- if [ "$file" = .hai/middleware-runtime.env ]; then compose_environment=true; fi
7706
- HAI_DEPLOY_SECRET_CANDIDATE="$candidate" HAI_DEPLOY_SECRET_VALUE="$value" LC_ALL=C awk -v group="$name" -v format="$format" -v compose_environment="$compose_environment" '
7614
+ HAI_DEPLOY_SECRET_CANDIDATE="$candidate" HAI_DEPLOY_SECRET_VALUE="$value" LC_ALL=C awk -v group="$name" '
7707
7615
  FILENAME==ARGV[1] {if($1==group) {keys[$2]=$3}; next}
7708
7616
  function replace_literal(line, needle, value, result, at) {
7709
7617
  result=""
7710
7618
  while((at=index(line,needle))>0) {result=result substr(line,1,at-1) value; line=substr(line,at+length(needle))}
7711
7619
  return result line
7712
7620
  }
7713
- function decode_hex(hex, result, i, high, low) {
7714
- for(i=1;i<=length(hex);i+=2) {high=index("0123456789abcdef",substr(hex,i,1))-1; low=index("0123456789abcdef",substr(hex,i+1,1))-1; result=result sprintf("%c",high*16+low)}
7715
- return result
7716
- }
7717
- function json_escape(value, apostrophe, result, i, ch) {
7718
- for(i=1;i<=length(value);i++) {
7719
- ch=substr(value,i,1)
7720
- if(ch=="\\") result=result "\\\\"
7721
- else if(ch=="\"") result=result "\\\""
7722
- else if(ch==sprintf("%c",39) && apostrophe) result=result "\\u0027"
7723
- else result=result ch
7724
- }
7725
- return result
7726
- }
7727
- function uri_escape(value, result, i, ch, j, ordinal) {
7728
- for(i=1;i<=length(value);i++) {
7729
- ch=substr(value,i,1)
7730
- if(ch~/[A-Za-z0-9_.~-]/) result=result ch
7731
- else {ordinal=0; for(j=1;j<=255;j++) if(sprintf("%c",j)==ch) {ordinal=j; break}; result=result sprintf("%%%02X",ordinal)}
7732
- }
7733
- return result
7734
- }
7735
7621
  {
7736
7622
  equal=index($0,"="); key=substr($0,1,equal-1); scoped=0
7737
7623
  for(prefix in keys) if(key==prefix || (keys[prefix]=="prefix" && index(key,prefix "_")==1)) scoped=1
7738
- if(scoped && format==2 && index($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"])>0) {
7739
- candidate=ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"]; value=decode_hex(ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
7740
- literal=substr($0,equal+1); quote=sprintf("%c",39)
7741
- json=substr(literal,1,1)==quote && (substr(literal,2,1)=="{" || substr(literal,2,1)=="[")
7742
- if(json) literal=substr(literal,2,length(literal)-2)
7743
- literal=replace_literal(literal,":" candidate "@",":" uri_escape(value) "@")
7744
- if(json) {literal=replace_literal(literal,candidate,json_escape(value,1)); print key "=" quote literal quote}
7745
- else {
7746
- if(substr(literal,1,1)==quote || substr(literal,1,1)=="\"") literal=substr(literal,2,length(literal)-2)
7747
- literal=replace_literal(literal,candidate,value)
7748
- literal=json_escape(literal,0); if(compose_environment=="true") literal=replace_literal(literal,"$","$")
7749
- print key "=\"" literal "\""
7750
- }
7751
- }
7752
- else if(scoped) print replace_literal($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"],ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
7624
+ if(scoped) print replace_literal($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"],ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
7753
7625
  else print
7754
7626
  }
7755
7627
  ' "$managed_bindings" "$release/$file" > "$release/$file.next"
@@ -7759,7 +7631,7 @@ done < "$candidate_file"
7759
7631
  chmod 600 "$next"
7760
7632
  mv "$next" "$state"
7761
7633
  printf '[INFO] Managed middleware credential source: %s (server state; values hidden).\n' "$state"
7762
- `.replace("@@MANAGED_VALUES_VALIDATOR@@", () => createManagedSecretValidationScript()).replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
7634
+ `.replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
7763
7635
  }
7764
7636
 
7765
7637
  // src/container/deploy-backend-state.ts
@@ -7822,11 +7694,11 @@ backend_manifest=$(deployment_metadata_file "$release" state-backends)
7822
7694
  regular() { test -f "$1" && test ! -L "$1" || fail "Backend state must be a regular file: $1"; }
7823
7695
  absent_or_regular() { if [ -e "$1" ] || [ -L "$1" ]; then regular "$1"; fi; }
7824
7696
  valid_tuples() {
7825
- LC_ALL=C awk -v legacy="$2" '
7697
+ LC_ALL=C awk '
7826
7698
  NF {
7827
- if((NF!=3 && !(legacy==1 && NF==2)) || roles[$1]++) exit 1
7699
+ if(NF!=3 || roles[$1]++) exit 1
7828
7700
  if(!(($1=="db" && $2~/^(postgresql|mysql)$/) || ($1=="storage" && $2~/^(rustfs|minio)$/) || ($1=="vecdb" && $2~/^(qdrant|pgvector|chroma)$/))) exit 1
7829
- if(NF==3 && ($3!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || volumes[$3]++)) exit 1
7701
+ if($3!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || volumes[$3]++) exit 1
7830
7702
  }
7831
7703
  ' "$1"
7832
7704
  }
@@ -7877,45 +7749,6 @@ known=$("$runtime" volume ls -q) || fail 'Cannot list persistent volumes; no bac
7877
7749
  printf '%s\n' "$known" | LC_ALL=C awk 'NF {if(NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || seen[$1]++) exit 1}' || fail 'Invalid persistent volume listing.'
7878
7750
  exists_volume() { printf '%s\n' "$known" | grep -Fxq "$1"; }
7879
7751
  ledger_tuple() { printf '%s\n' "$ledger" | awk -v volume="$1" '$3==volume {print}'; }
7880
- @@MANAGED_VALUES_VALIDATOR@@
7881
- @@COMPOSE_CONTAINER_LOOKUP@@
7882
- # 状态补证:卷挂载和镜像均取自同项目、同角色的唯一实际容器。
7883
- container_binding() {
7884
- binding_service=$1
7885
- binding_family=$2
7886
- binding_expected=$3
7887
- binding_running=$4
7888
- binding_flags=-aq
7889
- if [ "$binding_running" = true ]; then binding_flags=-q; fi
7890
- binding_containers=$(compose_container_ids "$runtime" "$project" "$binding_service" "$binding_flags") || fail 'Cannot inspect successful deployment containers.'
7891
- printf '%s\n' "$binding_containers" | LC_ALL=C awk 'NR!=1 || NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ {exit 1} END {if(NR!=1) exit 1}' || fail "Cannot prove the previous volume binding for $binding_service. Restore verified backend state."
7892
- if [ "$binding_running" = true ]; then
7893
- binding_status=$("$runtime" inspect --format '{{.State.Running}}' "$binding_containers") || fail 'Cannot verify the imported service is running.'
7894
- test "$binding_status" = true || fail "Imported backend requires its verified original container to be running: $binding_service."
7895
- fi
7896
- binding_mounted=$("$runtime" inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{println .Name}}{{end}}{{end}}' "$binding_containers") || fail 'Cannot inspect previous named volume mounts.'
7897
- printf '%s\n' "$binding_mounted" | LC_ALL=C awk 'NR!=1 || NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ {exit 1} END {if(NR!=1) exit 1}' || fail "Ambiguous previous volume binding for $binding_service."
7898
- binding_image=$("$runtime" inspect --format '{{.Config.Image}}' "$binding_containers") || fail 'Cannot identify the previous container image.'
7899
- binding_actual_family=$(printf '%s\n' "$binding_image" | LC_ALL=C awk 'NR!=1 || NF!=1 {exit 1} {image=$0; sub(/^.*\//,"",image); sub(/[:@].*$/,"",image); if(image=="postgres") image="postgresql"; print image}') || fail 'Invalid previous container image identity.'
7900
- test "$binding_actual_family" = "$binding_family" || fail "Actual previous container image does not confirm its backend family: $binding_service."
7901
- if [ -n "$binding_expected" ] && [ "$binding_expected" != "$binding_mounted" ]; then fail "Actual previous volume differs from its backend manifest: $binding_service."; fi
7902
- printf '%s\n' "$binding_mounted"
7903
- }
7904
- confirmed_import() {
7905
- import_service=$1
7906
- import_family=$2
7907
- import_volume=$3
7908
- import_state="$root/managed-secrets.env"
7909
- test -f "$import_state" && test ! -L "$import_state" || return 1
7910
- valid_values "$import_state" || return 1
7911
- test "$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$import_state")" = 2 || return 1
7912
- test "$(awk -F= '$1=="PROJECT" {print $2}' "$import_state")" = "$project" || return 1
7913
- case "$import_service" in db|vecdb) import_group=DB_PASSWORD;; storage) import_group=STORAGE_PASSWORD;; *) return 1;; esac
7914
- awk -F= -v group="$import_group" -v binding="$import_group:$import_volume" '$1==group && $2!="" {credential=1} $1~/^VOLUME_[0-9]+$/ && $2==binding {volume=1} END {exit !(credential && volume)}' "$import_state" || return 1
7915
- # 密码不证明目录格式;受控导入还须与唯一运行原容器的实际卷及镜像家族共同吻合。
7916
- import_actual=$(container_binding "$import_service" "$import_family" "$import_volume" true) || return 1
7917
- test "$import_actual" = "$import_volume"
7918
- }
7919
7752
  snapshot_previous=$previous
7920
7753
  if [ -z "$snapshot_previous" ]; then snapshot_previous=-; fi
7921
7754
  if [ "$mode" = recover ]; then
@@ -7933,7 +7766,7 @@ RECOVERY_TUPLES
7933
7766
  exit 0
7934
7767
  fi
7935
7768
  regular "$backend_manifest"
7936
- valid_tuples "$backend_manifest" 0 || fail 'Invalid backend manifest; rebuild the deployment bundle.'
7769
+ valid_tuples "$backend_manifest" || fail 'Invalid backend manifest; rebuild the deployment bundle.'
7937
7770
  active=''
7938
7771
  if [ -n "$previous" ]; then
7939
7772
  baseline="$root/releases/$previous"
@@ -7942,7 +7775,7 @@ if [ -n "$previous" ]; then
7942
7775
  baseline_services=$(deployment_services_file "$baseline")
7943
7776
  previous_manifest=$(deployment_metadata_file "$baseline" state-backends)
7944
7777
  regular "$previous_manifest"
7945
- valid_tuples "$previous_manifest" 0 || fail 'Invalid successful backend manifest.'
7778
+ valid_tuples "$previous_manifest" || fail 'Invalid successful backend manifest.'
7946
7779
  LC_ALL=C awk 'NF {if(NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || seen[$1]++) exit 1}' "$baseline_services" || fail 'Invalid successful deployment services.'
7947
7780
  for active_service in db storage vecdb; do
7948
7781
  if ! grep -Fxq "$active_service" "$baseline_services"; then continue; fi
@@ -7955,28 +7788,18 @@ if [ -n "$previous" ]; then
7955
7788
  printf '%s %s %s\n' "$active_service" "$active_family" "$active_volume" | LC_ALL=C awk 'NF!=3 || !(($1=="db" && $2~/^(postgresql|mysql)$/) || ($1=="storage" && $2~/^(rustfs|minio)$/) || ($1=="vecdb" && $2~/^(qdrant|pgvector|chroma)$/)) {exit 1}' || fail "Unknown installed backend for $active_service."
7956
7789
  exists_volume "$active_volume" || fail "Active persistent volume is missing: $active_volume. Restore its backup instead of creating an empty replacement."
7957
7790
  confirmed=$(ledger_tuple "$active_volume")
7958
- test -z "$confirmed" || test "$confirmed" = "$active_service $active_family $active_volume" || fail "Active persistent backend conflicts with the protected ledger: $active_service."
7959
- if [ -z "$confirmed" ]; then
7960
- proven_volume=$(container_binding "$active_service" "$active_family" "$active_volume" false)
7961
- test "$proven_volume" = "$active_volume" || fail "Cannot confirm the active persistent volume: $active_service."
7962
- fi
7791
+ test "$confirmed" = "$active_service $active_family $active_volume" || fail "Active persistent backend is missing from or conflicts with the protected ledger: $active_service. Restore verified backend state."
7963
7792
  active=$(printf '%s\n%s %s %s\n' "$active" "$active_service" "$active_family" "$active_volume" | sed '/^$/d')
7964
7793
  done
7965
7794
  fi
7966
- # 各个旧 Compose 片段不能把同一实际卷重复绑定给不同有状态角色。
7967
- printf '%s\n' "$active" | valid_tuples /dev/stdin 0 || fail 'Conflicting successful persistent volume bindings.'
7795
+ # 同一实际卷不能绑定给多个有状态服务。
7796
+ printf '%s\n' "$active" | valid_tuples /dev/stdin || fail 'Conflicting successful persistent volume bindings.'
7968
7797
  # 合并时只替换同一卷的无数据预认领,不能按本次拓扑裁剪历史绑定。
7969
7798
  merged=$ledger
7970
7799
  merge_tuple() {
7971
7800
  merged=$(printf '%s\n' "$merged" | awk -v volume="$3" 'NF && $3!=volume {print}')
7972
7801
  merged=$(printf '%s\n%s %s %s\n' "$merged" "$1" "$2" "$3" | sed '/^$/d')
7973
7802
  }
7974
- while IFS=' ' read -r active_service active_family active_volume; do
7975
- test -n "$active_service" || continue
7976
- merge_tuple "$active_service" "$active_family" "$active_volume"
7977
- done <<ACTIVE_TUPLES
7978
- $active
7979
- ACTIVE_TUPLES
7980
7803
  while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
7981
7804
  test -n "$candidate_service" || continue
7982
7805
  prior_active=$(printf '%s\n' "$active" | awk -v service="$candidate_service" '$1==service {print}')
@@ -7984,8 +7807,6 @@ while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
7984
7807
  test -z "$prior_active" || test "$prior_active" = "$tuple" || fail "Persistent backend $candidate_service changed or its active volume binding changed. Use a separate deployment identity and migrate or restore data explicitly."
7985
7808
  if exists_volume "$candidate_volume"; then
7986
7809
  confirmed=$(ledger_tuple "$candidate_volume")
7987
- if [ -z "$confirmed" ]; then confirmed=$(printf '%s\n' "$active" | awk -v volume="$candidate_volume" '$3==volume {print}'); fi
7988
- if [ -z "$confirmed" ] && confirmed_import "$candidate_service" "$candidate_family" "$candidate_volume"; then confirmed=$tuple; fi
7989
7810
  test "$confirmed" = "$tuple" || fail "Existing persistent volume has an unknown or incompatible backend: $candidate_service. Restore verified backend state; in-place reuse is refused."
7990
7811
  else
7991
7812
  users=$("$runtime" ps -aq --filter "volume=$candidate_volume") || fail 'Cannot check persistent volume users.'
@@ -8005,7 +7826,7 @@ mv -f -- "$snapshot.next" "$snapshot"
8005
7826
  (set -C; printf '%s\n' "$state_content" > "$state.next") || fail 'Cannot stage the persistent backend ledger.'
8006
7827
  chmod 600 "$state.next"
8007
7828
  mv -f -- "$state.next" "$state"
8008
- `.replace("@@PROJECT@@", () => projectName).replace("@@MANAGED_VALUES_VALIDATOR@@", () => createManagedSecretValidationScript()).replace("@@COMPOSE_CONTAINER_LOOKUP@@", () => createComposeContainerLookupScript()).replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
7829
+ `.replace("@@PROJECT@@", () => projectName).replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
8009
7830
  }
8010
7831
  var nativeJob = String.raw`
8011
7832
  using System;
@@ -8204,7 +8025,7 @@ async function groupExited(pid) {
8204
8025
  async function helper(command, args, timeoutMs) {
8205
8026
  if (timeoutMs <= 0)
8206
8027
  return false;
8207
- return new Promise((resolve9) => {
8028
+ return new Promise((resolve8) => {
8208
8029
  const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
8209
8030
  let settled = false;
8210
8031
  let timer;
@@ -8214,7 +8035,7 @@ async function helper(command, args, timeoutMs) {
8214
8035
  settled = true;
8215
8036
  if (timer)
8216
8037
  clearTimeout(timer);
8217
- resolve9(success);
8038
+ resolve8(success);
8218
8039
  };
8219
8040
  timer = globalThis.setTimeout(() => {
8220
8041
  child.kill("SIGKILL");
@@ -8367,7 +8188,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8367
8188
  ext: { commandExecution: state }
8368
8189
  });
8369
8190
  try {
8370
- return await new Promise((resolve9) => {
8191
+ return await new Promise((resolve8) => {
8371
8192
  if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
8372
8193
  throw new TypeError("Invalid command arguments");
8373
8194
  if (options.input !== void 0 && typeof options.input !== "string")
@@ -8410,7 +8231,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8410
8231
  settled = true;
8411
8232
  if (timer)
8412
8233
  clearTimeout(timer);
8413
- resolve9(result);
8234
+ resolve8(result);
8414
8235
  };
8415
8236
  const interrupt = (reason, message) => {
8416
8237
  interrupted = true;
@@ -8597,6 +8418,47 @@ watch_certificate &
8597
8418
  `;
8598
8419
  }
8599
8420
 
8421
+ // src/container/deploy-compose-identity.ts
8422
+ function createComposeContainerLookupScript() {
8423
+ const labelFormat = [
8424
+ "com.docker.compose.project",
8425
+ "com.docker.compose.service",
8426
+ "io.podman.compose.project",
8427
+ "io.podman.compose.service"
8428
+ ].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
8429
+ return String.raw`compose_container_ids() (
8430
+ if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
8431
+ lookup_runtime=$1
8432
+ lookup_project=$2
8433
+ lookup_service=$3
8434
+ lookup_selector=$4
8435
+ case "$lookup_runtime" in docker|podman) ;; *) printf 'Invalid Compose lookup runtime.\n' >&2; exit 1;; esac
8436
+ case "$lookup_project" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose project identity.\n' >&2; exit 1;; esac
8437
+ case "$lookup_service" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose service identity.\n' >&2; exit 1;; esac
8438
+ case "$lookup_selector" in -q|-aq) ;; *) printf 'Invalid Compose container selector.\n' >&2; exit 1;; esac
8439
+ lookup_ids=''
8440
+ for lookup_namespace in com.docker.compose io.podman.compose; do
8441
+ lookup_found=$("$lookup_runtime" ps "$lookup_selector" --filter "label=$lookup_namespace.project=$lookup_project" --filter "label=$lookup_namespace.service=$lookup_service") || { printf 'Cannot query Compose containers.\n' >&2; exit 1; }
8442
+ printf '%s\n' "$lookup_found" | LC_ALL=C awk 'NF {if(NF!=1 || $0!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1}' || { printf 'Invalid Compose container identifiers.\n' >&2; exit 1; }
8443
+ lookup_ids=$(printf '%s\n%s\n' "$lookup_ids" "$lookup_found" | LC_ALL=C awk 'NF && !seen[$0]++ {print}') || exit 1
8444
+ done
8445
+ for lookup_id in $lookup_ids; do
8446
+ lookup_labels=$("$lookup_runtime" inspect --format '${labelFormat}' "$lookup_id") || { printf 'Cannot inspect Compose container labels.\n' >&2; exit 1; }
8447
+ printf '%s\n' "$lookup_labels" | LC_ALL=C awk -F'|' -v project="$lookup_project" -v service="$lookup_service" '
8448
+ NR!=1 || NF!=4 {exit 1}
8449
+ {
8450
+ docker=($1!="" && $1!="<no value>") || ($2!="" && $2!="<no value>")
8451
+ podman=($3!="" && $3!="<no value>") || ($4!="" && $4!="<no value>")
8452
+ if((!docker && !podman) || (docker && ($1!=project || $2!=service)) || (podman && ($3!=project || $4!=service))) exit 1
8453
+ }
8454
+ END {if(NR!=1) exit 1}
8455
+ ' || { printf 'Container labels do not confirm the requested Compose project and service.\n' >&2; exit 1; }
8456
+ done
8457
+ if [ -n "$lookup_ids" ]; then printf '%s\n' "$lookup_ids"; fi
8458
+ )
8459
+ `;
8460
+ }
8461
+
8600
8462
  // src/container/deploy-runtime-script.ts
8601
8463
  function createRuntimeConfigurationScript(primaryService) {
8602
8464
  return String.raw`configuration_mount() {
@@ -9256,7 +9118,7 @@ if [ "$partial" = true ]; then
9256
9118
  fi
9257
9119
  ;;
9258
9120
  esac
9259
- # 目录属于完整Web拓扑;旧版本缺省为根目录,服务增量不能意外丢失或改变既有目录。
9121
+ # Web 挂载目录由完整拓扑确定,服务增量必须匹配已部署的目录。
9260
9122
  current_web_base=$(deployment_json read metadata.web-base-path string "$source_dir/manifest.json")
9261
9123
  previous_web_base=$(deployment_json read metadata.web-base-path string "$root/releases/$previous/manifest.json")
9262
9124
  test "$current_web_base" = "$previous_web_base" || { printf 'Web base path changed. Match the installed prefix for partial updates, or create a full deployment.\n' >&2; exit 1; }
@@ -10152,7 +10014,7 @@ async function createDeploymentBundle(options) {
10152
10014
  const metadata = {
10153
10015
  "layout": `${config.expose.hostPort}:${options.containerPort}:${options.hasWebConfig ? options.proxyService === "gateway" ? "gateway" : "true" : "false"}:${artifact.primaryService}:${options.hasWebConfig ? await artifactChecksum(join(directory, "web.conf")) : ""}${httpsFingerprint ? `:${httpsFingerprint}` : ""}
10154
10016
  `,
10155
- "web-base-path": `${artifact.webBasePath ?? ""}
10017
+ "web-base-path": `${artifact.webBasePath}
10156
10018
  `,
10157
10019
  "health-paths": `${options.healthPaths.join("\n")}
10158
10020
  `,
@@ -10165,7 +10027,6 @@ async function createDeploymentBundle(options) {
10165
10027
  "node-runtime": String(nodeRuntime),
10166
10028
  "proxy-service": options.hasWebConfig ? options.proxyService ?? "web" : "",
10167
10029
  "https": options.httpsAssets?.https ?? "",
10168
- "managed-project": managed["managed-project"],
10169
10030
  // 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
10170
10031
  "managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
10171
10032
  "managed-volumes": managed["managed-volumes"],
@@ -11470,7 +11331,7 @@ function makeDeploymentId() {
11470
11331
  return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
11471
11332
  }
11472
11333
  function sleep(ms) {
11473
- return new Promise((resolve9) => setTimeout(resolve9, ms));
11334
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
11474
11335
  }
11475
11336
  function createDockerSshProvider(config, container) {
11476
11337
  const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
@@ -11502,16 +11363,16 @@ function createDockerSshProvider(config, container) {
11502
11363
  for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
11503
11364
  try {
11504
11365
  if (certificate) {
11505
- const healthy = await new Promise((resolve9) => {
11366
+ const healthy = await new Promise((resolve8) => {
11506
11367
  const request$1 = request(url, {
11507
11368
  ca: [...rootCertificates, certificate],
11508
11369
  timeout: 5e3
11509
11370
  }, (response) => {
11510
11371
  response.resume();
11511
- resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
11372
+ resolve8(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
11512
11373
  });
11513
- request$1.on("error", () => resolve9(false));
11514
- request$1.on("close", () => resolve9(false));
11374
+ request$1.on("error", () => resolve8(false));
11375
+ request$1.on("close", () => resolve8(false));
11515
11376
  request$1.on("timeout", () => request$1.destroy());
11516
11377
  request$1.end();
11517
11378
  });
@@ -11562,7 +11423,7 @@ function createDockerSshProvider(config, container) {
11562
11423
  const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
11563
11424
  const host = config.ssh?.host;
11564
11425
  const url = https ? `https://${https.domain}${https.port === 443 ? "" : `:${https.port}`}` : host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
11565
- const webBasePath = artifact.webBasePath ?? "";
11426
+ const webBasePath = artifact.webBasePath;
11566
11427
  const applicationUrl = `${url}${webBasePath ? `${webBasePath}/` : ""}`;
11567
11428
  const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && (!!webBasePath || !!https);
11568
11429
  const healthPaths = hasWebProxy ? ["/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : primaryService === "serv" ? [] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
@@ -12340,7 +12201,7 @@ async function pollDeploymentStatus(token, deploymentId) {
12340
12201
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
12341
12202
  return "error";
12342
12203
  }
12343
- await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL));
12204
+ await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL));
12344
12205
  }
12345
12206
  return "building";
12346
12207
  }
@@ -13161,8 +13022,6 @@ var deploy = {
13161
13022
  if (!currentProvider && !currentContainerProvider) {
13162
13023
  return notInitialized.result();
13163
13024
  }
13164
- if (options && "projectName" in options && options.projectName !== void 0)
13165
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
13166
13025
  logger12.info("Starting deployment", { appDir, environment: options?.environment });
13167
13026
  const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
13168
13027
  const scanResult = await scanApp(appDir, scanOptions);
@@ -13896,8 +13755,6 @@ function describeMiddlewareBackend(backend) {
13896
13755
  return deployM(backend.purpose);
13897
13756
  }
13898
13757
  async function createDeploymentPlan(appDir, input, options = {}) {
13899
- if ("projectName" in options && options.projectName !== void 0)
13900
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
13901
13758
  const parsed = DeployConfigSchema.safeParse(input);
13902
13759
  if (!parsed.success) {
13903
13760
  return err({
@@ -14098,389 +13955,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
14098
13955
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
14099
13956
  }
14100
13957
  }
14101
- var nameSchema = z.string().regex(/^[a-z0-9][\w.-]{0,127}$/i);
14102
- var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(value, "utf8").toString("utf8") === value && [...value].every((character) => character.codePointAt(0) >= 32 && character.codePointAt(0) !== 127));
14103
- var importSchema = z.strictObject({
14104
- version: z.literal(1),
14105
- projectName: nameSchema,
14106
- credentials: z.strictObject({
14107
- DB_PASSWORD: passwordSchema.optional(),
14108
- STORAGE_PASSWORD: passwordSchema.optional(),
14109
- LDAP_PASSWORD: passwordSchema.optional()
14110
- }),
14111
- services: z.array(z.strictObject({
14112
- service: z.enum(["db", "vecdb", "storage", "ldap"]),
14113
- container: nameSchema,
14114
- kind: z.enum(["postgresql", "mysql", "s3", "ldap"]),
14115
- volumes: z.array(nameSchema).min(1).max(8),
14116
- username: z.string().min(1).max(128).default("hai"),
14117
- database: z.string().min(1).max(128).default("hai"),
14118
- bucket: z.string().regex(/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/).default("hai"),
14119
- accessKeyId: z.string().min(1).max(128).default("hai-storage"),
14120
- region: z.string().regex(/^[a-z0-9-]+$/).default("us-east-1"),
14121
- bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
14122
- })).min(1).max(4)
14123
- });
14124
- var groups2 = {
14125
- db: "DB_PASSWORD",
14126
- vecdb: "DB_PASSWORD",
14127
- storage: "STORAGE_PASSWORD",
14128
- ldap: "LDAP_PASSWORD"
14129
- };
14130
- var importCommitScript = String.raw`(async()=>{const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');const root=process.argv[1];let temporary;
14131
- try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
14132
- if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
14133
- const state=path.join(root,'managed-secrets.env');try{fs.lstatSync(state);throw Error('exists')}catch(error){if(error.code!=='ENOENT')throw error}
14134
- temporary=path.join(root,'.managed-secrets-'+crypto.randomUUID()+'.next');const fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600);
14135
- try{fs.writeFileSync(fd,content);fs.fsyncSync(fd)}finally{fs.closeSync(fd)}fs.renameSync(temporary,state);temporary=undefined;
14136
- const directory=fs.openSync(root,fs.constants.O_RDONLY);try{fs.fsyncSync(directory)}finally{fs.closeSync(directory)}process.stdout.write('COMMITTED\n');
14137
- }catch{process.exitCode=1}finally{if(temporary)try{fs.unlinkSync(temporary)}catch{}}})()`;
14138
- var s3VerificationScript = String.raw`(async()=>{try{
14139
- const {createHash,createHmac}=require('node:crypto');let input='';for await(const chunk of process.stdin)input+=chunk;
14140
- const {endpoint,bucket,accessKeyId,secret,region}=JSON.parse(input);const url=new URL('/'+encodeURIComponent(bucket),endpoint);
14141
- const hash=value=>createHash('sha256').update(value).digest('hex');const hmac=(key,value)=>createHmac('sha256',key).update(value).digest();
14142
- const date=new Date().toISOString().replace(/[:-]|\.\d{3}/g,'');const day=date.slice(0,8);const payload=hash('');
14143
- const headers='host:'+url.host+'\nx-amz-content-sha256:'+payload+'\nx-amz-date:'+date+'\n';const signed='host;x-amz-content-sha256;x-amz-date';
14144
- const scope=day+'/'+region+'/s3/aws4_request';const canonical=['HEAD',url.pathname,'',headers,signed,payload].join('\n');
14145
- const key=hmac(hmac(hmac(hmac('AWS4'+secret,day),region),'s3'),'aws4_request');const signature=hmac(key,['AWS4-HMAC-SHA256',date,scope,hash(canonical)].join('\n')).toString('hex');
14146
- const response=await fetch(url,{method:'HEAD',redirect:'manual',signal:AbortSignal.timeout(10000),headers:{'x-amz-date':date,'x-amz-content-sha256':payload,Authorization:'AWS4-HMAC-SHA256 Credential='+accessKeyId+'/'+scope+', SignedHeaders='+signed+', Signature='+signature}});
14147
- await response.body?.cancel();if(response.ok)process.stdout.write('VERIFIED');else process.exitCode=1;
14148
- }catch{process.exitCode=1}})()`;
14149
- async function importDeploymentSecrets(options) {
14150
- let lock;
14151
- let commitAttempted = false;
14152
- let phase = "input";
14153
- try {
14154
- if (!["docker", "podman"].includes(options.runtime))
14155
- throw new Error("input");
14156
- const root = resolve(options.directory);
14157
- if (realpathSync(root) !== root || !lstatSync(root).isDirectory() || process.platform !== "win32" && lstatSync(root).mode & 18)
14158
- throw new Error("input");
14159
- const input = privateFile(resolve(options.inputFile));
14160
- const parsed = importSchema.safeParse(JSON.parse(input));
14161
- if (!parsed.success) {
14162
- return err({
14163
- ...HaiDeployError.CONFIG_ERROR,
14164
- message: deployM("deploy_secretImportFailed", {
14165
- params: { phase }
14166
- }),
14167
- ext: {
14168
- phase,
14169
- state: "unchanged",
14170
- fields: parsed.error.issues.map((issue) => issue.path.join("."))
14171
- }
14172
- });
14173
- }
14174
- const config = parsed.data;
14175
- const project = `hai-${config.projectName}`;
14176
- const selectedGroups = [
14177
- ...new Set(config.services.map((service) => groups2[service.service]))
14178
- ].sort();
14179
- if (new Set(config.services.map((service) => service.service)).size !== config.services.length || Object.keys(config.credentials).some((group) => !selectedGroups.includes(group)) || selectedGroups.some((group) => !config.credentials[group])) {
14180
- throw new Error("input");
14181
- }
14182
- const secrets = Object.values(config.credentials).filter((value) => typeof value === "string");
14183
- const stateFile = join(root, "managed-secrets.env");
14184
- const lockFile = join(root, ".deploy.lock");
14185
- phase = "lock";
14186
- if (options.dryRun === false) {
14187
- if (process.platform !== "linux")
14188
- throw new Error("lock");
14189
- if (existsSync(lockFile) && (lstatSync(lockFile).isSymbolicLink() || !lstatSync(lockFile).isFile() || lstatSync(lockFile).nlink !== 1))
14190
- throw new Error("lock");
14191
- lock = await acquireImportLock(lockFile, root);
14192
- }
14193
- phase = "identity";
14194
- const volumes = [];
14195
- for (const service of config.services) {
14196
- const kindValid = service.service === "storage" ? service.kind === "s3" : service.service === "ldap" ? service.kind === "ldap" : ["postgresql", "mysql"].includes(service.kind);
14197
- if (!kindValid)
14198
- throw new Error("identity");
14199
- if (["postgresql", "mysql"].includes(service.kind) && (service.username !== "hai" || service.database !== "hai") || service.kind === "s3" && (service.accessKeyId !== "hai-storage" || service.bucket !== "hai" || service.region !== "us-east-1") || service.kind === "ldap" && service.bindDn !== "cn=admin,dc=example,dc=org") {
14200
- throw new Error("identity");
14201
- }
14202
- const identity = await inspectContainer(options.runtime, service.container, secrets);
14203
- if (identity.project !== project || identity.service !== service.service || !identity.running)
14204
- throw new Error("identity");
14205
- for (const volume of service.volumes) {
14206
- if (!identity.volumes.includes(volume))
14207
- throw new Error("identity");
14208
- volumes.push({ group: groups2[service.service], name: volume });
14209
- }
14210
- phase = "verification";
14211
- const password = config.credentials[groups2[service.service]];
14212
- let verified = false;
14213
- if (service.kind === "s3") {
14214
- const helper2 = `hai-${config.projectName}-app`;
14215
- const application = await inspectContainer(options.runtime, helper2, secrets);
14216
- if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
14217
- throw new Error("identity");
14218
- const result = await runCommand(options.runtime, [
14219
- "exec",
14220
- "-i",
14221
- helper2,
14222
- "node",
14223
- "-e",
14224
- s3VerificationScript
14225
- ], {
14226
- input: JSON.stringify({
14227
- endpoint: `http://${service.container}:9000`,
14228
- bucket: service.bucket,
14229
- accessKeyId: service.accessKeyId,
14230
- secret: password,
14231
- region: service.region
14232
- }),
14233
- secrets,
14234
- timeoutMs: 15e3,
14235
- maxCaptureChars: 1024
14236
- });
14237
- verified = result.success && result.data.stdout.trim() === "VERIFIED";
14238
- } else {
14239
- const args = service.kind === "ldap" ? [
14240
- "exec",
14241
- "-i",
14242
- service.container,
14243
- "ldapwhoami",
14244
- "-x",
14245
- "-H",
14246
- "ldap://127.0.0.1:389",
14247
- "-D",
14248
- service.bindDn,
14249
- "-y",
14250
- "/dev/stdin"
14251
- ] : [
14252
- "exec",
14253
- "-i",
14254
- service.container,
14255
- "sh",
14256
- "-c",
14257
- service.kind === "postgresql" ? 'IFS= read -r PGPASSWORD; export PGPASSWORD; exec psql --no-psqlrc -h "$3" -U "$1" -d "dbname=$2 require_auth=scram-sha-256,md5,password" -Atc "SELECT 1"' : 'IFS= read -r MYSQL_PWD; export MYSQL_PWD; exec mysql -h 127.0.0.1 -u "$1" "$2" -Nse "SELECT 1"',
14258
- "hai-secret-check",
14259
- service.username,
14260
- service.database,
14261
- service.container
14262
- ];
14263
- const result = await runCommand(options.runtime, args, {
14264
- input: service.kind === "ldap" ? password : `${password}
14265
- `,
14266
- secrets,
14267
- timeoutMs: 15e3,
14268
- maxCaptureChars: 1024
14269
- });
14270
- verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
14271
- if (verified && service.kind === "mysql") {
14272
- const rootCheck = await runCommand(options.runtime, [
14273
- ...args.slice(0, -3),
14274
- "root",
14275
- service.database,
14276
- service.container
14277
- ], {
14278
- input: `${password}
14279
- `,
14280
- secrets,
14281
- timeoutMs: 15e3,
14282
- maxCaptureChars: 1024
14283
- });
14284
- verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
14285
- const invalidPassword = `hai-import-invalid-${randomUUID()}`;
14286
- for (const username of [service.username, "root"]) {
14287
- if (!verified)
14288
- break;
14289
- const negative = await runCommand(options.runtime, [
14290
- ...args.slice(0, -3),
14291
- username,
14292
- service.database,
14293
- service.container
14294
- ], {
14295
- input: `${invalidPassword}
14296
- `,
14297
- secrets: [...secrets, invalidPassword],
14298
- timeoutMs: 15e3,
14299
- maxCaptureChars: 1024
14300
- });
14301
- verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
14302
- }
14303
- }
14304
- }
14305
- if (!verified)
14306
- throw new Error("verification");
14307
- phase = "identity";
14308
- }
14309
- phase = "state";
14310
- const uniqueVolumes = [
14311
- ...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()
14312
- ].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
14313
- const lines = [
14314
- "FORMAT_VERSION=2",
14315
- `PROJECT=${project}`,
14316
- ...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`),
14317
- ...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)
14318
- ];
14319
- let changed = false;
14320
- if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
14321
- const existing = readImportedState(privateFile(stateFile));
14322
- if (existing.FORMAT_VERSION !== "2" || existing.PROJECT !== project || selectedGroups.some((group) => existing[group] !== Buffer.from(config.credentials[group], "utf8").toString("hex")) || uniqueVolumes.some((volume) => !Object.entries(existing).some(([key, value]) => /^VOLUME_\d+$/.test(key) && value === `${volume.group}:${volume.name}`)))
14323
- throw new Error("state");
14324
- } else if (options.dryRun === false) {
14325
- commitAttempted = true;
14326
- if (!lock || !await lock.commit(`${lines.join("\n")}
14327
- `))
14328
- throw new Error("lock");
14329
- changed = true;
14330
- }
14331
- return ok({
14332
- projectName: config.projectName,
14333
- stateVersion: 2,
14334
- groups: selectedGroups,
14335
- volumes: uniqueVolumes.map((volume) => volume.name),
14336
- changed
14337
- });
14338
- } catch {
14339
- return err({
14340
- ...HaiDeployError.CONFIG_ERROR,
14341
- message: deployM("deploy_secretImportFailed", {
14342
- params: { phase }
14343
- }),
14344
- ext: {
14345
- phase,
14346
- state: commitAttempted ? "unknown" : "unchanged"
14347
- }
14348
- });
14349
- } finally {
14350
- await lock?.release();
14351
- }
14352
- }
14353
- function readImportedState(text) {
14354
- const result = {};
14355
- for (const line of text.trimEnd().split("\n")) {
14356
- const match = /^(FORMAT_VERSION|PROJECT|DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD|VOLUME_[1-9]\d*)=([^=]+)$/.exec(line);
14357
- if (!match || Object.hasOwn(result, match[1]))
14358
- throw new Error("state");
14359
- const key = match[1];
14360
- const value = match[2];
14361
- if (key.endsWith("_PASSWORD")) {
14362
- if (!/^(?:[a-f0-9]{2})+$/.test(value))
14363
- throw new Error("state");
14364
- const decoded = Buffer.from(value, "hex").toString("utf8");
14365
- if (!passwordSchema.safeParse(decoded).success || Buffer.from(decoded, "utf8").toString("hex") !== value)
14366
- throw new Error("state");
14367
- } else if (key.startsWith("VOLUME_") && !/^(?:DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[a-zA-Z0-9][\w.-]*$/.test(value)) {
14368
- throw new Error("state");
14369
- }
14370
- result[key] = value;
14371
- }
14372
- if (result.FORMAT_VERSION !== "2" || !nameSchema.safeParse(result.PROJECT).success)
14373
- throw new Error("state");
14374
- return result;
14375
- }
14376
- function isSymbolicLink(path) {
14377
- try {
14378
- return lstatSync(path).isSymbolicLink();
14379
- } catch {
14380
- return false;
14381
- }
14382
- }
14383
- function privateFile(path) {
14384
- return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
14385
- }
14386
- async function inspectContainer(runtime, container, secrets) {
14387
- const result = await runCommand(runtime, [
14388
- "inspect",
14389
- "--format",
14390
- "{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}",
14391
- container
14392
- ], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
14393
- if (!result.success || result.data.stdoutTruncated)
14394
- throw new Error("identity");
14395
- const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
14396
- const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
14397
- const mounts = z.array(z.object({
14398
- Type: z.string(),
14399
- Name: z.string().optional()
14400
- })).parse(JSON.parse(mountsText));
14401
- const networks = z.record(z.string(), z.object({
14402
- NetworkID: z.string()
14403
- })).parse(JSON.parse(networksText) ?? {});
14404
- const dockerProject = labels["com.docker.compose.project"] || void 0;
14405
- const podmanProject = labels["io.podman.compose.project"] || void 0;
14406
- const dockerService = labels["com.docker.compose.service"] || void 0;
14407
- const podmanService = labels["io.podman.compose.service"] || void 0;
14408
- if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
14409
- throw new Error("identity");
14410
- }
14411
- return {
14412
- project: dockerProject ?? podmanProject,
14413
- service: dockerService ?? podmanService,
14414
- running: runningText === "true",
14415
- volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []),
14416
- networks: Object.values(networks).map((network) => network.NetworkID)
14417
- };
14418
- }
14419
- async function acquireImportLock(path, root) {
14420
- return new Promise((resolveLock, reject) => {
14421
- const child = spawn("flock", [
14422
- "-n",
14423
- path,
14424
- process.execPath,
14425
- "-e",
14426
- importCommitScript,
14427
- root
14428
- ], {
14429
- stdio: ["pipe", "pipe", "ignore"],
14430
- windowsHide: true
14431
- });
14432
- let acquired = false;
14433
- let exited = false;
14434
- let output = "";
14435
- const finished = new Promise((done) => child.once("close", () => {
14436
- exited = true;
14437
- done();
14438
- }));
14439
- child.stdin.on("error", () => {
14440
- });
14441
- const timer = setTimeout(() => {
14442
- child.stdin.end();
14443
- child.kill();
14444
- reject(new Error("lock"));
14445
- }, 5e3);
14446
- child.once("error", () => {
14447
- clearTimeout(timer);
14448
- reject(new Error("lock"));
14449
- });
14450
- child.once("exit", () => {
14451
- if (!acquired) {
14452
- clearTimeout(timer);
14453
- reject(new Error("lock"));
14454
- }
14455
- });
14456
- child.stdout.on("data", (data) => {
14457
- output += data.toString();
14458
- if (acquired || !output.includes("\n"))
14459
- return;
14460
- if (!output.startsWith("LOCKED\n") || exited) {
14461
- child.stdin.end();
14462
- reject(new Error("lock"));
14463
- return;
14464
- }
14465
- acquired = true;
14466
- clearTimeout(timer);
14467
- resolveLock({
14468
- commit: async (content) => {
14469
- if (exited)
14470
- return false;
14471
- child.stdin.end(JSON.stringify({ content }));
14472
- await finished;
14473
- return child.exitCode === 0 && output === "LOCKED\nCOMMITTED\n";
14474
- },
14475
- release: async () => {
14476
- child.stdin.end();
14477
- await finished;
14478
- }
14479
- });
14480
- });
14481
- });
14482
- }
14483
13958
 
14484
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, parseMiddlewareField, readDeploymentArtifact, resolveDownloadProxy, resolveImageReference, saveDeploymentEnvironment, saveMiddlewareEnvironment, validateMiddlewareField };
13959
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, parseMiddlewareField, readDeploymentArtifact, resolveDownloadProxy, resolveImageReference, saveDeploymentEnvironment, saveMiddlewareEnvironment, validateMiddlewareField };
14485
13960
  //# sourceMappingURL=index.js.map
14486
13961
  //# sourceMappingURL=index.js.map