@h-ai/deploy 1.0.0-beta.21 → 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.",
@@ -221,7 +216,9 @@ var en_US_default = {
221
216
  deploy_middlewareMethod: "Enter POST or PUT.",
222
217
  deploy_configResolutionFailed: "Cannot resolve {path}. Check the configuration structure and array indices for {fields}.",
223
218
  deploy_reachEmptyProviderOverride: "While resolving {path}, {field} refers to a missing provider because HAI_REACH_PROVIDERS is []. Remove stale indexed variables and set HAI_REACH_PROVIDERS to the complete notification provider list in .env.<environment>.local.",
224
- deploy_s3Engine: "Choose the self-hosted S3 implementation"
219
+ deploy_s3Engine: "Choose the self-hosted S3 implementation",
220
+ deploy_downloadUnavailableTitle: "Download link unavailable",
221
+ deploy_downloadUnavailableInstruction: "Return to the download page to get a new link. If the download still fails, contact your administrator."
225
222
  };
226
223
 
227
224
  // messages/ja-JP.json
@@ -332,9 +329,7 @@ var ja_JP_default = {
332
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",
333
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",
334
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",
335
- 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",
336
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",
337
- 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",
338
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",
339
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",
340
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",
@@ -364,7 +359,9 @@ var ja_JP_default = {
364
359
  deploy_middlewareMethod: "POST \u307E\u305F\u306F PUT \u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
365
360
  deploy_configResolutionFailed: "{path} \u3092\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002{fields} \u306B\u5BFE\u5FDC\u3059\u308B\u8A2D\u5B9A\u306E\u69CB\u9020\u3068\u914D\u5217\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
366
361
  deploy_reachEmptyProviderOverride: "{path} \u306E\u6709\u52B9\u306A\u8A2D\u5B9A\u3092\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002HAI_REACH_PROVIDERS \u304C [] \u306E\u305F\u3081\u3001{field} \u306B\u5BFE\u5FDC\u3059\u308B\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u53E4\u3044\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u4ED8\u304D\u5909\u6570\u3092\u524A\u9664\u3057\u3001.env.<\u74B0\u5883>.local \u306E HAI_REACH_PROVIDERS \u306B\u901A\u77E5\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u5B8C\u5168\u306A\u4E00\u89A7\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
367
- deploy_s3Engine: "\u30BB\u30EB\u30D5\u30DB\u30B9\u30C8 S3 \u306E\u5B9F\u88C5\u3092\u9078\u629E"
362
+ deploy_s3Engine: "\u30BB\u30EB\u30D5\u30DB\u30B9\u30C8 S3 \u306E\u5B9F\u88C5\u3092\u9078\u629E",
363
+ deploy_downloadUnavailableTitle: "\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u30EA\u30F3\u30AF\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u307E\u305B\u3093",
364
+ deploy_downloadUnavailableInstruction: "\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u30DA\u30FC\u30B8\u306B\u623B\u308A\u3001\u65B0\u3057\u3044\u30EA\u30F3\u30AF\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u89E3\u6C7A\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u304A\u554F\u3044\u5408\u308F\u305B\u304F\u3060\u3055\u3044\u3002"
368
365
  };
369
366
 
370
367
  // messages/zh-CN.json
@@ -475,9 +472,7 @@ var zh_CN_default = {
475
472
  deploy_uploadPasswordCancelled: "\u5DF2\u53D6\u6D88\u5BC6\u7801\u8F93\u5165\uFF0C\u5C1A\u672A\u8FDE\u63A5\u670D\u52A1\u5668\u3002",
476
473
  deploy_uploadPasswordInvalid: "SSH \u5BC6\u7801\u987B\u4E3A 1\u20131023 \u4E2A UTF-8 \u5B57\u8282\uFF0C\u4E14\u4E0D\u80FD\u5305\u542B\u6362\u884C\u3002",
477
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",
478
- 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",
479
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",
480
- 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",
481
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",
482
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",
483
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",
@@ -507,7 +502,9 @@ var zh_CN_default = {
507
502
  deploy_middlewareMethod: "\u8BF7\u8F93\u5165 POST \u6216 PUT\u3002",
508
503
  deploy_configResolutionFailed: "\u65E0\u6CD5\u89E3\u6790 {path}\u3002\u8BF7\u68C0\u67E5 {fields} \u5BF9\u5E94\u7684\u914D\u7F6E\u7ED3\u6784\u4E0E\u6570\u7EC4\u7D22\u5F15\u3002",
509
504
  deploy_reachEmptyProviderOverride: "\u89E3\u6790 {path} \u7684\u751F\u6548\u914D\u7F6E\u65F6\u53D1\u73B0 {field} \u6307\u5411\u4E0D\u5B58\u5728\u7684 Provider\u3002HAI_REACH_PROVIDERS \u5F53\u524D\u4E3A []\u3002\u8BF7\u5220\u9664\u65E7\u7684\u7D22\u5F15\u53D8\u91CF\uFF0C\u5E76\u5728 .env.<\u73AF\u5883>.local \u4E2D\u7528 HAI_REACH_PROVIDERS \u914D\u7F6E\u5B8C\u6574\u7684\u901A\u77E5 Provider \u5217\u8868\u3002",
510
- deploy_s3Engine: "\u9009\u62E9\u81EA\u5EFA S3 \u670D\u52A1\u5B9E\u73B0"
505
+ deploy_s3Engine: "\u9009\u62E9\u81EA\u5EFA S3 \u670D\u52A1\u5B9E\u73B0",
506
+ deploy_downloadUnavailableTitle: "\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6CD5\u8BBF\u95EE",
507
+ deploy_downloadUnavailableInstruction: "\u8BF7\u8FD4\u56DE\u4E0B\u8F7D\u9875\u9762\u91CD\u65B0\u83B7\u53D6\u94FE\u63A5\u3002\u82E5\u4ECD\u65E0\u6CD5\u4E0B\u8F7D\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002"
511
508
  };
512
509
 
513
510
  // src/deploy-i18n.ts
@@ -1457,7 +1454,26 @@ var deploy_config_shapes_default = {
1457
1454
  type: "string",
1458
1455
  format: "uri"
1459
1456
  },
1457
+ multipart: {
1458
+ default: {},
1459
+ type: "object",
1460
+ properties: {
1461
+ partSizeBytes: {
1462
+ default: 5242880,
1463
+ type: "integer",
1464
+ minimum: 5242880,
1465
+ maximum: 5368709120
1466
+ },
1467
+ queueSize: {
1468
+ default: 2,
1469
+ type: "integer",
1470
+ minimum: 1,
1471
+ maximum: 9007199254740991
1472
+ }
1473
+ }
1474
+ },
1460
1475
  operationLog: {
1476
+ default: {},
1461
1477
  type: "object",
1462
1478
  properties: {
1463
1479
  read: {
@@ -1465,7 +1481,7 @@ var deploy_config_shapes_default = {
1465
1481
  type: "boolean"
1466
1482
  },
1467
1483
  write: {
1468
- default: false,
1484
+ default: true,
1469
1485
  type: "boolean"
1470
1486
  },
1471
1487
  maxLength: {
@@ -1473,15 +1489,6 @@ var deploy_config_shapes_default = {
1473
1489
  type: "integer",
1474
1490
  minimum: 0,
1475
1491
  maximum: 9007199254740991
1476
- },
1477
- level: {
1478
- default: "debug",
1479
- type: "string",
1480
- enum: [
1481
- "info",
1482
- "debug",
1483
- "trace"
1484
- ]
1485
1492
  }
1486
1493
  }
1487
1494
  }
@@ -1514,6 +1521,7 @@ var deploy_config_shapes_default = {
1514
1521
  type: "number"
1515
1522
  },
1516
1523
  operationLog: {
1524
+ default: {},
1517
1525
  type: "object",
1518
1526
  properties: {
1519
1527
  read: {
@@ -1521,7 +1529,7 @@ var deploy_config_shapes_default = {
1521
1529
  type: "boolean"
1522
1530
  },
1523
1531
  write: {
1524
- default: false,
1532
+ default: true,
1525
1533
  type: "boolean"
1526
1534
  },
1527
1535
  maxLength: {
@@ -1529,15 +1537,6 @@ var deploy_config_shapes_default = {
1529
1537
  type: "integer",
1530
1538
  minimum: 0,
1531
1539
  maximum: 9007199254740991
1532
- },
1533
- level: {
1534
- default: "debug",
1535
- type: "string",
1536
- enum: [
1537
- "info",
1538
- "debug",
1539
- "trace"
1540
- ]
1541
1540
  }
1542
1541
  }
1543
1542
  }
@@ -6253,9 +6252,9 @@ function existingFile(path) {
6253
6252
  throw error;
6254
6253
  }
6255
6254
  }
6256
- function writeLocalFile(path, content, privateFile2 = false) {
6255
+ function writeLocalFile(path, content, privateFile = false) {
6257
6256
  const previous = existingFile(path);
6258
- const mode = privateFile2 ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
6257
+ const mode = privateFile ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
6259
6258
  const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
6260
6259
  let fd;
6261
6260
  let owned;
@@ -6462,7 +6461,7 @@ function isSecret(name, value) {
6462
6461
  }
6463
6462
  function generatedVariables(values) {
6464
6463
  const generated = {};
6465
- const groups3 = /* @__PURE__ */ new Map();
6464
+ const groups2 = /* @__PURE__ */ new Map();
6466
6465
  const structures = /* @__PURE__ */ new Set();
6467
6466
  for (const [name, value] of Object.entries(values)) {
6468
6467
  if (value === void 0)
@@ -6474,9 +6473,9 @@ function generatedVariables(values) {
6474
6473
  const module = /^HAI_([A-Z0-9]+)(?:_|$)/.exec(name)?.[1]?.toLowerCase();
6475
6474
  if (!module || !shapes[module])
6476
6475
  continue;
6477
- const group = groups3.get(module) ?? {};
6476
+ const group = groups2.get(module) ?? {};
6478
6477
  group[name] = text;
6479
- groups3.set(module, group);
6478
+ groups2.set(module, group);
6480
6479
  try {
6481
6480
  const parsed = JSON.parse(text);
6482
6481
  if (parsed !== null && typeof parsed === "object")
@@ -6484,7 +6483,7 @@ function generatedVariables(values) {
6484
6483
  } catch {
6485
6484
  }
6486
6485
  }
6487
- for (const [module, group] of groups3) {
6486
+ for (const [module, group] of groups2) {
6488
6487
  const structuredNames = Object.keys(group).filter((name) => structures.has(name));
6489
6488
  if (!structuredNames.length)
6490
6489
  continue;
@@ -6531,7 +6530,7 @@ function generatedVariables(values) {
6531
6530
  }
6532
6531
  return generated;
6533
6532
  }
6534
- function updateFile(path, values, privateFile2) {
6533
+ function updateFile(path, values, privateFile) {
6535
6534
  let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
6536
6535
  for (const [name, value] of Object.entries(values)) {
6537
6536
  if (!/^[A-Z][A-Z0-9_]*$/.test(name))
@@ -6555,15 +6554,15 @@ ${line}
6555
6554
  content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
6556
6555
  }
6557
6556
  }
6558
- writeLocalFile(path, content, privateFile2);
6557
+ writeLocalFile(path, content, privateFile);
6559
6558
  }
6560
- function removeOverrides(path, names, privateFile2 = true) {
6559
+ function removeOverrides(path, names, privateFile = true) {
6561
6560
  if (!existsSync(path) || names.length === 0)
6562
6561
  return;
6563
6562
  let content = readLocalFile(path).toString("utf8");
6564
6563
  for (const name of names)
6565
6564
  content = content.replace(new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
6566
- writeLocalFile(path, content, privateFile2);
6565
+ writeLocalFile(path, content, privateFile);
6567
6566
  }
6568
6567
  function saveDeploymentEnvironment(environment, values, options = {}) {
6569
6568
  try {
@@ -7126,47 +7125,6 @@ async function scanApp(inputDir, options) {
7126
7125
  }
7127
7126
  }
7128
7127
 
7129
- // src/container/deploy-compose-identity.ts
7130
- function createComposeContainerLookupScript() {
7131
- const labelFormat = [
7132
- "com.docker.compose.project",
7133
- "com.docker.compose.service",
7134
- "io.podman.compose.project",
7135
- "io.podman.compose.service"
7136
- ].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
7137
- return String.raw`compose_container_ids() (
7138
- if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
7139
- lookup_runtime=$1
7140
- lookup_project=$2
7141
- lookup_service=$3
7142
- lookup_selector=$4
7143
- case "$lookup_runtime" in docker|podman) ;; *) printf 'Invalid Compose lookup runtime.\n' >&2; exit 1;; esac
7144
- case "$lookup_project" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose project identity.\n' >&2; exit 1;; esac
7145
- case "$lookup_service" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose service identity.\n' >&2; exit 1;; esac
7146
- case "$lookup_selector" in -q|-aq) ;; *) printf 'Invalid Compose container selector.\n' >&2; exit 1;; esac
7147
- lookup_ids=''
7148
- for lookup_namespace in com.docker.compose io.podman.compose; do
7149
- 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; }
7150
- 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; }
7151
- lookup_ids=$(printf '%s\n%s\n' "$lookup_ids" "$lookup_found" | LC_ALL=C awk 'NF && !seen[$0]++ {print}') || exit 1
7152
- done
7153
- for lookup_id in $lookup_ids; do
7154
- lookup_labels=$("$lookup_runtime" inspect --format '${labelFormat}' "$lookup_id") || { printf 'Cannot inspect Compose container labels.\n' >&2; exit 1; }
7155
- printf '%s\n' "$lookup_labels" | LC_ALL=C awk -F'|' -v project="$lookup_project" -v service="$lookup_service" '
7156
- NR!=1 || NF!=4 {exit 1}
7157
- {
7158
- docker=($1!="" && $1!="<no value>") || ($2!="" && $2!="<no value>")
7159
- podman=($3!="" && $3!="<no value>") || ($4!="" && $4!="<no value>")
7160
- if((!docker && !podman) || (docker && ($1!=project || $2!=service)) || (podman && ($3!=project || $4!=service))) exit 1
7161
- }
7162
- END {if(NR!=1) exit 1}
7163
- ' || { printf 'Container labels do not confirm the requested Compose project and service.\n' >&2; exit 1; }
7164
- done
7165
- if [ -n "$lookup_ids" ]; then printf '%s\n' "$lookup_ids"; fi
7166
- )
7167
- `;
7168
- }
7169
-
7170
7128
  // src/container/deploy-json.ts
7171
7129
  function createDeploymentJsonTool() {
7172
7130
  return String.raw`function fail(message) {
@@ -7491,7 +7449,7 @@ deployment_metadata_init() {
7491
7449
  deployment_metadata_file() {
7492
7450
  metadata_directory=$1
7493
7451
  metadata_name=$2
7494
- 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
7495
7453
  test -f "$metadata_directory/manifest.json" && test ! -L "$metadata_directory/manifest.json" || return 1
7496
7454
  metadata_identity=$(printf '%s' "$metadata_directory" | sha256sum | awk '{print $1}')
7497
7455
  metadata_cached="$metadata_work/$metadata_identity-$metadata_name"
@@ -7588,8 +7546,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7588
7546
  }
7589
7547
  }
7590
7548
  return {
7591
- "managed-project": `hai-${projectName}
7592
- `,
7593
7549
  "managed-candidates.env": candidates.length ? `${candidates.join("\n")}
7594
7550
  ` : "",
7595
7551
  "managed-bindings": bindings.size ? `${[...bindings].join("\n")}
@@ -7599,29 +7555,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7599
7555
  "resolve-managed-secrets.sh": createManagedSecretScript()
7600
7556
  };
7601
7557
  }
7602
- function createManagedSecretValidationScript() {
7603
- return String.raw`valid_values() {
7604
- version=$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$1")
7605
- if [ "$version" = 2 ]; then
7606
- LC_ALL=C awk -F= '
7607
- NF {
7608
- if(NF!=2 || seen[$1]++) exit 1
7609
- if($1=="FORMAT_VERSION") {if($2!="2") exit 1; next}
7610
- if($1=="PROJECT") {if($2!~/^[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; project=1; next}
7611
- if($1~/^VOLUME_[0-9]+$/) {if($2!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; next}
7612
- if($1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[0-9a-f]+$/ || length($2)%2 || length($2)>8192) exit 1
7613
- for(i=1;i<=length($2);i+=2) {b=substr($2,i,2); if(b~/^[01]/ || b=="7f") exit 1}
7614
- }
7615
- END {if(!project) exit 1}
7616
- ' "$1"
7617
- return
7618
- fi
7619
- LC_ALL=C awk -F= '
7620
- NF {if(NF!=2 || $1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[A-Za-z0-9_-]+$/ || length($2)!=32 || seen[$1]++) exit 1}
7621
- ' "$1"
7622
- }
7623
- `;
7624
- }
7625
7558
  function createManagedSecretScript() {
7626
7559
  return String.raw`#!/bin/sh
7627
7560
  # 由 apply.sh 在项目部署锁内调用;长期凭据只保存在服务器,不依赖打包机。
@@ -7636,12 +7569,15 @@ test -d "$root" && test ! -L "$root" && test -d "$release/.hai" && test ! -L "$r
7636
7569
  @@MANIFEST_FUNCTIONS@@
7637
7570
  deployment_metadata_init
7638
7571
  trap deployment_metadata_cleanup EXIT
7639
- managed_project=$(deployment_metadata_file "$release" managed-project)
7640
7572
  managed_bindings=$(deployment_metadata_file "$release" managed-bindings)
7641
7573
  managed_volumes=$(deployment_metadata_file "$release" managed-volumes)
7642
7574
  test -s "$candidate_file" || exit 0
7643
7575
  fail() { printf '%s\n' "$*" >&2; exit 1; }
7644
- @@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
+ }
7645
7581
  value_of() { awk -F= -v key="$2" '$1==key {print $2}' "$1"; }
7646
7582
  test ! -L "$state" || fail "Invalid managed middleware credential state: $state"
7647
7583
  valid_values "$candidate_file" || fail 'Invalid managed middleware credential candidates; rebuild the deployment bundle.'
@@ -7652,22 +7588,6 @@ if [ -f "$state" ]; then
7652
7588
  else
7653
7589
  : > "$next"
7654
7590
  fi
7655
- format=$(value_of "$next" FORMAT_VERSION)
7656
- if [ "$format" = 2 ]; then
7657
- test "$(value_of "$next" PROJECT)" = "$(cat "$managed_project")" || fail 'Managed middleware credential project mismatch.'
7658
- known=$("$runtime" volume ls -q) || fail 'Cannot check persistent volumes; managed middleware credentials were not changed.'
7659
- while IFS=' ' read -r volume_group volume; do
7660
- test -n "$volume" || continue
7661
- binding="$volume_group:$volume"
7662
- if ! awk -F= -v binding="$binding" '$1~/^VOLUME_[0-9]+$/ && $2==binding {found=1} END {exit !found}' "$next"; then
7663
- if printf '%s\n' "$known" | grep -Fxq "$volume"; then
7664
- fail 'Existing persistent volume is not bound to the imported credential state. Import its verified identity first.'
7665
- fi
7666
- index=$(awk -F= '$1~/^VOLUME_[0-9]+$/ {n=substr($1,8)+0; if(n>max)max=n} END {print max+1}' "$next")
7667
- printf 'VOLUME_%s=%s\n' "$index" "$binding" >> "$next"
7668
- fi
7669
- done < "$managed_volumes"
7670
- fi
7671
7591
  volumes_checked=false
7672
7592
  known_volumes=''
7673
7593
  while IFS='=' read -r name candidate; do
@@ -7684,64 +7604,24 @@ while IFS='=' read -r name candidate; do
7684
7604
  fail "Missing $name in $state for existing persistent volume $volume. Restore the server credential state before retrying."
7685
7605
  fi
7686
7606
  done < "$managed_volumes"
7687
- value=$candidate
7688
- if [ "$format" = 2 ]; then value=$(printf '%s' "$candidate" | od -An -tx1 | tr -d ' \n'); fi
7689
- printf '%s=%s\n' "$name" "$value" >> "$next"
7607
+ printf '%s=%s\n' "$name" "$candidate" >> "$next"
7690
7608
  fi
7691
7609
  done < "$candidate_file"
7692
7610
  while IFS='=' read -r name candidate; do
7693
7611
  test -n "$name" || continue
7694
7612
  value=$(value_of "$next" "$name")
7695
7613
  for file in config/.env.runtime .hai/middleware-runtime.env; do
7696
- compose_environment=false
7697
- if [ "$file" = .hai/middleware-runtime.env ]; then compose_environment=true; fi
7698
- 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" '
7699
7615
  FILENAME==ARGV[1] {if($1==group) {keys[$2]=$3}; next}
7700
7616
  function replace_literal(line, needle, value, result, at) {
7701
7617
  result=""
7702
7618
  while((at=index(line,needle))>0) {result=result substr(line,1,at-1) value; line=substr(line,at+length(needle))}
7703
7619
  return result line
7704
7620
  }
7705
- function decode_hex(hex, result, i, high, low) {
7706
- 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)}
7707
- return result
7708
- }
7709
- function json_escape(value, apostrophe, result, i, ch) {
7710
- for(i=1;i<=length(value);i++) {
7711
- ch=substr(value,i,1)
7712
- if(ch=="\\") result=result "\\\\"
7713
- else if(ch=="\"") result=result "\\\""
7714
- else if(ch==sprintf("%c",39) && apostrophe) result=result "\\u0027"
7715
- else result=result ch
7716
- }
7717
- return result
7718
- }
7719
- function uri_escape(value, result, i, ch, j, ordinal) {
7720
- for(i=1;i<=length(value);i++) {
7721
- ch=substr(value,i,1)
7722
- if(ch~/[A-Za-z0-9_.~-]/) result=result ch
7723
- else {ordinal=0; for(j=1;j<=255;j++) if(sprintf("%c",j)==ch) {ordinal=j; break}; result=result sprintf("%%%02X",ordinal)}
7724
- }
7725
- return result
7726
- }
7727
7621
  {
7728
7622
  equal=index($0,"="); key=substr($0,1,equal-1); scoped=0
7729
7623
  for(prefix in keys) if(key==prefix || (keys[prefix]=="prefix" && index(key,prefix "_")==1)) scoped=1
7730
- if(scoped && format==2 && index($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"])>0) {
7731
- candidate=ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"]; value=decode_hex(ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
7732
- literal=substr($0,equal+1); quote=sprintf("%c",39)
7733
- json=substr(literal,1,1)==quote && (substr(literal,2,1)=="{" || substr(literal,2,1)=="[")
7734
- if(json) literal=substr(literal,2,length(literal)-2)
7735
- literal=replace_literal(literal,":" candidate "@",":" uri_escape(value) "@")
7736
- if(json) {literal=replace_literal(literal,candidate,json_escape(value,1)); print key "=" quote literal quote}
7737
- else {
7738
- if(substr(literal,1,1)==quote || substr(literal,1,1)=="\"") literal=substr(literal,2,length(literal)-2)
7739
- literal=replace_literal(literal,candidate,value)
7740
- literal=json_escape(literal,0); if(compose_environment=="true") literal=replace_literal(literal,"$","$")
7741
- print key "=\"" literal "\""
7742
- }
7743
- }
7744
- 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"])
7745
7625
  else print
7746
7626
  }
7747
7627
  ' "$managed_bindings" "$release/$file" > "$release/$file.next"
@@ -7751,7 +7631,7 @@ done < "$candidate_file"
7751
7631
  chmod 600 "$next"
7752
7632
  mv "$next" "$state"
7753
7633
  printf '[INFO] Managed middleware credential source: %s (server state; values hidden).\n' "$state"
7754
- `.replace("@@MANAGED_VALUES_VALIDATOR@@", () => createManagedSecretValidationScript()).replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
7634
+ `.replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
7755
7635
  }
7756
7636
 
7757
7637
  // src/container/deploy-backend-state.ts
@@ -7814,11 +7694,11 @@ backend_manifest=$(deployment_metadata_file "$release" state-backends)
7814
7694
  regular() { test -f "$1" && test ! -L "$1" || fail "Backend state must be a regular file: $1"; }
7815
7695
  absent_or_regular() { if [ -e "$1" ] || [ -L "$1" ]; then regular "$1"; fi; }
7816
7696
  valid_tuples() {
7817
- LC_ALL=C awk -v legacy="$2" '
7697
+ LC_ALL=C awk '
7818
7698
  NF {
7819
- if((NF!=3 && !(legacy==1 && NF==2)) || roles[$1]++) exit 1
7699
+ if(NF!=3 || roles[$1]++) exit 1
7820
7700
  if(!(($1=="db" && $2~/^(postgresql|mysql)$/) || ($1=="storage" && $2~/^(rustfs|minio)$/) || ($1=="vecdb" && $2~/^(qdrant|pgvector|chroma)$/))) exit 1
7821
- 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
7822
7702
  }
7823
7703
  ' "$1"
7824
7704
  }
@@ -7869,45 +7749,6 @@ known=$("$runtime" volume ls -q) || fail 'Cannot list persistent volumes; no bac
7869
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.'
7870
7750
  exists_volume() { printf '%s\n' "$known" | grep -Fxq "$1"; }
7871
7751
  ledger_tuple() { printf '%s\n' "$ledger" | awk -v volume="$1" '$3==volume {print}'; }
7872
- @@MANAGED_VALUES_VALIDATOR@@
7873
- @@COMPOSE_CONTAINER_LOOKUP@@
7874
- # 状态补证:卷挂载和镜像均取自同项目、同角色的唯一实际容器。
7875
- container_binding() {
7876
- binding_service=$1
7877
- binding_family=$2
7878
- binding_expected=$3
7879
- binding_running=$4
7880
- binding_flags=-aq
7881
- if [ "$binding_running" = true ]; then binding_flags=-q; fi
7882
- binding_containers=$(compose_container_ids "$runtime" "$project" "$binding_service" "$binding_flags") || fail 'Cannot inspect successful deployment containers.'
7883
- 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."
7884
- if [ "$binding_running" = true ]; then
7885
- binding_status=$("$runtime" inspect --format '{{.State.Running}}' "$binding_containers") || fail 'Cannot verify the imported service is running.'
7886
- test "$binding_status" = true || fail "Imported backend requires its verified original container to be running: $binding_service."
7887
- fi
7888
- binding_mounted=$("$runtime" inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{println .Name}}{{end}}{{end}}' "$binding_containers") || fail 'Cannot inspect previous named volume mounts.'
7889
- 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."
7890
- binding_image=$("$runtime" inspect --format '{{.Config.Image}}' "$binding_containers") || fail 'Cannot identify the previous container image.'
7891
- 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.'
7892
- test "$binding_actual_family" = "$binding_family" || fail "Actual previous container image does not confirm its backend family: $binding_service."
7893
- if [ -n "$binding_expected" ] && [ "$binding_expected" != "$binding_mounted" ]; then fail "Actual previous volume differs from its backend manifest: $binding_service."; fi
7894
- printf '%s\n' "$binding_mounted"
7895
- }
7896
- confirmed_import() {
7897
- import_service=$1
7898
- import_family=$2
7899
- import_volume=$3
7900
- import_state="$root/managed-secrets.env"
7901
- test -f "$import_state" && test ! -L "$import_state" || return 1
7902
- valid_values "$import_state" || return 1
7903
- test "$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$import_state")" = 2 || return 1
7904
- test "$(awk -F= '$1=="PROJECT" {print $2}' "$import_state")" = "$project" || return 1
7905
- case "$import_service" in db|vecdb) import_group=DB_PASSWORD;; storage) import_group=STORAGE_PASSWORD;; *) return 1;; esac
7906
- 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
7907
- # 密码不证明目录格式;受控导入还须与唯一运行原容器的实际卷及镜像家族共同吻合。
7908
- import_actual=$(container_binding "$import_service" "$import_family" "$import_volume" true) || return 1
7909
- test "$import_actual" = "$import_volume"
7910
- }
7911
7752
  snapshot_previous=$previous
7912
7753
  if [ -z "$snapshot_previous" ]; then snapshot_previous=-; fi
7913
7754
  if [ "$mode" = recover ]; then
@@ -7925,7 +7766,7 @@ RECOVERY_TUPLES
7925
7766
  exit 0
7926
7767
  fi
7927
7768
  regular "$backend_manifest"
7928
- 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.'
7929
7770
  active=''
7930
7771
  if [ -n "$previous" ]; then
7931
7772
  baseline="$root/releases/$previous"
@@ -7934,7 +7775,7 @@ if [ -n "$previous" ]; then
7934
7775
  baseline_services=$(deployment_services_file "$baseline")
7935
7776
  previous_manifest=$(deployment_metadata_file "$baseline" state-backends)
7936
7777
  regular "$previous_manifest"
7937
- valid_tuples "$previous_manifest" 0 || fail 'Invalid successful backend manifest.'
7778
+ valid_tuples "$previous_manifest" || fail 'Invalid successful backend manifest.'
7938
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.'
7939
7780
  for active_service in db storage vecdb; do
7940
7781
  if ! grep -Fxq "$active_service" "$baseline_services"; then continue; fi
@@ -7947,28 +7788,18 @@ if [ -n "$previous" ]; then
7947
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."
7948
7789
  exists_volume "$active_volume" || fail "Active persistent volume is missing: $active_volume. Restore its backup instead of creating an empty replacement."
7949
7790
  confirmed=$(ledger_tuple "$active_volume")
7950
- test -z "$confirmed" || test "$confirmed" = "$active_service $active_family $active_volume" || fail "Active persistent backend conflicts with the protected ledger: $active_service."
7951
- if [ -z "$confirmed" ]; then
7952
- proven_volume=$(container_binding "$active_service" "$active_family" "$active_volume" false)
7953
- test "$proven_volume" = "$active_volume" || fail "Cannot confirm the active persistent volume: $active_service."
7954
- 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."
7955
7792
  active=$(printf '%s\n%s %s %s\n' "$active" "$active_service" "$active_family" "$active_volume" | sed '/^$/d')
7956
7793
  done
7957
7794
  fi
7958
- # 各个旧 Compose 片段不能把同一实际卷重复绑定给不同有状态角色。
7959
- 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.'
7960
7797
  # 合并时只替换同一卷的无数据预认领,不能按本次拓扑裁剪历史绑定。
7961
7798
  merged=$ledger
7962
7799
  merge_tuple() {
7963
7800
  merged=$(printf '%s\n' "$merged" | awk -v volume="$3" 'NF && $3!=volume {print}')
7964
7801
  merged=$(printf '%s\n%s %s %s\n' "$merged" "$1" "$2" "$3" | sed '/^$/d')
7965
7802
  }
7966
- while IFS=' ' read -r active_service active_family active_volume; do
7967
- test -n "$active_service" || continue
7968
- merge_tuple "$active_service" "$active_family" "$active_volume"
7969
- done <<ACTIVE_TUPLES
7970
- $active
7971
- ACTIVE_TUPLES
7972
7803
  while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
7973
7804
  test -n "$candidate_service" || continue
7974
7805
  prior_active=$(printf '%s\n' "$active" | awk -v service="$candidate_service" '$1==service {print}')
@@ -7976,8 +7807,6 @@ while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
7976
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."
7977
7808
  if exists_volume "$candidate_volume"; then
7978
7809
  confirmed=$(ledger_tuple "$candidate_volume")
7979
- if [ -z "$confirmed" ]; then confirmed=$(printf '%s\n' "$active" | awk -v volume="$candidate_volume" '$3==volume {print}'); fi
7980
- if [ -z "$confirmed" ] && confirmed_import "$candidate_service" "$candidate_family" "$candidate_volume"; then confirmed=$tuple; fi
7981
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."
7982
7811
  else
7983
7812
  users=$("$runtime" ps -aq --filter "volume=$candidate_volume") || fail 'Cannot check persistent volume users.'
@@ -7997,7 +7826,7 @@ mv -f -- "$snapshot.next" "$snapshot"
7997
7826
  (set -C; printf '%s\n' "$state_content" > "$state.next") || fail 'Cannot stage the persistent backend ledger.'
7998
7827
  chmod 600 "$state.next"
7999
7828
  mv -f -- "$state.next" "$state"
8000
- `.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());
8001
7830
  }
8002
7831
  var nativeJob = String.raw`
8003
7832
  using System;
@@ -8196,7 +8025,7 @@ async function groupExited(pid) {
8196
8025
  async function helper(command, args, timeoutMs) {
8197
8026
  if (timeoutMs <= 0)
8198
8027
  return false;
8199
- return new Promise((resolve9) => {
8028
+ return new Promise((resolve8) => {
8200
8029
  const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
8201
8030
  let settled = false;
8202
8031
  let timer;
@@ -8206,7 +8035,7 @@ async function helper(command, args, timeoutMs) {
8206
8035
  settled = true;
8207
8036
  if (timer)
8208
8037
  clearTimeout(timer);
8209
- resolve9(success);
8038
+ resolve8(success);
8210
8039
  };
8211
8040
  timer = globalThis.setTimeout(() => {
8212
8041
  child.kill("SIGKILL");
@@ -8359,7 +8188,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8359
8188
  ext: { commandExecution: state }
8360
8189
  });
8361
8190
  try {
8362
- return await new Promise((resolve9) => {
8191
+ return await new Promise((resolve8) => {
8363
8192
  if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
8364
8193
  throw new TypeError("Invalid command arguments");
8365
8194
  if (options.input !== void 0 && typeof options.input !== "string")
@@ -8402,7 +8231,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8402
8231
  settled = true;
8403
8232
  if (timer)
8404
8233
  clearTimeout(timer);
8405
- resolve9(result);
8234
+ resolve8(result);
8406
8235
  };
8407
8236
  const interrupt = (reason, message) => {
8408
8237
  interrupted = true;
@@ -8589,6 +8418,47 @@ watch_certificate &
8589
8418
  `;
8590
8419
  }
8591
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
+
8592
8462
  // src/container/deploy-runtime-script.ts
8593
8463
  function createRuntimeConfigurationScript(primaryService) {
8594
8464
  return String.raw`configuration_mount() {
@@ -9248,7 +9118,7 @@ if [ "$partial" = true ]; then
9248
9118
  fi
9249
9119
  ;;
9250
9120
  esac
9251
- # 目录属于完整Web拓扑;旧版本缺省为根目录,服务增量不能意外丢失或改变既有目录。
9121
+ # Web 挂载目录由完整拓扑确定,服务增量必须匹配已部署的目录。
9252
9122
  current_web_base=$(deployment_json read metadata.web-base-path string "$source_dir/manifest.json")
9253
9123
  previous_web_base=$(deployment_json read metadata.web-base-path string "$root/releases/$previous/manifest.json")
9254
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; }
@@ -10144,7 +10014,7 @@ async function createDeploymentBundle(options) {
10144
10014
  const metadata = {
10145
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}` : ""}
10146
10016
  `,
10147
- "web-base-path": `${artifact.webBasePath ?? ""}
10017
+ "web-base-path": `${artifact.webBasePath}
10148
10018
  `,
10149
10019
  "health-paths": `${options.healthPaths.join("\n")}
10150
10020
  `,
@@ -10157,7 +10027,6 @@ async function createDeploymentBundle(options) {
10157
10027
  "node-runtime": String(nodeRuntime),
10158
10028
  "proxy-service": options.hasWebConfig ? options.proxyService ?? "web" : "",
10159
10029
  "https": options.httpsAssets?.https ?? "",
10160
- "managed-project": managed["managed-project"],
10161
10030
  // 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
10162
10031
  "managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
10163
10032
  "managed-volumes": managed["managed-volumes"],
@@ -10354,6 +10223,87 @@ async function copyArchive(source, destination) {
10354
10223
  await pipeline(createReadStream(source), digest, createWriteStream(destination, { mode: 384 }));
10355
10224
  return hash.digest("hex");
10356
10225
  }
10226
+
10227
+ // src/container/deploy-download-error.ts
10228
+ var PAGE_STYLE = `
10229
+ * { box-sizing: border-box; }
10230
+ body {
10231
+ margin: 0;
10232
+ min-height: 100vh;
10233
+ display: grid;
10234
+ place-items: center;
10235
+ padding: 24px;
10236
+ background: #f1f5f9;
10237
+ color: #172033;
10238
+ font-family: Inter, system-ui, "Microsoft YaHei", sans-serif;
10239
+ line-height: 1.7;
10240
+ }
10241
+ main {
10242
+ width: min(100%, 640px);
10243
+ padding: clamp(24px, 6vw, 48px);
10244
+ border: 1px solid #dbe3ee;
10245
+ border-top: 4px solid #2563eb;
10246
+ border-radius: 12px;
10247
+ background: #fff;
10248
+ }
10249
+ h1 { margin: 0 0 16px; font-size: clamp(24px, 5vw, 32px); line-height: 1.4; }
10250
+ p { margin: 0; color: #475569; font-size: 16px; }
10251
+ `;
10252
+ function escapeHtml(value) {
10253
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
10254
+ }
10255
+ function renderPage() {
10256
+ return `<!doctype html>
10257
+ <html lang="$hai_download_language">
10258
+ <head>
10259
+ <meta charset="utf-8">
10260
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10261
+ <meta name="robots" content="noindex">
10262
+ <meta name="referrer" content="no-referrer">
10263
+ <link rel="icon" href="data:,">
10264
+ <title>$hai_download_title</title>
10265
+ <style>${PAGE_STYLE}</style>
10266
+ </head>
10267
+ <body><main><h1>$hai_download_title</h1><p>$hai_download_instruction</p></main></body>
10268
+ </html>`;
10269
+ }
10270
+ function renderDownloadErrorMaps() {
10271
+ return `map "$request_method:$args" $hai_signed_download {
10272
+ default 0;
10273
+ "~*^(GET|HEAD):(?:[^&]*&)*X-Amz-Signature=[^&]+(?:&|$)" 1;
10274
+ }
10275
+ map $http_accept_language $hai_download_language {
10276
+ default zh-CN;
10277
+ ~*^en en-US;
10278
+ ~*^ja ja-JP;
10279
+ }
10280
+ map $hai_download_language $hai_download_title {
10281
+ default ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableTitle", { locale: "zh-CN" })))};
10282
+ en-US ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableTitle", { locale: "en-US" })))};
10283
+ ja-JP ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableTitle", { locale: "ja-JP" })))};
10284
+ }
10285
+ map $hai_download_language $hai_download_instruction {
10286
+ default ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableInstruction", { locale: "zh-CN" })))};
10287
+ en-US ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableInstruction", { locale: "en-US" })))};
10288
+ ja-JP ${JSON.stringify(escapeHtml(deployM("deploy_downloadUnavailableInstruction", { locale: "ja-JP" })))};
10289
+ }
10290
+ `;
10291
+ }
10292
+ function renderDownloadErrorLocation() {
10293
+ return ` location @hai_download_unavailable {
10294
+ access_log off;
10295
+ error_log /dev/null;
10296
+ types { }
10297
+ default_type text/html;
10298
+ charset utf-8;
10299
+ add_header Cache-Control "private, no-store" always;
10300
+ add_header Referrer-Policy no-referrer always;
10301
+ add_header X-Content-Type-Options nosniff always;
10302
+ add_header Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
10303
+ return 404 ${JSON.stringify(renderPage())};
10304
+ }
10305
+ `;
10306
+ }
10357
10307
  var CERTBOT_IMAGE = "docker.io/certbot/certbot:v5.9.0";
10358
10308
  function createHttpsAssets(config, projectDirectory) {
10359
10309
  const assets = {
@@ -11088,6 +11038,7 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
11088
11038
  }
11089
11039
  `).join("");
11090
11040
  return `include /tmp/hai-dns.conf;
11041
+ ${renderDownloadErrorMaps()}
11091
11042
  ${renderResolvedUpstream("hai_serv_backend", "serv", port)}${options.storageBucket ? renderStorageUpstream(options.storageBucket, options.storageBasePath) : ""}server {
11092
11043
  listen ${webPort};
11093
11044
  server_name _;
@@ -11171,17 +11122,7 @@ function generateApplicationProxyConfig(options) {
11171
11122
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
11172
11123
  const trustedEdges = renderTrustedEdgeProxies(options.trustedEdgeProxies);
11173
11124
  const forwardedProtocol = options.trustedEdgeProxies?.length ? "$hai_request_scheme" : "$scheme";
11174
- return `include /tmp/hai-dns.conf;
11175
- ${renderResolvedUpstream("hai_application_backend", primaryService, port)}${renderTrustedEdgeProtocol(options.trustedEdgeProxies)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
11176
- listen 8080;
11177
- server_name _;
11178
- ${trustedEdges} large_client_header_buffers 4 32k;
11179
- ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
11180
- access_log off;
11181
- return 200 'ok';
11182
- }
11183
- location / {
11184
- client_max_body_size ${bodyLimits.encryptedPayloadBytes};
11125
+ const proxySettings = ` client_max_body_size ${bodyLimits.encryptedPayloadBytes};
11185
11126
  proxy_pass http://hai_application_backend;
11186
11127
  proxy_http_version 1.1;
11187
11128
  proxy_set_header Host $http_host;
@@ -11195,7 +11136,29 @@ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath)
11195
11136
  proxy_request_buffering off;
11196
11137
  proxy_buffering off;
11197
11138
  proxy_buffer_size 32k;
11198
- proxy_buffers 8 32k;
11139
+ proxy_buffers 8 32k;`;
11140
+ return `include /tmp/hai-dns.conf;
11141
+ ${renderDownloadErrorMaps()}
11142
+ ${renderResolvedUpstream("hai_application_backend", primaryService, port)}${renderTrustedEdgeProtocol(options.trustedEdgeProxies)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
11143
+ listen 8080;
11144
+ server_name _;
11145
+ ${trustedEdges} large_client_header_buffers 4 32k;
11146
+ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
11147
+ access_log off;
11148
+ return 200 'ok';
11149
+ }
11150
+ ${renderDownloadErrorLocation()} location / {
11151
+ recursive_error_pages on;
11152
+ error_page 418 = @hai_download_proxy;
11153
+ if ($hai_signed_download) { return 418; }
11154
+ ${proxySettings}
11155
+ }
11156
+ location @hai_download_proxy {
11157
+ access_log off;
11158
+ error_log /dev/null;
11159
+ proxy_intercept_errors on;
11160
+ error_page 404 = @hai_download_unavailable;
11161
+ ${proxySettings}
11199
11162
  }
11200
11163
  }
11201
11164
  `;
@@ -11234,24 +11197,35 @@ map "$hai_trusted_edge:$http_x_forwarded_proto" $hai_request_scheme {
11234
11197
  function renderWebLocation(webBasePath) {
11235
11198
  if (webBasePath && (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(webBasePath) || webBasePath.split("/").some((segment) => segment === "." || segment === "..") || ["/health", "/_hai/web-health"].includes(webBasePath)))
11236
11199
  throw new Error("Invalid web base path");
11237
- return webBasePath ? ` absolute_redirect off;
11200
+ const fallback = `${renderDownloadErrorLocation()} location @hai_spa_fallback {
11201
+ error_page 418 = @hai_download_unavailable;
11202
+ if ($hai_signed_download) { return 418; }
11203
+ try_files /index.html =404;
11204
+ }
11205
+ `;
11206
+ return fallback + (webBasePath ? ` absolute_redirect off;
11238
11207
  location = ${webBasePath} {
11239
11208
  return 308 ${webBasePath}/$is_args$args;
11240
11209
  }
11241
11210
  location ${webBasePath}/ {
11242
11211
  rewrite ^${webBasePath.replaceAll(".", "\\.").replaceAll("-", "\\-")}/(.*)$ /$1 break;
11243
- try_files $uri/index.html $uri /index.html =404;
11212
+ try_files $uri/index.html $uri @hai_spa_fallback;
11213
+ }
11214
+ location / {
11215
+ error_page 418 = @hai_download_unavailable;
11216
+ if ($hai_signed_download) { return 418; }
11217
+ return 404;
11244
11218
  }
11245
- location / { return 404; }
11246
11219
  ` : ` location / {
11247
- try_files $uri $uri/ /index.html;
11220
+ try_files $uri $uri/ @hai_spa_fallback;
11248
11221
  }
11249
- `;
11222
+ `);
11250
11223
  }
11251
11224
  function generateStaticWebConfig(port, webBasePath) {
11252
11225
  if (!Number.isInteger(port) || port < 1 || port > 65535)
11253
11226
  throw new Error("Invalid web proxy target");
11254
- return `server {
11227
+ return `${renderDownloadErrorMaps()}
11228
+ server {
11255
11229
  listen ${port};
11256
11230
  server_name _;
11257
11231
  root /usr/share/nginx/html;
@@ -11357,7 +11331,7 @@ function makeDeploymentId() {
11357
11331
  return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
11358
11332
  }
11359
11333
  function sleep(ms) {
11360
- return new Promise((resolve9) => setTimeout(resolve9, ms));
11334
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
11361
11335
  }
11362
11336
  function createDockerSshProvider(config, container) {
11363
11337
  const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
@@ -11389,16 +11363,16 @@ function createDockerSshProvider(config, container) {
11389
11363
  for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
11390
11364
  try {
11391
11365
  if (certificate) {
11392
- const healthy = await new Promise((resolve9) => {
11366
+ const healthy = await new Promise((resolve8) => {
11393
11367
  const request$1 = request(url, {
11394
11368
  ca: [...rootCertificates, certificate],
11395
11369
  timeout: 5e3
11396
11370
  }, (response) => {
11397
11371
  response.resume();
11398
- resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
11372
+ resolve8(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
11399
11373
  });
11400
- request$1.on("error", () => resolve9(false));
11401
- request$1.on("close", () => resolve9(false));
11374
+ request$1.on("error", () => resolve8(false));
11375
+ request$1.on("close", () => resolve8(false));
11402
11376
  request$1.on("timeout", () => request$1.destroy());
11403
11377
  request$1.end();
11404
11378
  });
@@ -11449,7 +11423,7 @@ function createDockerSshProvider(config, container) {
11449
11423
  const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
11450
11424
  const host = config.ssh?.host;
11451
11425
  const url = https ? `https://${https.domain}${https.port === 443 ? "" : `:${https.port}`}` : host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
11452
- const webBasePath = artifact.webBasePath ?? "";
11426
+ const webBasePath = artifact.webBasePath;
11453
11427
  const applicationUrl = `${url}${webBasePath ? `${webBasePath}/` : ""}`;
11454
11428
  const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && (!!webBasePath || !!https);
11455
11429
  const healthPaths = hasWebProxy ? ["/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : primaryService === "serv" ? [] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
@@ -12042,7 +12016,7 @@ function createVercelProvider() {
12042
12016
  );
12043
12017
  token = apiToken;
12044
12018
  const username = user.user.username || user.user.email;
12045
- logger6.info("Vercel authenticated", { user: username });
12019
+ logger6.info("Vercel authenticated");
12046
12020
  return ok(username);
12047
12021
  } catch (error) {
12048
12022
  logger6.error("Vercel authentication failed", { error });
@@ -12227,7 +12201,7 @@ async function pollDeploymentStatus(token, deploymentId) {
12227
12201
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
12228
12202
  return "error";
12229
12203
  }
12230
- await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL));
12204
+ await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL));
12231
12205
  }
12232
12206
  return "building";
12233
12207
  }
@@ -12252,7 +12226,7 @@ function createAliyunProvisioner() {
12252
12226
  accessKey = akId;
12253
12227
  secretKey = akSecret;
12254
12228
  signName = credentials.signName;
12255
- logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
12229
+ logger7.info("Aliyun authenticated");
12256
12230
  return ok(akId);
12257
12231
  } catch (error) {
12258
12232
  logger7.error("Aliyun authentication failed", { error });
@@ -12493,7 +12467,7 @@ function createR2Provisioner() {
12493
12467
  token = apiTok;
12494
12468
  accessKey = credentials.accessKeyId;
12495
12469
  secretKey = credentials.secretAccessKey;
12496
- logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
12470
+ logger9.info("Cloudflare R2 authenticated");
12497
12471
  return ok(acctId);
12498
12472
  } catch (error) {
12499
12473
  logger9.error("Cloudflare R2 authentication failed", { error });
@@ -12716,7 +12690,7 @@ function createUpstashProvisioner() {
12716
12690
  await listUpstashDatabases(userEmail, userKey);
12717
12691
  email = userEmail;
12718
12692
  apiKey = userKey;
12719
- logger11.info("Upstash authenticated", { email: userEmail });
12693
+ logger11.info("Upstash authenticated");
12720
12694
  return ok(userEmail);
12721
12695
  } catch (error) {
12722
12696
  logger11.error("Upstash authentication failed", { error });
@@ -12925,10 +12899,8 @@ var deploy = {
12925
12899
  } else {
12926
12900
  provider = createProviderByType(parsed.provider.type);
12927
12901
  const authResult = await provider.authenticate(parsed.provider.token);
12928
- if (!authResult.success) {
12902
+ if (!authResult.success)
12929
12903
  return err(authResult.error);
12930
- }
12931
- logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
12932
12904
  }
12933
12905
  currentProvisioners.clear();
12934
12906
  if (parsed.services) {
@@ -12942,14 +12914,16 @@ var deploy = {
12942
12914
  if (!provAuthResult.success) {
12943
12915
  logger12.error("Provisioner authentication failed", {
12944
12916
  service: serviceType,
12945
- provisioner: serviceConfig.provisioner
12917
+ provisioner: serviceConfig.provisioner,
12918
+ errorCode: provAuthResult.error.code,
12919
+ traceId: provAuthResult.error.traceId
12946
12920
  });
12947
12921
  currentProvisioners.clear();
12948
12922
  return err(provAuthResult.error);
12949
12923
  }
12950
12924
  logger12.info("Provisioner authenticated", {
12951
12925
  service: serviceType,
12952
- user: provAuthResult.data
12926
+ provisioner: provisioner.name
12953
12927
  });
12954
12928
  currentProvisioners.set(serviceType, provisioner);
12955
12929
  }
@@ -13048,8 +13022,6 @@ var deploy = {
13048
13022
  if (!currentProvider && !currentContainerProvider) {
13049
13023
  return notInitialized.result();
13050
13024
  }
13051
- if (options && "projectName" in options && options.projectName !== void 0)
13052
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
13053
13025
  logger12.info("Starting deployment", { appDir, environment: options?.environment });
13054
13026
  const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
13055
13027
  const scanResult = await scanApp(appDir, scanOptions);
@@ -13783,8 +13755,6 @@ function describeMiddlewareBackend(backend) {
13783
13755
  return deployM(backend.purpose);
13784
13756
  }
13785
13757
  async function createDeploymentPlan(appDir, input, options = {}) {
13786
- if ("projectName" in options && options.projectName !== void 0)
13787
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
13788
13758
  const parsed = DeployConfigSchema.safeParse(input);
13789
13759
  if (!parsed.success) {
13790
13760
  return err({
@@ -13985,389 +13955,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
13985
13955
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
13986
13956
  }
13987
13957
  }
13988
- var nameSchema = z.string().regex(/^[a-z0-9][\w.-]{0,127}$/i);
13989
- 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));
13990
- var importSchema = z.strictObject({
13991
- version: z.literal(1),
13992
- projectName: nameSchema,
13993
- credentials: z.strictObject({
13994
- DB_PASSWORD: passwordSchema.optional(),
13995
- STORAGE_PASSWORD: passwordSchema.optional(),
13996
- LDAP_PASSWORD: passwordSchema.optional()
13997
- }),
13998
- services: z.array(z.strictObject({
13999
- service: z.enum(["db", "vecdb", "storage", "ldap"]),
14000
- container: nameSchema,
14001
- kind: z.enum(["postgresql", "mysql", "s3", "ldap"]),
14002
- volumes: z.array(nameSchema).min(1).max(8),
14003
- username: z.string().min(1).max(128).default("hai"),
14004
- database: z.string().min(1).max(128).default("hai"),
14005
- bucket: z.string().regex(/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/).default("hai"),
14006
- accessKeyId: z.string().min(1).max(128).default("hai-storage"),
14007
- region: z.string().regex(/^[a-z0-9-]+$/).default("us-east-1"),
14008
- bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
14009
- })).min(1).max(4)
14010
- });
14011
- var groups2 = {
14012
- db: "DB_PASSWORD",
14013
- vecdb: "DB_PASSWORD",
14014
- storage: "STORAGE_PASSWORD",
14015
- ldap: "LDAP_PASSWORD"
14016
- };
14017
- 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;
14018
- try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
14019
- if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
14020
- const state=path.join(root,'managed-secrets.env');try{fs.lstatSync(state);throw Error('exists')}catch(error){if(error.code!=='ENOENT')throw error}
14021
- 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);
14022
- try{fs.writeFileSync(fd,content);fs.fsyncSync(fd)}finally{fs.closeSync(fd)}fs.renameSync(temporary,state);temporary=undefined;
14023
- const directory=fs.openSync(root,fs.constants.O_RDONLY);try{fs.fsyncSync(directory)}finally{fs.closeSync(directory)}process.stdout.write('COMMITTED\n');
14024
- }catch{process.exitCode=1}finally{if(temporary)try{fs.unlinkSync(temporary)}catch{}}})()`;
14025
- var s3VerificationScript = String.raw`(async()=>{try{
14026
- const {createHash,createHmac}=require('node:crypto');let input='';for await(const chunk of process.stdin)input+=chunk;
14027
- const {endpoint,bucket,accessKeyId,secret,region}=JSON.parse(input);const url=new URL('/'+encodeURIComponent(bucket),endpoint);
14028
- const hash=value=>createHash('sha256').update(value).digest('hex');const hmac=(key,value)=>createHmac('sha256',key).update(value).digest();
14029
- const date=new Date().toISOString().replace(/[:-]|\.\d{3}/g,'');const day=date.slice(0,8);const payload=hash('');
14030
- 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';
14031
- const scope=day+'/'+region+'/s3/aws4_request';const canonical=['HEAD',url.pathname,'',headers,signed,payload].join('\n');
14032
- 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');
14033
- 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}});
14034
- await response.body?.cancel();if(response.ok)process.stdout.write('VERIFIED');else process.exitCode=1;
14035
- }catch{process.exitCode=1}})()`;
14036
- async function importDeploymentSecrets(options) {
14037
- let lock;
14038
- let commitAttempted = false;
14039
- let phase = "input";
14040
- try {
14041
- if (!["docker", "podman"].includes(options.runtime))
14042
- throw new Error("input");
14043
- const root = resolve(options.directory);
14044
- if (realpathSync(root) !== root || !lstatSync(root).isDirectory() || process.platform !== "win32" && lstatSync(root).mode & 18)
14045
- throw new Error("input");
14046
- const input = privateFile(resolve(options.inputFile));
14047
- const parsed = importSchema.safeParse(JSON.parse(input));
14048
- if (!parsed.success) {
14049
- return err({
14050
- ...HaiDeployError.CONFIG_ERROR,
14051
- message: deployM("deploy_secretImportFailed", {
14052
- params: { phase }
14053
- }),
14054
- ext: {
14055
- phase,
14056
- state: "unchanged",
14057
- fields: parsed.error.issues.map((issue) => issue.path.join("."))
14058
- }
14059
- });
14060
- }
14061
- const config = parsed.data;
14062
- const project = `hai-${config.projectName}`;
14063
- const selectedGroups = [
14064
- ...new Set(config.services.map((service) => groups2[service.service]))
14065
- ].sort();
14066
- 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])) {
14067
- throw new Error("input");
14068
- }
14069
- const secrets = Object.values(config.credentials).filter((value) => typeof value === "string");
14070
- const stateFile = join(root, "managed-secrets.env");
14071
- const lockFile = join(root, ".deploy.lock");
14072
- phase = "lock";
14073
- if (options.dryRun === false) {
14074
- if (process.platform !== "linux")
14075
- throw new Error("lock");
14076
- if (existsSync(lockFile) && (lstatSync(lockFile).isSymbolicLink() || !lstatSync(lockFile).isFile() || lstatSync(lockFile).nlink !== 1))
14077
- throw new Error("lock");
14078
- lock = await acquireImportLock(lockFile, root);
14079
- }
14080
- phase = "identity";
14081
- const volumes = [];
14082
- for (const service of config.services) {
14083
- const kindValid = service.service === "storage" ? service.kind === "s3" : service.service === "ldap" ? service.kind === "ldap" : ["postgresql", "mysql"].includes(service.kind);
14084
- if (!kindValid)
14085
- throw new Error("identity");
14086
- 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") {
14087
- throw new Error("identity");
14088
- }
14089
- const identity = await inspectContainer(options.runtime, service.container, secrets);
14090
- if (identity.project !== project || identity.service !== service.service || !identity.running)
14091
- throw new Error("identity");
14092
- for (const volume of service.volumes) {
14093
- if (!identity.volumes.includes(volume))
14094
- throw new Error("identity");
14095
- volumes.push({ group: groups2[service.service], name: volume });
14096
- }
14097
- phase = "verification";
14098
- const password = config.credentials[groups2[service.service]];
14099
- let verified = false;
14100
- if (service.kind === "s3") {
14101
- const helper2 = `hai-${config.projectName}-app`;
14102
- const application = await inspectContainer(options.runtime, helper2, secrets);
14103
- if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
14104
- throw new Error("identity");
14105
- const result = await runCommand(options.runtime, [
14106
- "exec",
14107
- "-i",
14108
- helper2,
14109
- "node",
14110
- "-e",
14111
- s3VerificationScript
14112
- ], {
14113
- input: JSON.stringify({
14114
- endpoint: `http://${service.container}:9000`,
14115
- bucket: service.bucket,
14116
- accessKeyId: service.accessKeyId,
14117
- secret: password,
14118
- region: service.region
14119
- }),
14120
- secrets,
14121
- timeoutMs: 15e3,
14122
- maxCaptureChars: 1024
14123
- });
14124
- verified = result.success && result.data.stdout.trim() === "VERIFIED";
14125
- } else {
14126
- const args = service.kind === "ldap" ? [
14127
- "exec",
14128
- "-i",
14129
- service.container,
14130
- "ldapwhoami",
14131
- "-x",
14132
- "-H",
14133
- "ldap://127.0.0.1:389",
14134
- "-D",
14135
- service.bindDn,
14136
- "-y",
14137
- "/dev/stdin"
14138
- ] : [
14139
- "exec",
14140
- "-i",
14141
- service.container,
14142
- "sh",
14143
- "-c",
14144
- 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"',
14145
- "hai-secret-check",
14146
- service.username,
14147
- service.database,
14148
- service.container
14149
- ];
14150
- const result = await runCommand(options.runtime, args, {
14151
- input: service.kind === "ldap" ? password : `${password}
14152
- `,
14153
- secrets,
14154
- timeoutMs: 15e3,
14155
- maxCaptureChars: 1024
14156
- });
14157
- verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
14158
- if (verified && service.kind === "mysql") {
14159
- const rootCheck = await runCommand(options.runtime, [
14160
- ...args.slice(0, -3),
14161
- "root",
14162
- service.database,
14163
- service.container
14164
- ], {
14165
- input: `${password}
14166
- `,
14167
- secrets,
14168
- timeoutMs: 15e3,
14169
- maxCaptureChars: 1024
14170
- });
14171
- verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
14172
- const invalidPassword = `hai-import-invalid-${randomUUID()}`;
14173
- for (const username of [service.username, "root"]) {
14174
- if (!verified)
14175
- break;
14176
- const negative = await runCommand(options.runtime, [
14177
- ...args.slice(0, -3),
14178
- username,
14179
- service.database,
14180
- service.container
14181
- ], {
14182
- input: `${invalidPassword}
14183
- `,
14184
- secrets: [...secrets, invalidPassword],
14185
- timeoutMs: 15e3,
14186
- maxCaptureChars: 1024
14187
- });
14188
- verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
14189
- }
14190
- }
14191
- }
14192
- if (!verified)
14193
- throw new Error("verification");
14194
- phase = "identity";
14195
- }
14196
- phase = "state";
14197
- const uniqueVolumes = [
14198
- ...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()
14199
- ].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
14200
- const lines = [
14201
- "FORMAT_VERSION=2",
14202
- `PROJECT=${project}`,
14203
- ...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`),
14204
- ...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)
14205
- ];
14206
- let changed = false;
14207
- if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
14208
- const existing = readImportedState(privateFile(stateFile));
14209
- 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}`)))
14210
- throw new Error("state");
14211
- } else if (options.dryRun === false) {
14212
- commitAttempted = true;
14213
- if (!lock || !await lock.commit(`${lines.join("\n")}
14214
- `))
14215
- throw new Error("lock");
14216
- changed = true;
14217
- }
14218
- return ok({
14219
- projectName: config.projectName,
14220
- stateVersion: 2,
14221
- groups: selectedGroups,
14222
- volumes: uniqueVolumes.map((volume) => volume.name),
14223
- changed
14224
- });
14225
- } catch {
14226
- return err({
14227
- ...HaiDeployError.CONFIG_ERROR,
14228
- message: deployM("deploy_secretImportFailed", {
14229
- params: { phase }
14230
- }),
14231
- ext: {
14232
- phase,
14233
- state: commitAttempted ? "unknown" : "unchanged"
14234
- }
14235
- });
14236
- } finally {
14237
- await lock?.release();
14238
- }
14239
- }
14240
- function readImportedState(text) {
14241
- const result = {};
14242
- for (const line of text.trimEnd().split("\n")) {
14243
- const match = /^(FORMAT_VERSION|PROJECT|DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD|VOLUME_[1-9]\d*)=([^=]+)$/.exec(line);
14244
- if (!match || Object.hasOwn(result, match[1]))
14245
- throw new Error("state");
14246
- const key = match[1];
14247
- const value = match[2];
14248
- if (key.endsWith("_PASSWORD")) {
14249
- if (!/^(?:[a-f0-9]{2})+$/.test(value))
14250
- throw new Error("state");
14251
- const decoded = Buffer.from(value, "hex").toString("utf8");
14252
- if (!passwordSchema.safeParse(decoded).success || Buffer.from(decoded, "utf8").toString("hex") !== value)
14253
- throw new Error("state");
14254
- } else if (key.startsWith("VOLUME_") && !/^(?:DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[a-zA-Z0-9][\w.-]*$/.test(value)) {
14255
- throw new Error("state");
14256
- }
14257
- result[key] = value;
14258
- }
14259
- if (result.FORMAT_VERSION !== "2" || !nameSchema.safeParse(result.PROJECT).success)
14260
- throw new Error("state");
14261
- return result;
14262
- }
14263
- function isSymbolicLink(path) {
14264
- try {
14265
- return lstatSync(path).isSymbolicLink();
14266
- } catch {
14267
- return false;
14268
- }
14269
- }
14270
- function privateFile(path) {
14271
- return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
14272
- }
14273
- async function inspectContainer(runtime, container, secrets) {
14274
- const result = await runCommand(runtime, [
14275
- "inspect",
14276
- "--format",
14277
- "{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}",
14278
- container
14279
- ], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
14280
- if (!result.success || result.data.stdoutTruncated)
14281
- throw new Error("identity");
14282
- const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
14283
- const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
14284
- const mounts = z.array(z.object({
14285
- Type: z.string(),
14286
- Name: z.string().optional()
14287
- })).parse(JSON.parse(mountsText));
14288
- const networks = z.record(z.string(), z.object({
14289
- NetworkID: z.string()
14290
- })).parse(JSON.parse(networksText) ?? {});
14291
- const dockerProject = labels["com.docker.compose.project"] || void 0;
14292
- const podmanProject = labels["io.podman.compose.project"] || void 0;
14293
- const dockerService = labels["com.docker.compose.service"] || void 0;
14294
- const podmanService = labels["io.podman.compose.service"] || void 0;
14295
- if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
14296
- throw new Error("identity");
14297
- }
14298
- return {
14299
- project: dockerProject ?? podmanProject,
14300
- service: dockerService ?? podmanService,
14301
- running: runningText === "true",
14302
- volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []),
14303
- networks: Object.values(networks).map((network) => network.NetworkID)
14304
- };
14305
- }
14306
- async function acquireImportLock(path, root) {
14307
- return new Promise((resolveLock, reject) => {
14308
- const child = spawn("flock", [
14309
- "-n",
14310
- path,
14311
- process.execPath,
14312
- "-e",
14313
- importCommitScript,
14314
- root
14315
- ], {
14316
- stdio: ["pipe", "pipe", "ignore"],
14317
- windowsHide: true
14318
- });
14319
- let acquired = false;
14320
- let exited = false;
14321
- let output = "";
14322
- const finished = new Promise((done) => child.once("close", () => {
14323
- exited = true;
14324
- done();
14325
- }));
14326
- child.stdin.on("error", () => {
14327
- });
14328
- const timer = setTimeout(() => {
14329
- child.stdin.end();
14330
- child.kill();
14331
- reject(new Error("lock"));
14332
- }, 5e3);
14333
- child.once("error", () => {
14334
- clearTimeout(timer);
14335
- reject(new Error("lock"));
14336
- });
14337
- child.once("exit", () => {
14338
- if (!acquired) {
14339
- clearTimeout(timer);
14340
- reject(new Error("lock"));
14341
- }
14342
- });
14343
- child.stdout.on("data", (data) => {
14344
- output += data.toString();
14345
- if (acquired || !output.includes("\n"))
14346
- return;
14347
- if (!output.startsWith("LOCKED\n") || exited) {
14348
- child.stdin.end();
14349
- reject(new Error("lock"));
14350
- return;
14351
- }
14352
- acquired = true;
14353
- clearTimeout(timer);
14354
- resolveLock({
14355
- commit: async (content) => {
14356
- if (exited)
14357
- return false;
14358
- child.stdin.end(JSON.stringify({ content }));
14359
- await finished;
14360
- return child.exitCode === 0 && output === "LOCKED\nCOMMITTED\n";
14361
- },
14362
- release: async () => {
14363
- child.stdin.end();
14364
- await finished;
14365
- }
14366
- });
14367
- });
14368
- });
14369
- }
14370
13958
 
14371
- 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 };
14372
13960
  //# sourceMappingURL=index.js.map
14373
13961
  //# sourceMappingURL=index.js.map