@h-ai/deploy 1.0.0-beta.17 → 1.0.0-beta.19

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,11 +1,12 @@
1
1
  import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
2
- import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, mkdirSync, readdirSync, realpathSync, chmodSync, statSync, mkdtempSync, copyFileSync, renameSync, rmSync, createWriteStream } from 'fs';
3
- import { resolve, extname, join, dirname, relative, basename, sep } from 'path';
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';
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
6
  import { spawn, execFileSync } from 'child_process';
7
7
  import process from 'process';
8
8
  import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
9
+ import { Buffer, constants as constants$1 } from 'buffer';
9
10
  import { homedir, tmpdir } from 'os';
10
11
  import { request } from 'https';
11
12
  import { rootCertificates } from 'tls';
@@ -16,19 +17,28 @@ import { Transform } from 'stream';
16
17
  import { pipeline } from 'stream/promises';
17
18
  import { readdir, readFile } from 'fs/promises';
18
19
  import { setTimeout as setTimeout$1 } from 'timers/promises';
19
- import { Buffer } from 'buffer';
20
20
  import { isIP } from 'net';
21
21
 
22
22
  // src/deploy-artifact.ts
23
23
  var DeploymentArtifactSchema = z.object({
24
24
  /** 清单格式版本,与应用版本和部署环境无关。 */
25
- version: z.union([z.literal(4), z.literal(5)]),
25
+ version: z.union([
26
+ z.literal(4),
27
+ z.literal(5)
28
+ ]),
26
29
  /** 主运行服务:独立后端为 serv,独立浏览器站点(静态或 SSR)为 web。 */
27
30
  primaryService: z.enum(["serv", "web"]),
28
31
  /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
29
32
  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),
30
33
  /** 本次构建标识;同一应用版本允许重复构建。 */
31
34
  buildId: z.string().max(128).regex(/^[a-z0-9][\w.+-]*$/i),
35
+ /** 构建启动时观察到的 Git 来源;历史清单可以省略。 */
36
+ source: z.object({
37
+ /** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
38
+ commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
39
+ /** 工程及所选镜像上下文/构建定义有未提交改动时为 true;跨仓库或无法检查时为 null。 */
40
+ dirty: z.boolean().nullable()
41
+ }).optional(),
32
42
  /** 只包含所选镜像的构建不能用于首次安装。 */
33
43
  partial: z.boolean().default(false),
34
44
  /** 是否包含浏览器端;仅构建 serv 时也保留完整工程的 Web 拓扑。 */
@@ -49,6 +59,8 @@ var DeploymentArtifactSchema = z.object({
49
59
  imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
50
60
  /** tar 内容的 SHA-256,用于部署前校验。 */
51
61
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
62
+ /** 引擎实际构建的镜像 OS/架构;历史清单可以省略。 */
63
+ platform: z.string().regex(/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/).optional(),
52
64
  /** 容器内部监听端口;服务器公开端口由部署选择。 */
53
65
  containerPort: z.number().int().min(1).max(65535).optional()
54
66
  })).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
@@ -552,8 +564,13 @@ var SshConfigSchema = z.object({
552
564
  }).superRefine((ssh, context) => {
553
565
  if (ssh.auth === "key" || ssh.auth === void 0 && !ssh.password)
554
566
  return;
555
- if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
556
- context.addIssue({ code: "custom", path: ["password"], message: deployM("deploy_sshPasswordInvalid") });
567
+ if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023) {
568
+ context.addIssue({
569
+ code: "custom",
570
+ path: ["password"],
571
+ message: deployM("deploy_sshPasswordInvalid")
572
+ });
573
+ }
557
574
  });
558
575
  var HttpsDomainSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
559
576
  var httpsOptions = {
@@ -590,8 +607,13 @@ var PortExposeSchema = z.object({
590
607
  /** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
591
608
  https: HttpsConfigSchema.optional().describe("\u53EF\u9009 HTTPS \u5165\u53E3\uFF1Bfiles \u4F7F\u7528\u5DF2\u6709\u8BC1\u4E66\uFF0Cletsencrypt \u4F7F\u7528\u5B98\u65B9 Certbot \u7533\u8BF7\u7EED\u671F\uFF0C\u7701\u7565\u4FDD\u6301 HTTP")
592
609
  }).superRefine((expose, context) => {
593
- if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
594
- context.addIssue({ code: "custom", path: ["https"], message: deployM("deploy_httpsPortsInvalid") });
610
+ if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80)) {
611
+ context.addIssue({
612
+ code: "custom",
613
+ path: ["https"],
614
+ message: deployM("deploy_httpsPortsInvalid")
615
+ });
616
+ }
595
617
  });
596
618
  var ExposeSchema = PortExposeSchema;
597
619
  var DockerSshProviderConfigSchema = z.object({
@@ -608,7 +630,12 @@ var DockerSshProviderConfigSchema = z.object({
608
630
  /** 应用暴露方式 */
609
631
  expose: ExposeSchema,
610
632
  /** Nginx 前方可信负载均衡的 IP/CIDR;省略时不接受外部转发头。 */
611
- trustedEdgeProxies: z.array(z.union([z.ipv4(), z.ipv6(), z.cidrv4(), z.cidrv6()])).max(1e3).optional()
633
+ trustedEdgeProxies: z.array(z.union([
634
+ z.ipv4(),
635
+ z.ipv6(),
636
+ z.cidrv4(),
637
+ z.cidrv6()
638
+ ])).max(1e3).optional()
612
639
  });
613
640
  var ProviderConfigSchema = z.discriminatedUnion("type", [
614
641
  VercelProviderConfigSchema,
@@ -649,23 +676,52 @@ var ServicesConfigSchema = z.object({
649
676
  sms: z.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
650
677
  }).optional();
651
678
  function provisionerFields(provisioner) {
652
- const schema = [NeonServiceSchema, UpstashServiceSchema, R2ServiceSchema, ResendServiceSchema, AliyunSmsServiceSchema].find((item) => item.shape.provisioner.value === provisioner);
679
+ const schema = [
680
+ NeonServiceSchema,
681
+ UpstashServiceSchema,
682
+ R2ServiceSchema,
683
+ ResendServiceSchema,
684
+ AliyunSmsServiceSchema
685
+ ].find((item) => item.shape.provisioner.value === provisioner);
653
686
  return schema ? Object.keys(schema.shape).filter((name) => name !== "provisioner") : [];
654
687
  }
655
- var reservedComposeHosts = /* @__PURE__ */ new Set(["localhost", "serv", "web", "gateway", "certbot", "db", "cache", "storage", "vecdb", "ldap", "email", "sms"]);
688
+ var reservedComposeHosts = /* @__PURE__ */ new Set([
689
+ "localhost",
690
+ "serv",
691
+ "web",
692
+ "gateway",
693
+ "certbot",
694
+ "db",
695
+ "cache",
696
+ "storage",
697
+ "vecdb",
698
+ "ldap",
699
+ "email",
700
+ "sms"
701
+ ]);
656
702
  var composeHostname = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
657
703
  var ComposeServiceConfigSchema = z.strictObject({
658
704
  /** 容器内的主机名到 IPv4 或 IPv6 地址映射,不能覆盖框架内部服务名。 */
659
- extra_hosts: z.record(z.string(), z.union([z.ipv4(), z.ipv6()], {
705
+ extra_hosts: z.record(z.string(), z.union([
706
+ z.ipv4(),
707
+ z.ipv6()
708
+ ], {
660
709
  error: () => deployM("deploy_composeHostAddressInvalid")
661
710
  })).superRefine((hosts, ctx) => {
662
711
  const seen = /* @__PURE__ */ new Set();
663
712
  for (const host of Object.keys(hosts)) {
664
713
  const key = host.toLowerCase();
665
714
  if (host.length > 253 || !composeHostname.test(host)) {
666
- ctx.addIssue({ code: "custom", message: deployM("deploy_composeHostnameInvalid") });
715
+ ctx.addIssue({
716
+ code: "custom",
717
+ message: deployM("deploy_composeHostnameInvalid")
718
+ });
667
719
  } else if (reservedComposeHosts.has(key) || seen.has(key)) {
668
- ctx.addIssue({ code: "custom", path: [host], message: deployM("deploy_composeHostnameConflict") });
720
+ ctx.addIssue({
721
+ code: "custom",
722
+ path: [host],
723
+ message: deployM("deploy_composeHostnameConflict")
724
+ });
669
725
  }
670
726
  seen.add(key);
671
727
  }
@@ -703,8 +759,12 @@ function validateComposeServices(container, artifact) {
703
759
  const field2 = `deploy.container.compose.services.${unavailable}`;
704
760
  return err({
705
761
  ...HaiDeployError.CONFIG_ERROR,
706
- message: deployM("deploy_composeServiceUnavailable", { params: { field: field2 } }),
707
- ext: { fields: [field2] }
762
+ message: deployM("deploy_composeServiceUnavailable", {
763
+ params: { field: field2 }
764
+ }),
765
+ ext: {
766
+ fields: [field2]
767
+ }
708
768
  });
709
769
  }
710
770
  function resolveImageReference(reference, source) {
@@ -721,8 +781,11 @@ function resolveImageReference(reference, source) {
721
781
  parts.unshift("library");
722
782
  const mappings = source.type === "cn" ? { "docker.io": "docker.1ms.run", "ghcr.io": "ghcr.1ms.run" } : source.registries;
723
783
  const replacement = mappings?.[registry];
724
- if (!replacement || typeof replacement !== "string")
725
- throw new Error(deployM("deploy_imageSourceMissing", { params: { registry } }));
784
+ if (!replacement || typeof replacement !== "string") {
785
+ throw new Error(deployM("deploy_imageSourceMissing", {
786
+ params: { registry }
787
+ }));
788
+ }
726
789
  if (replacement.split("/").some((part) => part === "." || part === ".."))
727
790
  throw new Error(deployM("deploy_downloadInvalid"));
728
791
  let url;
@@ -739,20 +802,32 @@ function resolveImageReference(reference, source) {
739
802
  function systemProxy() {
740
803
  if (process.platform === "win32") {
741
804
  const script = "$p=Get-ItemProperty -LiteralPath 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'; @{enabled=($p.ProxyEnable -eq 1);server=$p.ProxyServer;bypass=$p.ProxyOverride;pac=$p.AutoConfigURL}|ConvertTo-Json -Compress";
742
- const value = JSON.parse(execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", windowsHide: true, timeout: 5e3 }));
805
+ const value = JSON.parse(execFileSync("powershell.exe", [
806
+ "-NoProfile",
807
+ "-NonInteractive",
808
+ "-Command",
809
+ script
810
+ ], { encoding: "utf8", windowsHide: true, timeout: 5e3 }));
743
811
  if (value.pac)
744
812
  throw new Error(deployM("deploy_proxySystemUnsupported"));
745
813
  if (!value.enabled || !value.server)
746
814
  return {};
747
815
  const entries = Object.fromEntries(value.server.split(";").map((item) => item.includes("=") ? item.split("=", 2) : ["all", item]));
748
- return { http: entries.http ?? entries.all, https: entries.https ?? entries.http ?? entries.all, bypass: value.bypass?.split(";").filter((item) => item !== "<local>").join(",") };
816
+ return {
817
+ http: entries.http ?? entries.all,
818
+ https: entries.https ?? entries.http ?? entries.all,
819
+ bypass: value.bypass?.split(";").filter((item) => item !== "<local>").join(",")
820
+ };
749
821
  }
750
822
  if (process.platform === "darwin") {
751
823
  const output = execFileSync("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 5e3 });
752
824
  const get = (key) => output.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
753
825
  if (get("ProxyAutoConfigEnable") === "1" || get("ProxyAutoDiscoveryEnable") === "1")
754
826
  throw new Error(deployM("deploy_proxySystemUnsupported"));
755
- return { http: get("HTTPEnable") === "1" ? `http://${get("HTTPProxy")}:${get("HTTPPort")}` : void 0, https: get("HTTPSEnable") === "1" ? `http://${get("HTTPSProxy")}:${get("HTTPSPort")}` : void 0 };
827
+ return {
828
+ http: get("HTTPEnable") === "1" ? `http://${get("HTTPProxy")}:${get("HTTPPort")}` : void 0,
829
+ https: get("HTTPSEnable") === "1" ? `http://${get("HTTPSProxy")}:${get("HTTPSPort")}` : void 0
830
+ };
756
831
  }
757
832
  throw new Error(deployM("deploy_proxySystemUnsupported"));
758
833
  }
@@ -780,7 +855,18 @@ function resolveDownloadProxy(config) {
780
855
  const bypass = mode === "none" ? "" : config?.bypass ?? values.bypass ?? "";
781
856
  if (/[\r\n\0]/.test(bypass))
782
857
  throw new Error(deployM("deploy_downloadInvalid"));
783
- return { HTTP_PROXY: http, http_proxy: http, HTTPS_PROXY: https, https_proxy: https, NO_PROXY: bypass, no_proxy: bypass, ALL_PROXY: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "", all_proxy: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "", FTP_PROXY: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : "", ftp_proxy: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : "" };
858
+ return {
859
+ HTTP_PROXY: http,
860
+ http_proxy: http,
861
+ HTTPS_PROXY: https,
862
+ https_proxy: https,
863
+ NO_PROXY: bypass,
864
+ no_proxy: bypass,
865
+ ALL_PROXY: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "",
866
+ all_proxy: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "",
867
+ FTP_PROXY: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : "",
868
+ ftp_proxy: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : ""
869
+ };
784
870
  }
785
871
 
786
872
  // src/deploy-config-shapes.json
@@ -2483,6 +2569,18 @@ var deploy_config_shapes_default = {
2483
2569
  type: "string",
2484
2570
  pattern: "^[A-Z_]\\w*$"
2485
2571
  }
2572
+ },
2573
+ gitEnvironmentRefs: {
2574
+ default: {},
2575
+ type: "object",
2576
+ propertyNames: {
2577
+ type: "string",
2578
+ pattern: "^[A-Z_]\\w*$"
2579
+ },
2580
+ additionalProperties: {
2581
+ type: "string",
2582
+ pattern: "^[A-Z_]\\w*$"
2583
+ }
2486
2584
  }
2487
2585
  },
2488
2586
  required: [
@@ -6108,13 +6206,95 @@ var deploy_config_shapes_default = {
6108
6206
  }
6109
6207
  }
6110
6208
  };
6209
+ function regular(metadata, options = {}) {
6210
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || metadata.size < 0n || metadata.size > BigInt(options.maxBytes ?? constants$1.MAX_LENGTH - 1) || options.private && process.platform !== "win32" && ((metadata.mode & 0o077n) !== 0n || process.getuid?.() !== 0 && metadata.uid !== BigInt(process.getuid?.() ?? -1))) {
6211
+ throw new Error("Unsafe local file metadata");
6212
+ }
6213
+ }
6214
+ function sameFile(left, right) {
6215
+ return left.dev === right.dev && left.ino === right.ino;
6216
+ }
6217
+ function readLocalFile(path, options = {}) {
6218
+ const before = lstatSync(path, { bigint: true });
6219
+ regular(before, options);
6220
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
6221
+ try {
6222
+ const opened = fstatSync(fd, { bigint: true });
6223
+ regular(opened, options);
6224
+ if (!sameFile(before, opened))
6225
+ throw new Error("Local file changed before opening");
6226
+ const size = Number(opened.size);
6227
+ const bytes = Buffer.alloc(size + 1);
6228
+ let length = 0;
6229
+ while (length < bytes.length) {
6230
+ const read = readSync(fd, bytes, length, bytes.length - length, null);
6231
+ if (!read)
6232
+ break;
6233
+ length += read;
6234
+ }
6235
+ const after = fstatSync(fd, { bigint: true });
6236
+ regular(after, options);
6237
+ if (!sameFile(opened, after) || length !== size || opened.size !== after.size || opened.mtimeNs !== after.mtimeNs || opened.ctimeNs !== after.ctimeNs) {
6238
+ throw new Error("Local file changed while reading");
6239
+ }
6240
+ return bytes.subarray(0, length);
6241
+ } finally {
6242
+ closeSync(fd);
6243
+ }
6244
+ }
6245
+ function existingFile(path) {
6246
+ try {
6247
+ const metadata = lstatSync(path, { bigint: true });
6248
+ regular(metadata);
6249
+ return metadata;
6250
+ } catch (error) {
6251
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
6252
+ return void 0;
6253
+ throw error;
6254
+ }
6255
+ }
6256
+ function writeLocalFile(path, content, privateFile2 = false) {
6257
+ const previous = existingFile(path);
6258
+ const mode = privateFile2 ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
6259
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
6260
+ let fd;
6261
+ let owned;
6262
+ let published = false;
6263
+ try {
6264
+ fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
6265
+ owned = fstatSync(fd, { bigint: true });
6266
+ fchmodSync(fd, mode);
6267
+ writeFileSync(fd, content, "utf8");
6268
+ fsyncSync(fd);
6269
+ closeSync(fd);
6270
+ fd = void 0;
6271
+ existingFile(path);
6272
+ const candidate = lstatSync(temporary, { bigint: true });
6273
+ regular(candidate);
6274
+ if (!sameFile(owned, candidate))
6275
+ throw new Error("Local temporary file ownership changed");
6276
+ renameSync(temporary, path);
6277
+ published = true;
6278
+ } finally {
6279
+ if (fd !== void 0)
6280
+ closeSync(fd);
6281
+ if (!published && owned) {
6282
+ try {
6283
+ const candidate = lstatSync(temporary, { bigint: true });
6284
+ if (candidate.isFile() && sameFile(owned, candidate))
6285
+ unlinkSync(temporary);
6286
+ } catch {
6287
+ }
6288
+ }
6289
+ }
6290
+ }
6111
6291
 
6112
6292
  // src/deploy-environment.ts
6113
6293
  var shapes = deploy_config_shapes_default;
6114
6294
  function environmentFile(path) {
6115
6295
  const values = {};
6116
6296
  if (existsSync(path)) {
6117
- for (const [name, value] of Object.entries(core.config.parseEnv(readFileSync(path, "utf8")))) {
6297
+ for (const [name, value] of Object.entries(core.config.parseEnv(readLocalFile(path).toString("utf8")))) {
6118
6298
  if (value !== void 0)
6119
6299
  values[name] = value;
6120
6300
  }
@@ -6125,7 +6305,15 @@ function mergeEnvironmentLayers(layers, prefix) {
6125
6305
  return core.config.mergeEnv(layers.map((layer) => Object.fromEntries(Object.entries(layer).filter(([name]) => !prefix || name === prefix || name.startsWith(`${prefix}_`)))));
6126
6306
  }
6127
6307
  function describeDeploymentVariableSources(directory, environment, values, names) {
6128
- const files = [`.env.${environment}.local`, ".env.local", `.env.${environment}`, ".env"].map((name) => ({ path: join(directory, name), values: environmentFile(join(directory, name)) }));
6308
+ const files = [
6309
+ `.env.${environment}.local`,
6310
+ ".env.local",
6311
+ `.env.${environment}`,
6312
+ ".env"
6313
+ ].map((name) => ({
6314
+ path: join(directory, name),
6315
+ values: environmentFile(join(directory, name))
6316
+ }));
6129
6317
  return names.filter((name) => values[name]).map((name) => {
6130
6318
  const source = process.env[name] !== void 0 ? "process.env" : files.find((file) => Object.hasOwn(file.values, name))?.path;
6131
6319
  return `${name}: ${source ?? ".env.runtime"}`;
@@ -6140,32 +6328,65 @@ function loadDeploymentConfig(name, path, values) {
6140
6328
  const project = JSON.parse(content);
6141
6329
  input = project && typeof project === "object" && !Array.isArray(project) ? project[name] : void 0;
6142
6330
  } catch {
6143
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
6331
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
6332
+ params: { path, line: "1" }
6333
+ }));
6334
+ }
6335
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
6336
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
6337
+ params: { path, line: "1" }
6338
+ }));
6144
6339
  }
6145
- if (!input || typeof input !== "object" || Array.isArray(input))
6146
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
6147
6340
  } else {
6148
6341
  const lineCounter = new LineCounter();
6149
6342
  const document = parseDocument(content, { lineCounter, prettyErrors: false });
6150
- if (document.errors.length)
6151
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
6343
+ if (document.errors.length) {
6344
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
6345
+ params: {
6346
+ path,
6347
+ line: String(lineCounter.linePos(document.errors[0].pos[0]).line)
6348
+ }
6349
+ }));
6350
+ }
6152
6351
  input = document.toJS();
6153
6352
  }
6154
- const resolved = core.config.resolve({ name, value: input, environment: values, shape: shapes[name], missing: "empty" });
6353
+ const resolved = core.config.resolve({
6354
+ name,
6355
+ value: input,
6356
+ environment: values,
6357
+ shape: shapes[name],
6358
+ missing: "empty"
6359
+ });
6155
6360
  if (!resolved.success) {
6156
6361
  const cause = resolved.error.cause;
6157
6362
  const fields = cause && typeof cause === "object" && "fields" in cause ? cause.fields : void 0;
6158
6363
  const safeFields = Array.isArray(fields) ? fields.filter((field2) => typeof field2 === "string" && /^HAI_[A-Z0-9_]+$/.test(field2)).join(", ") : "";
6159
- if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? ""))
6160
- return err({ ...resolved.error, message: deployM("deploy_reachEmptyProviderOverride", { params: { path, field: safeFields } }) });
6161
- return err({ ...resolved.error, message: deployM("deploy_configResolutionFailed", { params: { path, fields: safeFields || name } }) });
6364
+ if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? "")) {
6365
+ return err({
6366
+ ...resolved.error,
6367
+ message: deployM("deploy_reachEmptyProviderOverride", {
6368
+ params: { path, field: safeFields }
6369
+ })
6370
+ });
6371
+ }
6372
+ return err({
6373
+ ...resolved.error,
6374
+ message: deployM("deploy_configResolutionFailed", {
6375
+ params: { path, fields: safeFields || name }
6376
+ })
6377
+ });
6162
6378
  }
6163
6379
  const config = resolved.data;
6164
- if (config !== null && (typeof config !== "object" || Array.isArray(config)))
6165
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
6380
+ if (config !== null && (typeof config !== "object" || Array.isArray(config))) {
6381
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
6382
+ params: { path, line: "1" }
6383
+ }));
6384
+ }
6166
6385
  return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
6167
6386
  } catch {
6168
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
6387
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
6388
+ params: { path }
6389
+ }));
6169
6390
  }
6170
6391
  }
6171
6392
  function loadDeploymentEnvironment(options) {
@@ -6174,8 +6395,11 @@ function loadDeploymentEnvironment(options) {
6174
6395
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
6175
6396
  const envDir = resolve(options.envDir);
6176
6397
  const target = join(envDir, `.env.${environment}`);
6177
- if (options.requireFile !== false && !existsSync(target))
6178
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", { params: { path: target } }));
6398
+ if (options.requireFile !== false && !existsSync(target)) {
6399
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", {
6400
+ params: { path: target }
6401
+ }));
6402
+ }
6179
6403
  const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
6180
6404
  if (!loaded.success)
6181
6405
  return loaded;
@@ -6189,25 +6413,40 @@ function loadDeploymentEnvironment(options) {
6189
6413
  values.HAI_DEPLOY_ENVIRONMENT = environment;
6190
6414
  if (values.HAI_CORE_ENV === void 0) {
6191
6415
  const fallback = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
6192
- const configured = core.config.resolve({ name: "core", value: { env: fallback }, environment: values, shape: shapes.core });
6416
+ const configured = core.config.resolve({
6417
+ name: "core",
6418
+ value: { env: fallback },
6419
+ environment: values,
6420
+ shape: shapes.core
6421
+ });
6193
6422
  const settings = configured.success && configured.data && typeof configured.data === "object" ? configured.data : void 0;
6194
6423
  values.HAI_CORE_ENV = typeof settings?.env === "string" ? settings.env : fallback;
6195
6424
  }
6196
- return ok({ environment, envDir, values, targetValues: selected });
6425
+ return ok({
6426
+ environment,
6427
+ envDir,
6428
+ values,
6429
+ targetValues: selected
6430
+ });
6197
6431
  } catch {
6198
6432
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
6199
6433
  }
6200
6434
  }
6201
6435
  var reachGatewayEnvironment = /^HAI_REACH_PROVIDERS_\d+_GATEWAY$/i;
6202
6436
  function isSecret(name, value) {
6437
+ if (name === "headers" || /^HAI_REACH_PROVIDERS_\d+_HEADERS(?:_|$)/i.test(name))
6438
+ return true;
6203
6439
  if (/^HAI_REACH_PROVIDERS_\d+_GATEWAY_(?:HEADERS|BODY)(?:_|$)/i.test(name))
6204
6440
  return true;
6205
6441
  if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
6206
6442
  return true;
6207
6443
  try {
6208
6444
  const url = new URL(value);
6209
- if (url.username || url.password || [...url.searchParams.keys()].some((key) => /password|secret|token|key|authorization/i.test(key)))
6445
+ if (url.username || url.password || [
6446
+ ...url.searchParams.keys()
6447
+ ].some((key) => /password|secret|token|key|authorization/i.test(key))) {
6210
6448
  return true;
6449
+ }
6211
6450
  } catch {
6212
6451
  }
6213
6452
  try {
@@ -6249,7 +6488,13 @@ function generatedVariables(values) {
6249
6488
  const structuredNames = Object.keys(group).filter((name) => structures.has(name));
6250
6489
  if (!structuredNames.length)
6251
6490
  continue;
6252
- const resolved = core.config.resolve({ name: module, value: {}, environment: group, shape: shapes[module], missing: "empty" });
6491
+ const resolved = core.config.resolve({
6492
+ name: module,
6493
+ value: {},
6494
+ environment: group,
6495
+ shape: shapes[module],
6496
+ missing: "empty"
6497
+ });
6253
6498
  if (resolved.success) {
6254
6499
  const flattened = core.config.toEnv({ prefix: `HAI_${module.toUpperCase()}`, value: resolved.data, shape: shapes[module] });
6255
6500
  const represented = structuredNames.every((name) => Object.entries(flattened).some(([key, value]) => key === name || key.startsWith(`${name}_`) || name.startsWith(`${key}_`) && value.startsWith("[")));
@@ -6287,12 +6532,12 @@ function generatedVariables(values) {
6287
6532
  return generated;
6288
6533
  }
6289
6534
  function updateFile(path, values, privateFile2) {
6290
- let content = existsSync(path) ? readFileSync(path, "utf8") : "# Local private deployment configuration; do not commit.\n";
6535
+ let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
6291
6536
  for (const [name, value] of Object.entries(values)) {
6292
6537
  if (!/^[A-Z][A-Z0-9_]*$/.test(name))
6293
6538
  throw new Error("Invalid environment variable name");
6294
6539
  const line = core.config.stringifyEnv({ [name]: value }).trimEnd();
6295
- const pattern = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
6540
+ const pattern = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
6296
6541
  if (pattern.test(content))
6297
6542
  content = content.replace(pattern, () => line);
6298
6543
  else
@@ -6303,25 +6548,22 @@ ${line}
6303
6548
  for (const [name, value] of Object.entries(core.config.parseEnv(content)).sort(([left], [right]) => left.length - right.length)) {
6304
6549
  if (!name.startsWith("HAI_") || !/^\s*[[{]/.test(value))
6305
6550
  continue;
6306
- const parent = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "m").exec(content);
6307
- const child = new RegExp(`^(?:export\\s+)?${name}_[A-Z0-9_]+\\s*=`, "m").exec(content);
6551
+ const parent = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "m").exec(content);
6552
+ const child = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}_[A-Z0-9_]+[\\t ]*=`, "m").exec(content);
6308
6553
  if (parent && child && parent.index > child.index) {
6309
6554
  const withoutParent = content.slice(0, parent.index) + content.slice(parent.index + parent[0].length);
6310
6555
  content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
6311
6556
  }
6312
6557
  }
6313
- writeFileSync(path, content, { mode: privateFile2 ? 384 : 420 });
6314
- if (privateFile2)
6315
- chmodSync(path, 384);
6558
+ writeLocalFile(path, content, privateFile2);
6316
6559
  }
6317
6560
  function removeOverrides(path, names, privateFile2 = true) {
6318
6561
  if (!existsSync(path) || names.length === 0)
6319
6562
  return;
6320
- let content = readFileSync(path, "utf8");
6563
+ let content = readLocalFile(path).toString("utf8");
6321
6564
  for (const name of names)
6322
- content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
6323
- writeFileSync(path, content, { mode: privateFile2 ? 384 : 420 });
6324
- chmodSync(path, privateFile2 ? 384 : 420);
6565
+ content = content.replace(new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
6566
+ writeLocalFile(path, content, privateFile2);
6325
6567
  }
6326
6568
  function saveDeploymentEnvironment(environment, values, options = {}) {
6327
6569
  try {
@@ -6331,8 +6573,12 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
6331
6573
  return value;
6332
6574
  if (Array.isArray(value))
6333
6575
  return value.map((item, index) => splitArrayValue2(`${name}_${index}`, item) ?? null);
6334
- if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key)))
6335
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, splitArrayValue2(`${name}_${key.toUpperCase()}`, item)]).filter(([, item]) => item !== void 0));
6576
+ if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key))) {
6577
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
6578
+ key,
6579
+ splitArrayValue2(`${name}_${key.toUpperCase()}`, item)
6580
+ ]).filter(([, item]) => item !== void 0));
6581
+ }
6336
6582
  privateValues[name] = literal;
6337
6583
  return void 0;
6338
6584
  };
@@ -6386,10 +6632,10 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
6386
6632
  removeOverrides(privatePath, Object.keys(shared).filter((name) => !(name in privateValues)));
6387
6633
  if (Object.keys(privateValues).length) {
6388
6634
  const ignorePath = join(environment.envDir, ".gitignore");
6389
- const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
6635
+ const ignore = existsSync(ignorePath) ? readLocalFile(ignorePath).toString("utf8") : "";
6390
6636
  const missing = [".env.local", ".env.*.local", ".hai/deploy/"].filter((line) => !ignore.split(/\r?\n/).includes(line));
6391
6637
  if (missing.length)
6392
- writeFileSync(ignorePath, `${ignore.trimEnd()}
6638
+ writeLocalFile(ignorePath, `${ignore.trimEnd()}
6393
6639
  ${missing.join("\n")}
6394
6640
  `);
6395
6641
  }
@@ -6399,7 +6645,14 @@ ${missing.join("\n")}
6399
6645
  }
6400
6646
  }
6401
6647
  function normalizeProjectName(name) {
6402
- const normalized = name.trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
6648
+ const replaced = name.trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
6649
+ let start = 0;
6650
+ let end = replaced.length;
6651
+ while (start < end && replaced[start] === "-")
6652
+ start++;
6653
+ while (end > start && replaced[end - 1] === "-")
6654
+ end--;
6655
+ const normalized = replaced.slice(start, end);
6403
6656
  if (!normalized)
6404
6657
  throw new Error(deployM("deploy_projectNameRequired"));
6405
6658
  return normalized;
@@ -6452,7 +6705,9 @@ function normalizeCredentialEntries(entries) {
6452
6705
  }
6453
6706
  function assertAllowedCredentialKey(key) {
6454
6707
  if (!isAllowedCredentialKey(key)) {
6455
- throw new Error(deployM("deploy_credentialInvalidKey", { params: { key } }));
6708
+ throw new Error(deployM("deploy_credentialInvalidKey", {
6709
+ params: { key }
6710
+ }));
6456
6711
  }
6457
6712
  }
6458
6713
  function assertAllowedCredentialEntries(entries) {
@@ -6504,7 +6759,9 @@ function loadCredentials() {
6504
6759
  return err(
6505
6760
  HaiDeployError.CREDENTIAL_ERROR,
6506
6761
  deployM("deploy_credentialError", {
6507
- params: { error: error instanceof Error ? error.message : String(error) }
6762
+ params: {
6763
+ error: error instanceof Error ? error.message : String(error)
6764
+ }
6508
6765
  }),
6509
6766
  error
6510
6767
  );
@@ -6524,7 +6781,9 @@ function saveCredential(key, value) {
6524
6781
  return err(
6525
6782
  HaiDeployError.CREDENTIAL_ERROR,
6526
6783
  deployM("deploy_credentialError", {
6527
- params: { error: error instanceof Error ? error.message : String(error) }
6784
+ params: {
6785
+ error: error instanceof Error ? error.message : String(error)
6786
+ }
6528
6787
  }),
6529
6788
  error
6530
6789
  );
@@ -6546,7 +6805,9 @@ function saveCredentials(entries) {
6546
6805
  return err(
6547
6806
  HaiDeployError.CREDENTIAL_ERROR,
6548
6807
  deployM("deploy_credentialError", {
6549
- params: { error: error instanceof Error ? error.message : String(error) }
6808
+ params: {
6809
+ error: error instanceof Error ? error.message : String(error)
6810
+ }
6550
6811
  }),
6551
6812
  error
6552
6813
  );
@@ -6555,12 +6816,59 @@ function saveCredentials(entries) {
6555
6816
 
6556
6817
  // src/deploy-middleware-config.ts
6557
6818
  var connectionFields = {
6558
- db: ["url", "host", "port", "database", "user", "password", "ssl"],
6559
- cache: ["url", "host", "port", "password", "cluster", "sentinel", "tls"],
6560
- storage: ["endpoint", "bucket", "region", "accessKeyId", "secretAccessKey", "forcePathStyle", "createBucket"],
6561
- vecdb: ["url", "host", "port", "database", "user", "password", "apiKey", "path", "serverCommand", "startupTimeout"],
6562
- ldap: ["url", "bindDn", "bindPassword", "searchBase", "useTls"],
6563
- email: ["host", "port", "secure", "user", "pass"]
6819
+ db: [
6820
+ "url",
6821
+ "host",
6822
+ "port",
6823
+ "database",
6824
+ "user",
6825
+ "password",
6826
+ "ssl"
6827
+ ],
6828
+ cache: [
6829
+ "url",
6830
+ "host",
6831
+ "port",
6832
+ "password",
6833
+ "cluster",
6834
+ "sentinel",
6835
+ "tls"
6836
+ ],
6837
+ storage: [
6838
+ "endpoint",
6839
+ "bucket",
6840
+ "region",
6841
+ "accessKeyId",
6842
+ "secretAccessKey",
6843
+ "forcePathStyle",
6844
+ "createBucket"
6845
+ ],
6846
+ vecdb: [
6847
+ "url",
6848
+ "host",
6849
+ "port",
6850
+ "database",
6851
+ "user",
6852
+ "password",
6853
+ "apiKey",
6854
+ "path",
6855
+ "serverCommand",
6856
+ "startupTimeout"
6857
+ ],
6858
+ ldap: [
6859
+ "url",
6860
+ "bindDn",
6861
+ "bindPassword",
6862
+ "searchBase",
6863
+ "useTls"
6864
+ ],
6865
+ email: [
6866
+ "host",
6867
+ "port",
6868
+ "secure",
6869
+ "user",
6870
+ "pass"
6871
+ ]
6564
6872
  };
6565
6873
  function mergeManagedMiddlewareConfig(requirement, supplied) {
6566
6874
  if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
@@ -6568,7 +6876,10 @@ function mergeManagedMiddlewareConfig(requirement, supplied) {
6568
6876
  const configured = { ...requirement.config };
6569
6877
  for (const field2 of connectionFields[requirement.service] ?? [])
6570
6878
  delete configured[field2];
6571
- const merged = { ...configured, ...supplied };
6879
+ const merged = {
6880
+ ...configured,
6881
+ ...supplied
6882
+ };
6572
6883
  if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
6573
6884
  const url = new URL(supplied.url);
6574
6885
  if (configured.db !== 0 || url.pathname !== "" && url.pathname !== "/") {
@@ -6583,21 +6894,39 @@ var RecoverySchema = z.object({
6583
6894
  stage: z.string(),
6584
6895
  platformProjectId: z.string().optional(),
6585
6896
  resources: z.array(z.object({
6586
- serviceType: z.enum(["db", "cache", "storage", "email", "sms"]),
6897
+ serviceType: z.enum([
6898
+ "db",
6899
+ "cache",
6900
+ "storage",
6901
+ "email",
6902
+ "sms"
6903
+ ]),
6587
6904
  provisionerName: z.string(),
6588
6905
  resourceInfo: z.string(),
6589
6906
  resourceStatus: z.enum(["created", "reused", "unknown"])
6590
6907
  }))
6591
6908
  });
6592
6909
  var CommandStateSchema = z.object({
6593
- reason: z.enum(["timeout", "output", "input", "spawn", "exit", "configuration"]),
6910
+ reason: z.enum([
6911
+ "timeout",
6912
+ "output",
6913
+ "input",
6914
+ "spawn",
6915
+ "exit",
6916
+ "configuration"
6917
+ ]),
6594
6918
  localTermination: z.enum(["confirmed", "unknown", "not-started"]),
6595
6919
  remoteExecution: z.enum(["unknown", "finished"]).optional(),
6596
6920
  deploymentId: z.string().optional(),
6597
6921
  resultFile: z.string().optional(),
6598
6922
  remoteExitCode: z.number().int().optional(),
6599
6923
  remoteStage: z.string().optional(),
6600
- remoteRecovery: z.enum(["not-needed", "passed", "failed", "stopped"]).optional()
6924
+ remoteRecovery: z.enum([
6925
+ "not-needed",
6926
+ "passed",
6927
+ "failed",
6928
+ "stopped"
6929
+ ]).optional()
6601
6930
  });
6602
6931
  function getDeployCommandState(error) {
6603
6932
  const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
@@ -6613,7 +6942,9 @@ function recoveryFailure(error, recovery) {
6613
6942
  httpStatus: error.httpStatus,
6614
6943
  system: error.system,
6615
6944
  module: error.module,
6616
- message: deployM("deploy_recoveryFailure", { params: { stage: recovery.stage } }),
6945
+ message: deployM("deploy_recoveryFailure", {
6946
+ params: { stage: recovery.stage }
6947
+ }),
6617
6948
  suggestion: deployM("deploy_recoverySuggestion"),
6618
6949
  ext: { recovery }
6619
6950
  });
@@ -6648,12 +6979,15 @@ function runtimeDependencies(appDir) {
6648
6979
  visited.add(id);
6649
6980
  const metadata = importer === void 0 ? record(packages[id.split("(")[0]]) : {};
6650
6981
  const peers = record(metadata.peerDependencies);
6651
- for (const [name, reference] of Object.entries({ ...record(node.dependencies), ...record(node.optionalDependencies) })) {
6982
+ for (const [name, reference] of Object.entries({
6983
+ ...record(node.dependencies),
6984
+ ...record(node.optionalDependencies)
6985
+ })) {
6652
6986
  if (name in peers)
6653
6987
  continue;
6654
6988
  const version = typeof reference === "string" ? reference : record(reference).version;
6655
6989
  if (typeof version !== "string")
6656
- throw new Error("Invalid pnpm dependency reference");
6990
+ throw new TypeError("Invalid pnpm dependency reference");
6657
6991
  if (version.startsWith("link:")) {
6658
6992
  if (importer === void 0)
6659
6993
  throw new Error("Invalid workspace dependency");
@@ -6674,7 +7008,14 @@ function runtimeDependencies(appDir) {
6674
7008
  }
6675
7009
  };
6676
7010
  visit(`workspace:${importerKey}`, record(importers[importerKey]), importerKey);
6677
- return { runtime: names, build: /* @__PURE__ */ new Set([...names, ...Object.keys(record(record(importers[importerKey]).devDependencies))]), root };
7011
+ return {
7012
+ runtime: names,
7013
+ build: /* @__PURE__ */ new Set([
7014
+ ...names,
7015
+ ...Object.keys(record(record(importers[importerKey]).devDependencies))
7016
+ ]),
7017
+ root
7018
+ };
6678
7019
  }
6679
7020
  async function scanApp(inputDir, options) {
6680
7021
  try {
@@ -6696,7 +7037,14 @@ async function scanApp(inputDir, options) {
6696
7037
  const deps = graph.runtime;
6697
7038
  const configs = {};
6698
7039
  const enabledConfigs = /* @__PURE__ */ new Set();
6699
- for (const [name, owner] of Object.entries({ db: "reldb", cache: "cache", storage: "storage", vecdb: "vecdb", iam: "iam", reach: "reach" })) {
7040
+ for (const [name, owner] of Object.entries({
7041
+ db: "reldb",
7042
+ cache: "cache",
7043
+ storage: "storage",
7044
+ vecdb: "vecdb",
7045
+ iam: "iam",
7046
+ reach: "reach"
7047
+ })) {
6700
7048
  configs[name] = {};
6701
7049
  if (!deps.has(`@h-ai/${owner}`))
6702
7050
  continue;
@@ -6710,7 +7058,15 @@ async function scanApp(inputDir, options) {
6710
7058
  }
6711
7059
  const middlewares = [];
6712
7060
  const add = (service, backend, configName = service, configPath = [], config = configs[configName] ?? {}, source = `_${configName}.yml`) => {
6713
- middlewares.push({ service, backend, source, configName, configPath, envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"), config });
7061
+ middlewares.push({
7062
+ service,
7063
+ backend,
7064
+ source,
7065
+ configName,
7066
+ configPath,
7067
+ envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"),
7068
+ config
7069
+ });
6714
7070
  };
6715
7071
  const iam = enabledConfigs.has("iam");
6716
7072
  if (enabledConfigs.has("db"))
@@ -6731,8 +7087,12 @@ async function scanApp(inputDir, options) {
6731
7087
  const provider = record(item);
6732
7088
  const type = String(provider.type ?? "");
6733
7089
  const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
6734
- if (service)
6735
- add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
7090
+ if (service) {
7091
+ add(service, type, "reach", [
7092
+ "providers",
7093
+ String(index)
7094
+ ], provider, "@h-ai/reach");
7095
+ }
6736
7096
  }
6737
7097
  }
6738
7098
  if (enabledConfigs.has("reach") && providers === void 0)
@@ -6747,19 +7107,33 @@ async function scanApp(inputDir, options) {
6747
7107
  isSvelteKit,
6748
7108
  adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
6749
7109
  nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
6750
- requiredServices: [...new Set(middlewares.map((item) => item.service))],
7110
+ requiredServices: [
7111
+ ...new Set(middlewares.map((item) => item.service))
7112
+ ],
6751
7113
  serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
6752
7114
  };
6753
- logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
7115
+ logger2.info("Application scanned", {
7116
+ appName: scan.appName,
7117
+ haiModules: scan.haiModules,
7118
+ requiredServices: scan.requiredServices,
7119
+ serviceBackends: scan.serviceBackends
7120
+ });
6754
7121
  return ok(scan);
6755
7122
  } catch {
6756
- return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
7123
+ return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", {
7124
+ params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" }
7125
+ }));
6757
7126
  }
6758
7127
  }
6759
7128
 
6760
7129
  // src/container/deploy-compose-identity.ts
6761
7130
  function createComposeContainerLookupScript() {
6762
- const labelFormat = ["com.docker.compose.project", "com.docker.compose.service", "io.podman.compose.project", "io.podman.compose.service"].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
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("|");
6763
7137
  return String.raw`compose_container_ids() (
6764
7138
  if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
6765
7139
  lookup_runtime=$1
@@ -7147,11 +7521,28 @@ deployment_forget_metadata() {
7147
7521
 
7148
7522
  // src/container/deploy-managed-secrets.ts
7149
7523
  var groups = [
7150
- { name: "DB_PASSWORD", keys: ["POSTGRES_PASSWORD", "MYSQL_PASSWORD", "MYSQL_ROOT_PASSWORD"], services: ["db", "vecdb"] },
7151
- { name: "STORAGE_PASSWORD", keys: ["RUSTFS_SECRET_KEY", "MINIO_ROOT_PASSWORD"], services: ["storage"] },
7152
- { name: "LDAP_PASSWORD", keys: ["LDAP_ADMIN_PASSWORD"], services: ["ldap"] }
7524
+ {
7525
+ name: "DB_PASSWORD",
7526
+ keys: ["POSTGRES_PASSWORD", "MYSQL_PASSWORD", "MYSQL_ROOT_PASSWORD"],
7527
+ services: ["db", "vecdb"]
7528
+ },
7529
+ {
7530
+ name: "STORAGE_PASSWORD",
7531
+ keys: ["RUSTFS_SECRET_KEY", "MINIO_ROOT_PASSWORD"],
7532
+ services: ["storage"]
7533
+ },
7534
+ {
7535
+ name: "LDAP_PASSWORD",
7536
+ keys: ["LDAP_ADMIN_PASSWORD"],
7537
+ services: ["ldap"]
7538
+ }
7153
7539
  ];
7154
- var appPrefixes = { db: "HAI_DB", vecdb: "HAI_VECDB", storage: "HAI_STORAGE", ldap: "HAI_IAM_LDAP" };
7540
+ var appPrefixes = {
7541
+ db: "HAI_DB",
7542
+ vecdb: "HAI_VECDB",
7543
+ storage: "HAI_STORAGE",
7544
+ ldap: "HAI_IAM_LDAP"
7545
+ };
7155
7546
  function resolveComposeNamedVolumes(projectName, mounts, declarations = {}) {
7156
7547
  if (!Array.isArray(mounts))
7157
7548
  return [];
@@ -7177,7 +7568,9 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7177
7568
  const bindings = /* @__PURE__ */ new Set();
7178
7569
  const volumes = /* @__PURE__ */ new Set();
7179
7570
  for (const group of groups) {
7180
- const values = [...new Set(group.keys.flatMap((key) => native[key] === void 0 ? [] : [native[key]]))];
7571
+ const values = [
7572
+ ...new Set(group.keys.flatMap((key) => native[key] === void 0 ? [] : [native[key]]))
7573
+ ];
7181
7574
  if (!values.length)
7182
7575
  continue;
7183
7576
  if (values.length !== 1 || !/^[\w-]{32}$/.test(values[0]))
@@ -7379,7 +7772,7 @@ function createBackendStateAssets(projectName, compose, selected) {
7379
7772
  const service = compose.services[serviceName];
7380
7773
  const image = service?.image;
7381
7774
  if (typeof image !== "string")
7382
- throw new Error(deployM("deploy_artifactInvalid"));
7775
+ throw new TypeError(deployM("deploy_artifactInvalid"));
7383
7776
  const imageFamily = image.slice(image.lastIndexOf("/") + 1).split(":")[0];
7384
7777
  const family = imageFamily === "postgres" ? "postgresql" : imageFamily;
7385
7778
  const namedVolumes = resolveComposeNamedVolumes(projectName, service.volumes, compose.volumes);
@@ -7732,7 +8125,13 @@ public static class HaiCommandJob {
7732
8125
  }
7733
8126
  `;
7734
8127
  function powershell(script) {
7735
- return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
8128
+ return [
8129
+ "-NoLogo",
8130
+ "-NoProfile",
8131
+ "-NonInteractive",
8132
+ "-EncodedCommand",
8133
+ Buffer.from(script, "utf16le").toString("base64")
8134
+ ];
7736
8135
  }
7737
8136
  function windowsCommand(command, args) {
7738
8137
  const name = `Local\\hai-deploy-${randomUUID()}`;
@@ -7746,7 +8145,13 @@ ${nativeJob}
7746
8145
  [Environment]::SetEnvironmentVariable('${key}', $null)
7747
8146
  exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
7748
8147
  } catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
7749
- return { name, args: powershell(script), env: { [key]: JSON.stringify({ name, command, args }) } };
8148
+ return {
8149
+ name,
8150
+ args: powershell(script),
8151
+ env: {
8152
+ [key]: JSON.stringify({ name, command, args })
8153
+ }
8154
+ };
7750
8155
  }
7751
8156
  function windowsJobProbe(name) {
7752
8157
  return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
@@ -7816,7 +8221,10 @@ function spawnCommand(command, args, options) {
7816
8221
  const job = process.platform === "win32" ? windowsCommand(command, args) : void 0;
7817
8222
  const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
7818
8223
  ...options,
7819
- env: job ? { ...options.env, ...job.env } : options.env,
8224
+ env: job ? {
8225
+ ...options.env,
8226
+ ...job.env
8227
+ } : options.env,
7820
8228
  windowsHide: true,
7821
8229
  shell: false,
7822
8230
  detached: !job
@@ -7843,7 +8251,12 @@ function spawnCommand(command, args, options) {
7843
8251
  if (child.pid) {
7844
8252
  if (job) {
7845
8253
  if (!exited) {
7846
- windowsKillConfirmed = await helper("taskkill", ["/pid", String(child.pid), "/t", "/f"], Math.max(1, deadline - performance.now()));
8254
+ windowsKillConfirmed = await helper("taskkill", [
8255
+ "/pid",
8256
+ String(child.pid),
8257
+ "/t",
8258
+ "/f"
8259
+ ], Math.max(1, deadline - performance.now()));
7847
8260
  child.kill("SIGKILL");
7848
8261
  }
7849
8262
  } else {
@@ -7885,7 +8298,9 @@ function spawnCommand(command, args, options) {
7885
8298
 
7886
8299
  // src/container/deploy-secret-masker.ts
7887
8300
  function createSecretMasker(secrets = []) {
7888
- const values = [...new Set(secrets.filter(Boolean))].sort((a, b) => b.length - a.length);
8301
+ const values = [
8302
+ ...new Set(secrets.filter(Boolean))
8303
+ ].sort((a, b) => b.length - a.length);
7889
8304
  if (values.reduce((total, value) => total + value.length, 0) > 1048576)
7890
8305
  throw new RangeError("Secret redaction input is too large");
7891
8306
  const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
@@ -7957,11 +8372,19 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
7957
8372
  throw new RangeError("Invalid command termination timeout");
7958
8373
  if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
7959
8374
  throw new RangeError("Invalid command timeout");
7960
- const secrets = [...options.secrets ?? []];
8375
+ const secrets = [
8376
+ ...options.secrets ?? []
8377
+ ];
7961
8378
  const safeCommand = maskSecrets(command, secrets);
7962
8379
  const outputMasker = createSecretMasker(secrets);
7963
8380
  const errorMasker = createSecretMasker(secrets);
7964
- const control = spawnCommand(command, args, { cwd: options.cwd, env: options.env ? { ...process.env, ...options.env } : process.env });
8381
+ const control = spawnCommand(command, args, {
8382
+ cwd: options.cwd,
8383
+ env: options.env ? {
8384
+ ...process.env,
8385
+ ...options.env
8386
+ } : process.env
8387
+ });
7965
8388
  const child = control.child;
7966
8389
  child.stdout.setEncoding("utf8");
7967
8390
  child.stderr.setEncoding("utf8");
@@ -8000,7 +8423,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8000
8423
  try {
8001
8424
  await options.onOutput?.(text);
8002
8425
  } catch {
8003
- await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
8426
+ await interrupt("output", deployM("deploy_commandFailed", {
8427
+ params: {
8428
+ command: safeCommand,
8429
+ error: deployM("deploy_commandOutputFailed")
8430
+ }
8431
+ }));
8004
8432
  }
8005
8433
  };
8006
8434
  const captureError = (text) => {
@@ -8010,7 +8438,9 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8010
8438
  if (options.timeoutMs && options.timeoutMs > 0) {
8011
8439
  timer = setTimeout(() => {
8012
8440
  logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
8013
- void interrupt("timeout", deployM("deploy_commandTimeout", { params: { command: safeCommand } }));
8441
+ void interrupt("timeout", deployM("deploy_commandTimeout", {
8442
+ params: { command: safeCommand }
8443
+ }));
8014
8444
  }, options.timeoutMs);
8015
8445
  }
8016
8446
  const outputTask = (async () => {
@@ -8025,7 +8455,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8025
8455
  if (!settled && !interrupted)
8026
8456
  await publish(outputMasker.end());
8027
8457
  } catch {
8028
- await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
8458
+ await interrupt("output", deployM("deploy_commandFailed", {
8459
+ params: {
8460
+ command: safeCommand,
8461
+ error: deployM("deploy_commandOutputFailed")
8462
+ }
8463
+ }));
8029
8464
  }
8030
8465
  })();
8031
8466
  const errorTask = (async () => {
@@ -8040,16 +8475,28 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8040
8475
  if (!settled && !interrupted)
8041
8476
  captureError(errorMasker.end());
8042
8477
  } catch {
8043
- await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
8478
+ await interrupt("output", deployM("deploy_commandFailed", {
8479
+ params: {
8480
+ command: safeCommand,
8481
+ error: deployM("deploy_commandOutputFailed")
8482
+ }
8483
+ }));
8044
8484
  }
8045
8485
  })();
8046
8486
  child.stdin.on("error", () => {
8047
- void interrupt("input", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandInputFailed") } }));
8487
+ void interrupt("input", deployM("deploy_commandFailed", {
8488
+ params: {
8489
+ command: safeCommand,
8490
+ error: deployM("deploy_commandInputFailed")
8491
+ }
8492
+ }));
8048
8493
  });
8049
8494
  child.on("error", (error) => {
8050
8495
  const safeError = maskSecrets(error.message, secrets);
8051
8496
  logger3.error("Command failed to start", { command: safeCommand, error: safeError });
8052
- void interrupt("spawn", deployM("deploy_commandFailed", { params: { command: safeCommand, error: safeError } }));
8497
+ void interrupt("spawn", deployM("deploy_commandFailed", {
8498
+ params: { command: safeCommand, error: safeError }
8499
+ }));
8053
8500
  });
8054
8501
  child.on("close", async (code) => {
8055
8502
  await Promise.all([outputTask, errorTask]);
@@ -8058,24 +8505,52 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8058
8505
  if (timer)
8059
8506
  clearTimeout(timer);
8060
8507
  if (!await control.treeExited()) {
8061
- await interrupt("exit", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandTreeActive") } }));
8508
+ await interrupt("exit", deployM("deploy_commandFailed", {
8509
+ params: {
8510
+ command: safeCommand,
8511
+ error: deployM("deploy_commandTreeActive")
8512
+ }
8513
+ }));
8062
8514
  return;
8063
8515
  }
8064
8516
  const exitCode = code ?? -1;
8065
8517
  if (exitCode === 0) {
8066
- finish(ok({ code: exitCode, stdout, stderr, stdoutTruncated, stderrTruncated }));
8518
+ finish(ok({
8519
+ code: exitCode,
8520
+ stdout,
8521
+ stderr,
8522
+ stdoutTruncated,
8523
+ stderrTruncated
8524
+ }));
8067
8525
  return;
8068
8526
  }
8069
- logger3.error("Command exited with non-zero code", { command: safeCommand, code: exitCode, stderr: maskSecrets(stderr, secrets).slice(-500), stdout: maskSecrets(stdout, secrets).slice(-3e3), stdoutTruncated, stderrTruncated });
8070
- finish(failure(deployM("deploy_commandFailed", { params: { command: safeCommand, error: maskSecrets(`${stdout.slice(-2500)}
8071
- ${stderr || `exit ${exitCode}`}`, secrets).slice(-3e3) } }), { reason: "exit", localTermination: "confirmed" }));
8527
+ logger3.error("Command exited with non-zero code", {
8528
+ command: safeCommand,
8529
+ code: exitCode,
8530
+ stderr: maskSecrets(stderr, secrets).slice(-500),
8531
+ stdout: maskSecrets(stdout, secrets).slice(-3e3),
8532
+ stdoutTruncated,
8533
+ stderrTruncated
8534
+ });
8535
+ finish(failure(deployM("deploy_commandFailed", {
8536
+ params: {
8537
+ command: safeCommand,
8538
+ error: maskSecrets(`${stdout.slice(-2500)}
8539
+ ${stderr || `exit ${exitCode}`}`, secrets).slice(-3e3)
8540
+ }
8541
+ }), { reason: "exit", localTermination: "confirmed" }));
8072
8542
  });
8073
8543
  if (options.input !== void 0)
8074
8544
  child.stdin.write(options.input);
8075
8545
  child.stdin.end();
8076
8546
  });
8077
8547
  } catch {
8078
- return failure(deployM("deploy_commandFailed", { params: { command: "<command>", error: deployM("deploy_commandExecutionFailed") } }), { reason: "configuration", localTermination: "not-started" });
8548
+ return failure(deployM("deploy_commandFailed", {
8549
+ params: {
8550
+ command: "<command>",
8551
+ error: deployM("deploy_commandExecutionFailed")
8552
+ }
8553
+ }), { reason: "configuration", localTermination: "not-started" });
8079
8554
  }
8080
8555
  }
8081
8556
 
@@ -8318,7 +8793,13 @@ printf '%s\n' "$HAI_DEPLOY_ASKPASS_PASSWORD"
8318
8793
  `.replaceAll("\\${", "${");
8319
8794
  var compileAskpassScript = "$ErrorActionPreference='Stop'; Add-Type -Language CSharp -TypeDefinition ([System.IO.File]::ReadAllText($env:HAI_DEPLOY_ASKPASS_SOURCE, [System.Text.Encoding]::UTF8)) -OutputType ConsoleApplication -OutputAssembly $env:HAI_DEPLOY_ASKPASS_OUTPUT";
8320
8795
  function helperFailure(key, reason = "configuration") {
8321
- return err({ ...HaiDeployError.REMOTE_COMMAND_FAILED, message: deployM(key), ext: { commandExecution: { reason, localTermination: "not-started" } } });
8796
+ return err({
8797
+ ...HaiDeployError.REMOTE_COMMAND_FAILED,
8798
+ message: deployM(key),
8799
+ ext: {
8800
+ commandExecution: { reason, localTermination: "not-started" }
8801
+ }
8802
+ });
8322
8803
  }
8323
8804
  async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8324
8805
  let directory;
@@ -8349,7 +8830,12 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8349
8830
  const source = join(directory, "askpass.cs");
8350
8831
  writeFileSync(source, windowsAskpassSource, { mode: 384 });
8351
8832
  const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
8352
- const compiled = await runCommand(compiler, ["-NoProfile", "-NonInteractive", "-Command", compileAskpassScript], {
8833
+ const compiled = await runCommand(compiler, [
8834
+ "-NoProfile",
8835
+ "-NonInteractive",
8836
+ "-Command",
8837
+ compileAskpassScript
8838
+ ], {
8353
8839
  env: { HAI_DEPLOY_ASKPASS_SOURCE: source, HAI_DEPLOY_ASKPASS_OUTPUT: helper2, HAI_DEPLOY_ASKPASS_PASSWORD: "" },
8354
8840
  timeoutMs: Math.max(1, Math.min(3e4, Math.ceil(deadline - performance.now()))),
8355
8841
  terminationTimeoutMs,
@@ -8370,7 +8856,16 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8370
8856
  const removed = cleanup();
8371
8857
  return removed.success ? helperFailure("deploy_sshAskpassFailed", "timeout") : removed;
8372
8858
  }
8373
- return ok({ env: { SSH_ASKPASS: helper2, SSH_ASKPASS_REQUIRE: "force", SSH_ASKPASS_PROMPT: "", DISPLAY: "hai-deploy:0", HAI_DEPLOY_ASKPASS_PASSWORD: password }, cleanup });
8859
+ return ok({
8860
+ env: {
8861
+ SSH_ASKPASS: helper2,
8862
+ SSH_ASKPASS_REQUIRE: "force",
8863
+ SSH_ASKPASS_PROMPT: "",
8864
+ DISPLAY: "hai-deploy:0",
8865
+ HAI_DEPLOY_ASKPASS_PASSWORD: password
8866
+ },
8867
+ cleanup
8868
+ });
8374
8869
  } catch {
8375
8870
  const removed = cleanup();
8376
8871
  return removed.success ? helperFailure("deploy_sshAskpassFailed") : removed;
@@ -9331,7 +9826,10 @@ function usesSshPassword(config) {
9331
9826
  }
9332
9827
  function buildConnectOptions(config) {
9333
9828
  const password = usesSshPassword(config);
9334
- const args = ["-o", password ? "BatchMode=no" : "BatchMode=yes"];
9829
+ const args = [
9830
+ "-o",
9831
+ password ? "BatchMode=no" : "BatchMode=yes"
9832
+ ];
9335
9833
  if (password)
9336
9834
  args.push("-o", "PreferredAuthentications=password", "-o", "PasswordAuthentication=yes", "-o", "PubkeyAuthentication=no", "-o", "NumberOfPasswordPrompts=1");
9337
9835
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
@@ -9368,8 +9866,15 @@ function createSshExecutor(config) {
9368
9866
  const runAuthenticated = async (command, args, options) => {
9369
9867
  if (!usesSshPassword(config))
9370
9868
  return runCommand(command, args, options);
9371
- if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
9372
- return err({ ...HaiDeployError.REMOTE_COMMAND_FAILED, message: deployM("deploy_sshPasswordInvalid"), ext: { commandExecution: { reason: "configuration", localTermination: "not-started" } } });
9869
+ if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647)) {
9870
+ return err({
9871
+ ...HaiDeployError.REMOTE_COMMAND_FAILED,
9872
+ message: deployM("deploy_sshPasswordInvalid"),
9873
+ ext: {
9874
+ commandExecution: { reason: "configuration", localTermination: "not-started" }
9875
+ }
9876
+ });
9877
+ }
9373
9878
  const deadline = options.timeoutMs === void 0 ? Number.POSITIVE_INFINITY : performance.now() + options.timeoutMs;
9374
9879
  const prepared = await prepareSshAskpass(config.password, deadline, options.terminationTimeoutMs);
9375
9880
  if (!prepared.success)
@@ -9378,20 +9883,36 @@ function createSshExecutor(config) {
9378
9883
  let removed = ok(void 0);
9379
9884
  try {
9380
9885
  const remaining = deadline - performance.now();
9381
- result = remaining <= 0 ? err({ ...HaiDeployError.REMOTE_COMMAND_FAILED, message: deployM("deploy_sshAskpassFailed"), ext: { commandExecution: { reason: "timeout", localTermination: "not-started" } } }) : await runCommand(command, args, {
9886
+ result = remaining <= 0 ? err({
9887
+ ...HaiDeployError.REMOTE_COMMAND_FAILED,
9888
+ message: deployM("deploy_sshAskpassFailed"),
9889
+ ext: {
9890
+ commandExecution: { reason: "timeout", localTermination: "not-started" }
9891
+ }
9892
+ }) : await runCommand(command, args, {
9382
9893
  ...options,
9383
9894
  env: prepared.data.env,
9384
9895
  timeoutMs: Number.isFinite(deadline) ? Math.ceil(remaining) : void 0,
9385
- secrets: [...options.secrets ?? [], config.password]
9896
+ secrets: [
9897
+ ...options.secrets ?? [],
9898
+ config.password
9899
+ ]
9386
9900
  });
9387
9901
  } catch {
9388
- result = err({ ...HaiDeployError.REMOTE_COMMAND_FAILED, message: deployM("deploy_sshAskpassFailed"), ext: { commandExecution: { reason: "configuration", localTermination: "unknown" } } });
9902
+ result = err({
9903
+ ...HaiDeployError.REMOTE_COMMAND_FAILED,
9904
+ message: deployM("deploy_sshAskpassFailed"),
9905
+ ext: {
9906
+ commandExecution: { reason: "configuration", localTermination: "unknown" }
9907
+ }
9908
+ });
9389
9909
  } finally {
9390
9910
  removed = prepared.data.cleanup();
9391
9911
  }
9392
- if (!removed.success)
9912
+ if (!removed.success) {
9393
9913
  return result.success ? removed : err({ ...result.error, message: `${result.error.message}
9394
9914
  ${removed.error.message}` });
9915
+ }
9395
9916
  return result;
9396
9917
  };
9397
9918
  const exec = async (remoteCommand, options) => {
@@ -9405,13 +9926,31 @@ ${removed.error.message}` });
9405
9926
  if (result.success)
9406
9927
  return result;
9407
9928
  const state = getDeployCommandState(result.error);
9408
- return err({ ...result.error, suggestion: deployM("deploy_remoteUnknown"), ext: { ...result.error.ext, commandExecution: { ...state, reason: state?.reason ?? "exit", localTermination: state?.localTermination ?? "unknown", remoteExecution: "unknown" } } });
9929
+ return err({
9930
+ ...result.error,
9931
+ suggestion: deployM("deploy_remoteUnknown"),
9932
+ ext: {
9933
+ ...result.error.ext,
9934
+ commandExecution: {
9935
+ ...state,
9936
+ reason: state?.reason ?? "exit",
9937
+ localTermination: state?.localTermination ?? "unknown",
9938
+ remoteExecution: "unknown"
9939
+ }
9940
+ }
9941
+ });
9409
9942
  };
9410
9943
  const upload = async (localPath, remotePath) => {
9411
9944
  const args = buildScpUploadArgs(config, localPath, remotePath);
9412
9945
  const result = await runAuthenticated("scp", args, { timeoutMs: 6e5 });
9413
9946
  if (!result.success) {
9414
- return err({ ...HaiDeployError.IMAGE_TRANSFER_FAILED, message: deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }), ext: result.error.ext });
9947
+ return err({
9948
+ ...HaiDeployError.IMAGE_TRANSFER_FAILED,
9949
+ message: deployM("deploy_imageTransferFailed", {
9950
+ params: { error: result.error.message }
9951
+ }),
9952
+ ext: result.error.ext
9953
+ });
9415
9954
  }
9416
9955
  return ok(void 0);
9417
9956
  };
@@ -9419,7 +9958,13 @@ ${removed.error.message}` });
9419
9958
  logger4.debug("SSH preflight", { host: config.host, port: config.port });
9420
9959
  const result = await exec("true", { timeoutMs: 2e4 });
9421
9960
  if (!result.success) {
9422
- return err({ ...HaiDeployError.REMOTE_CONNECT_FAILED, message: deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }), ext: result.error.ext });
9961
+ return err({
9962
+ ...HaiDeployError.REMOTE_CONNECT_FAILED,
9963
+ message: deployM("deploy_remoteConnectFailed", {
9964
+ params: { host: config.host, error: result.error.message }
9965
+ }),
9966
+ ext: result.error.ext
9967
+ });
9423
9968
  }
9424
9969
  return ok(void 0);
9425
9970
  };
@@ -9452,8 +9997,11 @@ async function createDeploymentBundle(options) {
9452
9997
  const nodeRuntime = artifact.primaryService === "serv" || !!compose.services[artifact.primaryService]?.environment?.NODE_OPTIONS;
9453
9998
  for (const name of selected) {
9454
9999
  const service = compose.services[name];
9455
- if (!service)
9456
- throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
10000
+ if (!service) {
10001
+ throw new Error(deployM("deploy_selectionInvalid", {
10002
+ params: { service: name }
10003
+ }));
10004
+ }
9457
10005
  if (typeof service.image === "string")
9458
10006
  references.add(service.image);
9459
10007
  if (service.depends_on) {
@@ -9467,12 +10015,18 @@ async function createDeploymentBundle(options) {
9467
10015
  service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace(/^\.\/config(?=:)/, "../../../config").replace(/\.\/(web\.conf|request-access\.yml|tls|proxy-start\.sh|renew-https\.sh|runtime-env\.mjs):/g, `../../${options.deploymentId}/.hai/$1:`) : mount);
9468
10016
  }
9469
10017
  }
9470
- put(".hai/compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
10018
+ put(".hai/compose.json", JSON.stringify({
10019
+ ...compose,
10020
+ services: Object.fromEntries(selected.map((name) => [name, compose.services[name]]))
10021
+ }));
9471
10022
  let runtime;
9472
10023
  const imageList = [];
9473
10024
  for (const reference of references) {
9474
- if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
9475
- throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
10025
+ if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference)) {
10026
+ throw new Error(deployM("deploy_imageVersionRequired", {
10027
+ params: { image: reference }
10028
+ }));
10029
+ }
9476
10030
  const built = artifact.images.find((image) => image.imageRef === reference);
9477
10031
  const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
9478
10032
  const destination = join(staging, "images", file);
@@ -9493,10 +10047,20 @@ async function createDeploymentBundle(options) {
9493
10047
  if (!runtime)
9494
10048
  throw new Error(deployM("deploy_bundleRuntimeRequired"));
9495
10049
  }
9496
- let inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
10050
+ let inspected = await runCommand(runtime, [
10051
+ "image",
10052
+ "inspect",
10053
+ "--format",
10054
+ "{{.Id}}",
10055
+ reference
10056
+ ]);
9497
10057
  if (!inspected.success) {
9498
10058
  const download = resolveImageReference(reference, options.imageSource);
9499
- const pulled = await runCommand(runtime, ["pull", download], { timeoutMs: 6e5, env: proxyEnvironment, secrets: Object.values(proxyEnvironment).filter(Boolean) });
10059
+ const pulled = await runCommand(runtime, ["pull", download], {
10060
+ timeoutMs: 6e5,
10061
+ env: proxyEnvironment,
10062
+ secrets: Object.values(proxyEnvironment).filter(Boolean)
10063
+ });
9500
10064
  if (!pulled.success)
9501
10065
  throw new Error(pulled.error.message);
9502
10066
  if (download !== reference) {
@@ -9504,7 +10068,13 @@ async function createDeploymentBundle(options) {
9504
10068
  if (!tagged.success)
9505
10069
  throw new Error(tagged.error.message);
9506
10070
  }
9507
- inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
10071
+ inspected = await runCommand(runtime, [
10072
+ "image",
10073
+ "inspect",
10074
+ "--format",
10075
+ "{{.Id}}",
10076
+ reference
10077
+ ]);
9508
10078
  }
9509
10079
  const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
9510
10080
  const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
@@ -9516,7 +10086,13 @@ async function createDeploymentBundle(options) {
9516
10086
  checksum = copied;
9517
10087
  }
9518
10088
  if (!checksum) {
9519
- const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", destination, reference], { timeoutMs: 6e5 });
10089
+ const saved = await runCommand(runtime, [
10090
+ "save",
10091
+ ...runtime === "podman" ? ["--format", "docker-archive"] : [],
10092
+ "-o",
10093
+ destination,
10094
+ reference
10095
+ ], { timeoutMs: 6e5 });
9520
10096
  if (!saved.success)
9521
10097
  throw new Error(saved.error.message);
9522
10098
  checksum = await artifactChecksum(destination);
@@ -9558,7 +10134,12 @@ async function createDeploymentBundle(options) {
9558
10134
  if (file !== "https")
9559
10135
  put(`.hai/${file}`, content);
9560
10136
  }
9561
- const httpsFingerprint = config.expose.https ? createHash("sha256").update(JSON.stringify({ ...config.expose.https, certificateFile: void 0, privateKeyFile: void 0, certificate: options.httpsAssets?.["tls/fullchain.pem"] })).digest("hex") : "";
10137
+ const httpsFingerprint = config.expose.https ? createHash("sha256").update(JSON.stringify({
10138
+ ...config.expose.https,
10139
+ certificateFile: void 0,
10140
+ privateKeyFile: void 0,
10141
+ certificate: options.httpsAssets?.["tls/fullchain.pem"]
10142
+ })).digest("hex") : "";
9562
10143
  const backend = createBackendStateAssets(projectName, compose, selected);
9563
10144
  const metadata = {
9564
10145
  "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}` : ""}
@@ -9580,31 +10161,103 @@ async function createDeploymentBundle(options) {
9580
10161
  // 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
9581
10162
  "managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
9582
10163
  "managed-volumes": managed["managed-volumes"],
9583
- "managed-app-keys": updatesApp ? `${[...new Set(options.managedAppKeys)].join("\n")}
10164
+ "managed-app-keys": updatesApp ? `${[
10165
+ ...new Set(options.managedAppKeys)
10166
+ ].join("\n")}
9584
10167
  ` : "\n",
9585
- "unmanaged-app-keys": updatesApp ? `${[...new Set(options.unmanagedAppKeys)].join("\n")}
10168
+ "unmanaged-app-keys": updatesApp ? `${[
10169
+ ...new Set(options.unmanagedAppKeys)
10170
+ ].join("\n")}
9586
10171
  ` : "\n",
9587
10172
  "state-backends": backend["state-backends"]
9588
10173
  };
9589
- put("manifest.json", `${JSON.stringify({ ...artifact, bundleFormat: 2, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial, metadata }, null, 2)}
10174
+ put("manifest.json", `${JSON.stringify({
10175
+ ...artifact,
10176
+ bundleFormat: 2,
10177
+ images: artifact.images.filter((image) => references.has(image.imageRef)),
10178
+ deploymentId: options.deploymentId,
10179
+ selectedServices: selected,
10180
+ partial: options.partial,
10181
+ metadata
10182
+ }, null, 2)}
9590
10183
  `);
9591
10184
  const serverDirectory = `${config.remote.baseDir}/${projectName}`;
9592
- put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: serverDirectory, runtime: config.remote.runtime, partial: options.partial, selected, primaryService: artifact.primaryService, hostPort: config.expose.hostPort, healthPaths: options.healthPaths }));
9593
- const instructionParams = { file: basename(output), images: [...references].join("\n") };
10185
+ put("apply.sh", createApplyScript({
10186
+ deploymentId: options.deploymentId,
10187
+ projectName,
10188
+ environment,
10189
+ directory: serverDirectory,
10190
+ runtime: config.remote.runtime,
10191
+ partial: options.partial,
10192
+ selected,
10193
+ primaryService: artifact.primaryService,
10194
+ hostPort: config.expose.hostPort,
10195
+ healthPaths: options.healthPaths
10196
+ }));
10197
+ const instructionParams = {
10198
+ file: basename(output),
10199
+ images: [...references].join("\n")
10200
+ };
9594
10201
  const mode = deployM(options.partial ? "deploy_bundlePartial" : "deploy_bundleFull");
9595
- const selection = deployM("deploy_bundleSelection", { params: { mode, services: selected.join(", ") } });
10202
+ const selection = deployM("deploy_bundleSelection", {
10203
+ params: {
10204
+ mode,
10205
+ services: selected.join(", ")
10206
+ }
10207
+ });
9596
10208
  const instructions = `${selection}
9597
10209
  ${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
9598
- const runtimeGuide = deployM("deploy_bundleRuntime", { params: { directory: serverDirectory, deploymentId: options.deploymentId } });
9599
- const variableGuide = deployM("deploy_bundleVariables", { params: { environment, keys: Object.keys(core.config.parseEnv(applicationEnvironment)).sort().join(", ") || "\u2014" } });
10210
+ const runtimeGuide = deployM("deploy_bundleRuntime", {
10211
+ params: { directory: serverDirectory, deploymentId: options.deploymentId }
10212
+ });
10213
+ const variableGuide = deployM("deploy_bundleVariables", {
10214
+ params: { environment, keys: Object.keys(core.config.parseEnv(applicationEnvironment)).sort().join(", ") || "\u2014" }
10215
+ });
9600
10216
  const https = config.expose.https;
9601
10217
  const certificates = https?.type === "files" ? `${serverDirectory}/releases/${options.deploymentId}/.hai/tls` : `${serverDirectory}/https/certificates/${https?.domain}`;
9602
- const httpsGuide = https ? deployM("deploy_bundleHttps", { params: { domain: https.domain, port: https.port, mode: https.type, certificates, environment } }) + (https.type === "letsencrypt" ? `
9603
- ${deployM("deploy_bundleAcme", { params: { state: `${serverDirectory}/https/acme`, domain: https.domain } })}` : "") : deployM("deploy_bundleHttpOnly", { params: { port: config.expose.hostPort } });
9604
- const accountGuide = deployM("deploy_bundleAccounts", { params: { sources: options.accountSources || "\u2014" } });
9605
- const guides = [runtimeGuide, variableGuide, httpsGuide, accountGuide, deployM("deploy_bundleDirectories", { params: { directory: serverDirectory, deploymentId: options.deploymentId } })].join("\n\n");
9606
- const bundleMetadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, deploymentId: options.deploymentId, compose: ".hai/compose.json" } });
9607
- const manualGuide = deployM("deploy_bundleApplyGuide", { params: { file: shellArgument(basename(output)), directory: serverDirectory, target: shellArgument(serverDirectory), runtime: config.remote.runtime, deploymentId: options.deploymentId } });
10218
+ const httpsGuide = https ? deployM("deploy_bundleHttps", {
10219
+ params: {
10220
+ domain: https.domain,
10221
+ port: https.port,
10222
+ mode: https.type,
10223
+ certificates,
10224
+ environment
10225
+ }
10226
+ }) + (https.type === "letsencrypt" ? `
10227
+ ${deployM("deploy_bundleAcme", {
10228
+ params: { state: `${serverDirectory}/https/acme`, domain: https.domain }
10229
+ })}` : "") : deployM("deploy_bundleHttpOnly", {
10230
+ params: { port: config.expose.hostPort }
10231
+ });
10232
+ const accountGuide = deployM("deploy_bundleAccounts", {
10233
+ params: { sources: options.accountSources || "\u2014" }
10234
+ });
10235
+ const guides = [
10236
+ runtimeGuide,
10237
+ variableGuide,
10238
+ httpsGuide,
10239
+ accountGuide,
10240
+ deployM("deploy_bundleDirectories", {
10241
+ params: { directory: serverDirectory, deploymentId: options.deploymentId }
10242
+ })
10243
+ ].join("\n\n");
10244
+ const bundleMetadata = deployM("deploy_bundleMetadata", {
10245
+ params: {
10246
+ version: artifact.appVersion,
10247
+ buildId: artifact.buildId,
10248
+ deploymentId: options.deploymentId,
10249
+ compose: ".hai/compose.json"
10250
+ }
10251
+ });
10252
+ const manualGuide = deployM("deploy_bundleApplyGuide", {
10253
+ params: {
10254
+ file: shellArgument(basename(output)),
10255
+ directory: serverDirectory,
10256
+ target: shellArgument(serverDirectory),
10257
+ runtime: config.remote.runtime,
10258
+ deploymentId: options.deploymentId
10259
+ }
10260
+ });
9608
10261
  put("APPLY.txt", `${bundleMetadata}
9609
10262
  ${selection}
9610
10263
 
@@ -9622,7 +10275,13 @@ ${guides}
9622
10275
  checksums.push(`${fileHashes.get(file) ?? await artifactChecksum(join(staging, file))} ${file}`);
9623
10276
  writeFileSync(join(staging, ".hai/SHA256SUMS"), `${checksums.join("\n")}
9624
10277
  `);
9625
- const tar = await runCommand("tar", [output.endsWith(".gz") ? "-czf" : "-cf", temporaryTar, "-C", staging, "."]);
10278
+ const tar = await runCommand("tar", [
10279
+ output.endsWith(".gz") ? "-czf" : "-cf",
10280
+ temporaryTar,
10281
+ "-C",
10282
+ staging,
10283
+ "."
10284
+ ]);
9626
10285
  if (!tar.success)
9627
10286
  throw new Error(tar.error.message);
9628
10287
  chmodSync(temporaryTar, 384);
@@ -9630,7 +10289,10 @@ ${guides}
9630
10289
  const upload = createUploadScripts();
9631
10290
  const scripts = directory;
9632
10291
  writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
9633
- writeFileSync(join(scripts, "deploy-local.sh"), createLocalInstallScript({ bundle: basename(output), directory: serverDirectory }), { mode: 448 });
10292
+ writeFileSync(join(scripts, "deploy-local.sh"), createLocalInstallScript({
10293
+ bundle: basename(output),
10294
+ directory: serverDirectory
10295
+ }), { mode: 448 });
9634
10296
  writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
9635
10297
  writeFileSync(join(scripts, "read-target.awk"), upload.reader);
9636
10298
  const target = {
@@ -9662,10 +10324,18 @@ ${guides}
9662
10324
  writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
9663
10325
  `);
9664
10326
  writeFileSync(join(scripts, "README.txt"), readme);
9665
- for (const file of [`docker-compose-${artifact.appVersion}.yml`, ".env.runtime", "middleware-runtime.env", "request-access.yml", ...options.hasWebConfig ? ["web.conf"] : []])
10327
+ for (const file of [
10328
+ `docker-compose-${artifact.appVersion}.yml`,
10329
+ ".env.runtime",
10330
+ "middleware-runtime.env",
10331
+ "request-access.yml",
10332
+ ...options.hasWebConfig ? ["web.conf"] : []
10333
+ ])
9666
10334
  rmSync(join(directory, file), { force: true });
9667
10335
  return { bundlePath: output, instructions: `${selection}
9668
- ${deployM("deploy_bundleInstructions", { params: { ...instructionParams, file: output } })}
10336
+ ${deployM("deploy_bundleInstructions", {
10337
+ params: { ...instructionParams, file: output }
10338
+ })}
9669
10339
  ${guides}
9670
10340
  ${scripts}`, startName: "apply.sh" };
9671
10341
  } finally {
@@ -9694,9 +10364,7 @@ function createHttpsAssets(config, projectDirectory) {
9694
10364
  try {
9695
10365
  const readPem = (file) => {
9696
10366
  const path = resolve(projectDirectory, file);
9697
- if (!statSync(path).isFile() || statSync(path).size > 1048576)
9698
- throw new Error("Invalid PEM file");
9699
- return readFileSync(path, "utf8");
10367
+ return readLocalFile(path, { maxBytes: 1048576 }).toString("utf8");
9700
10368
  };
9701
10369
  const certificate = readPem(config.certificateFile);
9702
10370
  const key = readPem(config.privateKeyFile);
@@ -9889,7 +10557,21 @@ function planPostgresDb(plan, projectName, password, net) {
9889
10557
  restart: "unless-stopped",
9890
10558
  env_file: ["middleware-runtime.env"],
9891
10559
  volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
9892
- healthcheck: { test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "hai", "-d", "hai"], interval: "2s", timeout: "5s", retries: 60 },
10560
+ healthcheck: {
10561
+ test: [
10562
+ "CMD",
10563
+ "pg_isready",
10564
+ "-h",
10565
+ "127.0.0.1",
10566
+ "-U",
10567
+ "hai",
10568
+ "-d",
10569
+ "hai"
10570
+ ],
10571
+ interval: "2s",
10572
+ timeout: "5s",
10573
+ retries: 60
10574
+ },
9893
10575
  networks: net
9894
10576
  };
9895
10577
  plan.volumes[`${projectName}-db-data`] = null;
@@ -9908,7 +10590,12 @@ function planMysqlDb(plan, projectName, password, net) {
9908
10590
  restart: "unless-stopped",
9909
10591
  env_file: ["middleware-runtime.env"],
9910
10592
  volumes: [`${projectName}-db-data:/var/lib/mysql`],
9911
- healthcheck: { test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"], interval: "2s", timeout: "5s", retries: 60 },
10593
+ healthcheck: {
10594
+ test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"],
10595
+ interval: "2s",
10596
+ timeout: "5s",
10597
+ retries: 60
10598
+ },
9912
10599
  networks: net
9913
10600
  };
9914
10601
  plan.volumes[`${projectName}-db-data`] = null;
@@ -9926,7 +10613,12 @@ function planRedisCache(plan, projectName, net) {
9926
10613
  image: IMAGES.redis,
9927
10614
  container_name: `hai-${projectName}-cache`,
9928
10615
  restart: "unless-stopped",
9929
- healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: "2s", timeout: "5s", retries: 60 },
10616
+ healthcheck: {
10617
+ test: ["CMD", "redis-cli", "ping"],
10618
+ interval: "2s",
10619
+ timeout: "5s",
10620
+ retries: 60
10621
+ },
9930
10622
  networks: net
9931
10623
  };
9932
10624
  plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
@@ -9938,15 +10630,40 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
9938
10630
  image: IMAGES[engine],
9939
10631
  container_name: `hai-${projectName}-storage`,
9940
10632
  restart: "unless-stopped",
9941
- ...engine === "minio" ? { command: ["server", "/data"] } : {},
9942
- healthcheck: { test: engine === "rustfs" ? ["CMD", "curl", "--noproxy", "*", "-fsS", "http://127.0.0.1:9000/minio/health/ready"] : ["CMD", "mc", "ready", "http://127.0.0.1:9000"], interval: "2s", timeout: "5s", retries: 60 },
10633
+ ...engine === "minio" ? {
10634
+ command: ["server", "/data"]
10635
+ } : {},
10636
+ healthcheck: {
10637
+ test: engine === "rustfs" ? [
10638
+ "CMD",
10639
+ "curl",
10640
+ "--noproxy",
10641
+ "*",
10642
+ "-fsS",
10643
+ "http://127.0.0.1:9000/minio/health/ready"
10644
+ ] : [
10645
+ "CMD",
10646
+ "mc",
10647
+ "ready",
10648
+ "http://127.0.0.1:9000"
10649
+ ],
10650
+ interval: "2s",
10651
+ timeout: "5s",
10652
+ retries: 60
10653
+ },
9943
10654
  env_file: ["middleware-runtime.env"],
9944
10655
  volumes: [`${projectName}-storage-data:/data`],
9945
10656
  networks: net
9946
10657
  };
9947
10658
  plan.volumes[`${projectName}-storage-data`] = null;
9948
10659
  if (engine === "rustfs") {
9949
- Object.assign(plan.containerEnv, { RUSTFS_ACCESS_KEY: "hai-storage", RUSTFS_SECRET_KEY: password, RUSTFS_ADDRESS: "0.0.0.0:9000", RUSTFS_VOLUMES: "/data", RUSTFS_CONSOLE_ENABLE: "false" });
10660
+ Object.assign(plan.containerEnv, {
10661
+ RUSTFS_ACCESS_KEY: "hai-storage",
10662
+ RUSTFS_SECRET_KEY: password,
10663
+ RUSTFS_ADDRESS: "0.0.0.0:9000",
10664
+ RUSTFS_VOLUMES: "/data",
10665
+ RUSTFS_CONSOLE_ENABLE: "false"
10666
+ });
9950
10667
  } else {
9951
10668
  Object.assign(plan.containerEnv, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
9952
10669
  }
@@ -9956,7 +10673,12 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
9956
10673
  bucket: configured.bucket || "hai",
9957
10674
  region: configured.region || "us-east-1",
9958
10675
  // 自建只替换连接和托管凭据,不能丢失对象前缀、外部签名地址或操作日志策略。
9959
- ...Object.fromEntries(["prefix", "publicEndpoint", "publicUrl", "operationLog"].filter((name) => configured[name] !== void 0).map((name) => [name, configured[name]])),
10676
+ ...Object.fromEntries([
10677
+ "prefix",
10678
+ "publicEndpoint",
10679
+ "publicUrl",
10680
+ "operationLog"
10681
+ ].filter((name) => configured[name] !== void 0).map((name) => [name, configured[name]])),
9960
10682
  accessKeyId: "hai-storage",
9961
10683
  secretAccessKey: password,
9962
10684
  forcePathStyle: true,
@@ -10081,11 +10803,30 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
10081
10803
  const password = secrets.ldapPassword ?? secrets.dbPassword;
10082
10804
  plan.services.push("ldap");
10083
10805
  plan.dependsOn.push("ldap");
10084
- plan.composeServices.ldap = { image: IMAGES.ldap, restart: "unless-stopped", env_file: ["middleware-runtime.env"], networks: net, volumes: [`${projectName}-ldap-data:/var/lib/ldap`, `${projectName}-ldap-config:/etc/ldap/slapd.d`] };
10806
+ plan.composeServices.ldap = {
10807
+ image: IMAGES.ldap,
10808
+ restart: "unless-stopped",
10809
+ env_file: ["middleware-runtime.env"],
10810
+ networks: net,
10811
+ volumes: [`${projectName}-ldap-data:/var/lib/ldap`, `${projectName}-ldap-config:/etc/ldap/slapd.d`]
10812
+ };
10085
10813
  plan.volumes[`${projectName}-ldap-data`] = null;
10086
10814
  plan.volumes[`${projectName}-ldap-config`] = null;
10087
- Object.assign(plan.containerEnv, { LDAP_DOMAIN: "example.org", LDAP_ORGANISATION: "HAI", LDAP_ADMIN_PASSWORD: password, LDAP_TLS: "false" });
10088
- Object.assign(plan.env, { HAI_IAM_LOGIN_LDAP: "true", HAI_IAM_LDAP: JSON.stringify({ url: "ldap://ldap:389", bindDn: "cn=admin,dc=example,dc=org", bindPassword: password, searchBase: "dc=example,dc=org" }) });
10815
+ Object.assign(plan.containerEnv, {
10816
+ LDAP_DOMAIN: "example.org",
10817
+ LDAP_ORGANISATION: "HAI",
10818
+ LDAP_ADMIN_PASSWORD: password,
10819
+ LDAP_TLS: "false"
10820
+ });
10821
+ Object.assign(plan.env, {
10822
+ HAI_IAM_LOGIN_LDAP: "true",
10823
+ HAI_IAM_LDAP: JSON.stringify({
10824
+ url: "ldap://ldap:389",
10825
+ bindDn: "cn=admin,dc=example,dc=org",
10826
+ bindPassword: password,
10827
+ searchBase: "dc=example,dc=org"
10828
+ })
10829
+ });
10089
10830
  plan.secrets.push(password);
10090
10831
  } else if (service === "email" && (scan.middlewares ? scan.middlewares.some((item) => item.service === "email" && item.backend === "smtp" && (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email ?? "default") === "default") : backends.email === "smtp") && !provisionEnv.HAI_REACH_PROVIDERS) {
10091
10832
  plan.services.push("email");
@@ -10093,10 +10834,25 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
10093
10834
  plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
10094
10835
  const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
10095
10836
  if (requirements?.length) {
10096
- for (const requirement of requirements)
10097
- plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: requirement.config.from || "noreply@example.com" });
10837
+ for (const requirement of requirements) {
10838
+ plan.env[requirement.envPrefix] = JSON.stringify({
10839
+ name: requirement.config.name ?? "email",
10840
+ type: "smtp",
10841
+ host: "email",
10842
+ port: 1025,
10843
+ secure: false,
10844
+ from: requirement.config.from || "noreply@example.com"
10845
+ });
10846
+ }
10098
10847
  } else if (!scan.middlewares) {
10099
- plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
10848
+ plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({
10849
+ name: "email",
10850
+ type: "smtp",
10851
+ host: "email",
10852
+ port: 1025,
10853
+ secure: false,
10854
+ from: "noreply@example.com"
10855
+ });
10100
10856
  }
10101
10857
  }
10102
10858
  }
@@ -10111,15 +10867,21 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
10111
10867
  const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
10112
10868
  const environment = options?.environment ?? appEnv.HAI_DEPLOY_ENVIRONMENT ?? "prod";
10113
10869
  const nodeEnv = environment === "dev" || environment === "development" ? "development" : environment === "test" ? "test" : "production";
10114
- const env = mergeEnvironmentLayers([{
10115
- NODE_ENV: nodeEnv
10116
- }, Object.fromEntries(Object.entries(appEnv).filter(([name]) => !CONTAINER_ENV_KEYS.has(name))), sidecar.env, provisionEnv, {
10117
- // 容器监听由本次制品决定,本机回环地址和开发端口不能覆盖部署拓扑。
10118
- HOST: "0.0.0.0",
10119
- PORT: String(containerPort),
10120
- HAI_SERV_SERVER_HOST: "0.0.0.0",
10121
- HAI_SERV_SERVER_PORT: String(containerPort)
10122
- }]);
10870
+ const env = mergeEnvironmentLayers([
10871
+ {
10872
+ NODE_ENV: nodeEnv
10873
+ },
10874
+ Object.fromEntries(Object.entries(appEnv).filter(([name]) => !CONTAINER_ENV_KEYS.has(name))),
10875
+ sidecar.env,
10876
+ provisionEnv,
10877
+ {
10878
+ // 容器监听由本次制品决定,本机回环地址和开发端口不能覆盖部署拓扑。
10879
+ HOST: "0.0.0.0",
10880
+ PORT: String(containerPort),
10881
+ HAI_SERV_SERVER_HOST: "0.0.0.0",
10882
+ HAI_SERV_SERVER_PORT: String(containerPort)
10883
+ }
10884
+ ]);
10123
10885
  return { env, sidecar };
10124
10886
  }
10125
10887
  function generateComposeFile(options) {
@@ -10139,7 +10901,12 @@ function generateComposeFile(options) {
10139
10901
  };
10140
10902
  if (primaryService === "serv") {
10141
10903
  app.healthcheck = {
10142
- test: ["CMD", "node", "-e", "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config', configSchemas: { serv: ServConfigSchema } }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"],
10904
+ test: [
10905
+ "CMD",
10906
+ "node",
10907
+ "-e",
10908
+ "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config', configSchemas: { serv: ServConfigSchema } }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"
10909
+ ],
10143
10910
  interval: "30s",
10144
10911
  timeout: "5s",
10145
10912
  start_period: "15s",
@@ -10157,14 +10924,31 @@ function generateComposeFile(options) {
10157
10924
  appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
10158
10925
  if (options.web || options.gateway)
10159
10926
  appEnvironment.HAI_DEPLOY_PRIVATE_PROXY = "1";
10927
+ if (nodeRuntime && primaryService === "web") {
10928
+ if (!options.gateway)
10929
+ throw new Error("SvelteKit SSR requires a private gateway");
10930
+ Object.assign(appEnvironment, {
10931
+ PROTOCOL_HEADER: "x-forwarded-proto",
10932
+ HOST_HEADER: "host",
10933
+ PORT_HEADER: "",
10934
+ ADDRESS_HEADER: "x-forwarded-for",
10935
+ XFF_DEPTH: "1"
10936
+ });
10937
+ }
10160
10938
  if (Object.keys(appEnvironment).length)
10161
10939
  app.environment = appEnvironment;
10162
10940
  if (options.mountWebConfig)
10163
10941
  appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
10164
10942
  if (appVolumes.length)
10165
10943
  app.volumes = appVolumes;
10166
- if (sidecar.dependsOn.length > 0)
10167
- app.depends_on = Object.fromEntries(sidecar.dependsOn.map((name) => [name, { condition: sidecar.composeServices[name]?.healthcheck ? "service_healthy" : "service_started" }]));
10944
+ if (sidecar.dependsOn.length > 0) {
10945
+ app.depends_on = Object.fromEntries(sidecar.dependsOn.map((name) => [
10946
+ name,
10947
+ {
10948
+ condition: sidecar.composeServices[name]?.healthcheck ? "service_healthy" : "service_started"
10949
+ }
10950
+ ]));
10951
+ }
10168
10952
  const services = { [primaryService]: app, ...sidecar.composeServices };
10169
10953
  if (options.web) {
10170
10954
  delete app.ports;
@@ -10176,7 +10960,21 @@ function generateComposeFile(options) {
10176
10960
  pull_policy: "never",
10177
10961
  ports: [`${hostPort}:${options.web.containerPort}`],
10178
10962
  environment: { PUBLIC_API_BASE: "" },
10179
- healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
10963
+ healthcheck: {
10964
+ test: [
10965
+ "CMD",
10966
+ "wget",
10967
+ "-Y",
10968
+ "off",
10969
+ "-q",
10970
+ "-O",
10971
+ "/dev/null",
10972
+ `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`
10973
+ ],
10974
+ interval: "30s",
10975
+ timeout: "5s",
10976
+ retries: 3
10977
+ },
10180
10978
  volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
10181
10979
  depends_on: sidecar.services.includes("storage") ? ["serv", "storage"] : ["serv"],
10182
10980
  networks: ["hai-app"]
@@ -10189,9 +10987,26 @@ function generateComposeFile(options) {
10189
10987
  container_name: `hai-${projectName}-gateway`,
10190
10988
  restart: "unless-stopped",
10191
10989
  ports: [`${hostPort}:8080`],
10192
- healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/_hai/gateway-health"], interval: "30s", timeout: "5s", retries: 3 },
10990
+ healthcheck: {
10991
+ test: [
10992
+ "CMD",
10993
+ "wget",
10994
+ "-Y",
10995
+ "off",
10996
+ "-q",
10997
+ "-O",
10998
+ "/dev/null",
10999
+ "http://127.0.0.1:8080/_hai/gateway-health"
11000
+ ],
11001
+ interval: "30s",
11002
+ timeout: "5s",
11003
+ retries: 3
11004
+ },
10193
11005
  volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
10194
- depends_on: [primaryService, ...sidecar.services.includes("storage") ? ["storage"] : []],
11006
+ depends_on: [
11007
+ primaryService,
11008
+ ...sidecar.services.includes("storage") ? ["storage"] : []
11009
+ ],
10195
11010
  networks: ["hai-app"]
10196
11011
  };
10197
11012
  }
@@ -10234,7 +11049,10 @@ function generateComposeFile(options) {
10234
11049
  };
10235
11050
  if (Object.keys(sidecar.volumes).length > 0)
10236
11051
  compose.volumes = sidecar.volumes;
10237
- for (const role of [primaryService, ...options.web ? ["web"] : []]) {
11052
+ for (const role of [
11053
+ primaryService,
11054
+ ...options.web ? ["web"] : []
11055
+ ]) {
10238
11056
  const hosts = Object.entries(options.compose?.services[role]?.extra_hosts ?? {});
10239
11057
  if (hosts.length)
10240
11058
  services[role].extra_hosts = hosts.map(([host, ip]) => `${host}:${ip}`);
@@ -10248,12 +11066,18 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
10248
11066
  const webPort = options.webPort ?? 8080;
10249
11067
  const webBasePath = options.webBasePath ?? "";
10250
11068
  const apiPath = apiPrefix.replace(/\/+$/, "");
10251
- const backendPaths = [...new Set(options.backendPaths ?? ["/health"])];
11069
+ const backendPaths = [
11070
+ ...new Set(options.backendPaths ?? ["/health"])
11071
+ ];
10252
11072
  if (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(apiPrefix) || apiPrefix.split("/").some((part) => part === "." || part === "..") || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535) || backendPaths.includes(`${webBasePath}/`) || backendPaths.some((path) => !/^\/[\w./-]*$/.test(path) || path.includes("//") || path.split("/").some((part) => part === "." || part === "..") || path === `${webBasePath}/runtime-config.js` || path === "/_hai/web-health") || webBasePath && (backendPaths.includes(webBasePath) || webBasePath === apiPath || webBasePath.startsWith(`${apiPath}/`))) {
10253
11073
  throw new Error("Invalid web proxy target");
10254
11074
  }
10255
11075
  const webLocation = renderWebLocation(webBasePath);
10256
- const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, ...backendPaths, ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], options.storageBasePath) : "";
11076
+ const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [
11077
+ apiPath,
11078
+ ...backendPaths,
11079
+ ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]
11080
+ ], options.storageBasePath) : "";
10257
11081
  const backendLocations = backendPaths.map((path) => ` location = ${path} {
10258
11082
  proxy_pass http://hai_serv_backend;
10259
11083
  proxy_http_version 1.1;
@@ -10345,11 +11169,13 @@ function generateApplicationProxyConfig(options) {
10345
11169
  if (!Number.isInteger(port) || port < 1 || port > 65535)
10346
11170
  throw new Error("Invalid application proxy target");
10347
11171
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
11172
+ const trustedEdges = renderTrustedEdgeProxies(options.trustedEdgeProxies);
11173
+ const forwardedProtocol = options.trustedEdgeProxies?.length ? "$hai_request_scheme" : "$scheme";
10348
11174
  return `include /tmp/hai-dns.conf;
10349
- ${renderResolvedUpstream("hai_application_backend", primaryService, port)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
11175
+ ${renderResolvedUpstream("hai_application_backend", primaryService, port)}${renderTrustedEdgeProtocol(options.trustedEdgeProxies)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
10350
11176
  listen 8080;
10351
11177
  server_name _;
10352
- ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
11178
+ ${trustedEdges} large_client_header_buffers 4 32k;
10353
11179
  ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
10354
11180
  access_log off;
10355
11181
  return 200 'ok';
@@ -10359,8 +11185,11 @@ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath)
10359
11185
  proxy_pass http://hai_application_backend;
10360
11186
  proxy_http_version 1.1;
10361
11187
  proxy_set_header Host $http_host;
10362
- proxy_set_header X-Forwarded-Proto $scheme;
11188
+ proxy_set_header X-Forwarded-Proto ${forwardedProtocol};
10363
11189
  proxy_set_header Connection "";
11190
+ proxy_set_header X-Forwarded-Host $http_host;
11191
+ proxy_set_header X-Forwarded-Port "";
11192
+ proxy_set_header Forwarded "";
10364
11193
  proxy_set_header X-Forwarded-For $remote_addr;
10365
11194
  proxy_set_header X-Real-IP $remote_addr;
10366
11195
  proxy_request_buffering off;
@@ -10388,6 +11217,20 @@ function renderTrustedEdgeProxies(entries = []) {
10388
11217
  real_ip_recursive on;
10389
11218
  `;
10390
11219
  }
11220
+ function renderTrustedEdgeProtocol(entries = []) {
11221
+ if (!entries.length)
11222
+ return "";
11223
+ return `geo $realip_remote_addr $hai_trusted_edge {
11224
+ default 0;
11225
+ ${entries.map((entry) => ` ${entry} 1;`).join("\n")}
11226
+ }
11227
+ map "$hai_trusted_edge:$http_x_forwarded_proto" $hai_request_scheme {
11228
+ default $scheme;
11229
+ "1:http" http;
11230
+ "1:https" https;
11231
+ }
11232
+ `;
11233
+ }
10391
11234
  function renderWebLocation(webBasePath) {
10392
11235
  if (webBasePath && (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(webBasePath) || webBasePath.split("/").some((segment) => segment === "." || segment === "..") || ["/health", "/_hai/web-health"].includes(webBasePath)))
10393
11236
  throw new Error("Invalid web base path");
@@ -10443,8 +11286,13 @@ function prepareRequestAccess(options) {
10443
11286
  if (!scope || typeof scope !== "object" || !("access" in scope))
10444
11287
  return ok({ settings: options.settings });
10445
11288
  const configured = RequestAccessConfigSchema.safeParse(scope.access);
10446
- if (!configured.success)
10447
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", { params: { field: options.module === "serv" ? "serv.http.access" : "kit.access" } }));
11289
+ if (!configured.success) {
11290
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
11291
+ params: {
11292
+ field: options.module === "serv" ? "serv.http.access" : "kit.access"
11293
+ }
11294
+ }));
11295
+ }
10448
11296
  try {
10449
11297
  const policy = core.access.loadPolicy(configured.data, options.directory);
10450
11298
  const check = core.access.createRequestCheck(policy);
@@ -10453,10 +11301,20 @@ function prepareRequestAccess(options) {
10453
11301
  if (configured.data === false || !("file" in configured.data))
10454
11302
  return ok({ settings: options.settings });
10455
11303
  const access = { file: "/run/hai/request-access.yml" };
10456
- const settings = options.module === "serv" ? { ...options.settings, http: { ...scope, access } } : { ...options.settings, access };
10457
- return ok({ settings, snapshot: stringify(policy, { aliasDuplicateObjects: false }) });
11304
+ const settings = options.module === "serv" ? {
11305
+ ...options.settings,
11306
+ http: { ...scope, access }
11307
+ } : { ...options.settings, access };
11308
+ return ok({
11309
+ settings,
11310
+ snapshot: stringify(policy, { aliasDuplicateObjects: false })
11311
+ });
10458
11312
  } catch {
10459
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", { params: { field: options.module === "serv" ? "serv.http.access" : "kit.access" } }));
11313
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
11314
+ params: {
11315
+ field: options.module === "serv" ? "serv.http.access" : "kit.access"
11316
+ }
11317
+ }));
10460
11318
  }
10461
11319
  }
10462
11320
  var HEALTH_MAX_ATTEMPTS = 40;
@@ -10475,10 +11333,25 @@ function endpointSettings(value) {
10475
11333
  }
10476
11334
  var ServerResultSchema = z.object({
10477
11335
  deploymentId: z.string(),
10478
- stage: z.enum(["preflight", "credentials", "recover", "images", "dependencies", "start", "https", "health", "complete"]),
11336
+ stage: z.enum([
11337
+ "preflight",
11338
+ "credentials",
11339
+ "recover",
11340
+ "images",
11341
+ "dependencies",
11342
+ "start",
11343
+ "https",
11344
+ "health",
11345
+ "complete"
11346
+ ]),
10479
11347
  exitCode: z.number().int().min(0).max(255),
10480
11348
  durationSeconds: z.number().int().nonnegative(),
10481
- recoveryStatus: z.enum(["not-needed", "passed", "failed", "stopped"]).optional()
11349
+ recoveryStatus: z.enum([
11350
+ "not-needed",
11351
+ "passed",
11352
+ "failed",
11353
+ "stopped"
11354
+ ]).optional()
10482
11355
  });
10483
11356
  function makeDeploymentId() {
10484
11357
  return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
@@ -10505,7 +11378,11 @@ function createDockerSshProvider(config, container) {
10505
11378
  }
10506
11379
  return err(
10507
11380
  HaiDeployError.REMOTE_COMMAND_FAILED,
10508
- deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
11381
+ deployM("deploy_remoteRuntimeMissing", {
11382
+ params: {
11383
+ runtime: candidates.join(" / ")
11384
+ }
11385
+ })
10509
11386
  );
10510
11387
  }
10511
11388
  async function healthCheck(url, certificate) {
@@ -10513,7 +11390,10 @@ function createDockerSshProvider(config, container) {
10513
11390
  try {
10514
11391
  if (certificate) {
10515
11392
  const healthy = await new Promise((resolve9) => {
10516
- const request$1 = request(url, { ca: [...rootCertificates, certificate], timeout: 5e3 }, (response) => {
11393
+ const request$1 = request(url, {
11394
+ ca: [...rootCertificates, certificate],
11395
+ timeout: 5e3
11396
+ }, (response) => {
10517
11397
  response.resume();
10518
11398
  resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
10519
11399
  });
@@ -10525,7 +11405,9 @@ function createDockerSshProvider(config, container) {
10525
11405
  if (healthy)
10526
11406
  return true;
10527
11407
  } else {
10528
- const response = await fetch(url, { signal: AbortSignal.timeout(5e3) });
11408
+ const response = await fetch(url, {
11409
+ signal: AbortSignal.timeout(5e3)
11410
+ });
10529
11411
  await response.body?.cancel();
10530
11412
  if (response.ok)
10531
11413
  return true;
@@ -10550,10 +11432,16 @@ function createDockerSshProvider(config, container) {
10550
11432
  const primaryService = artifact.primaryService;
10551
11433
  const app = images.find((image) => image.name === primaryService);
10552
11434
  if (options?.selectedServices) {
10553
- const available = [...images.map((image) => image.name), ...scan.requiredServices];
11435
+ const available = [
11436
+ ...images.map((image) => image.name),
11437
+ ...scan.requiredServices
11438
+ ];
10554
11439
  const invalid = options.selectedServices.find((name2) => !available.includes(name2));
10555
- if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
10556
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
11440
+ if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid) {
11441
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
11442
+ params: { service: invalid ?? options.selectedServices.join(", ") }
11443
+ }));
11444
+ }
10557
11445
  }
10558
11446
  const hasWebProxy = primaryService === "serv" && artifact.hasWeb;
10559
11447
  const partial = artifact.partial || options?.selectedServices !== void 0;
@@ -10568,14 +11456,12 @@ function createDockerSshProvider(config, container) {
10568
11456
  const plannedRuntime = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
10569
11457
  const { sidecar } = plannedRuntime;
10570
11458
  let { env } = plannedRuntime;
10571
- if (https && scan.isSvelteKit && !env.ORIGIN)
10572
- env.ORIGIN = url;
10573
11459
  const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
10574
11460
  let storageBasePath = "";
10575
11461
  if (managedStorage?.publicEndpoint !== void 0) {
10576
11462
  try {
10577
11463
  if (typeof managedStorage.publicEndpoint !== "string")
10578
- throw new Error("Invalid public endpoint type");
11464
+ throw new TypeError("Invalid public endpoint type");
10579
11465
  const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
10580
11466
  const endpoint = new URL(managedStorage.publicEndpoint);
10581
11467
  if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !/^\/(?:[\w.-]+(?:\/[\w.-]+)*\/?)?$/.test(endpoint.pathname) || unsafeCharacters || /(?:^|\/)\.{1,2}(?:\/|$)/.test(managedStorage.publicEndpoint)) {
@@ -10587,7 +11473,7 @@ function createDockerSshProvider(config, container) {
10587
11473
  }
10588
11474
  }
10589
11475
  const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
10590
- const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
11476
+ const gateway = !hasWebProxy && (scan.isSvelteKit || !!managedStorage || !!https && !standaloneWebConfig);
10591
11477
  if (gateway && primaryService === "web" && !scan.isSvelteKit)
10592
11478
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
10593
11479
  const proxyService = gateway ? "gateway" : "web";
@@ -10649,8 +11535,14 @@ function createDockerSshProvider(config, container) {
10649
11535
  return prepared2;
10650
11536
  Object.assign(server.data, prepared2.data.settings);
10651
11537
  requestAccessSnapshot = prepared2.data.snapshot;
10652
- if (requestAccessSnapshot)
10653
- env = mergeEnvironmentLayers([env, { HAI_SERV: JSON.stringify(server.data) }]);
11538
+ if (requestAccessSnapshot) {
11539
+ env = mergeEnvironmentLayers([
11540
+ env,
11541
+ {
11542
+ HAI_SERV: JSON.stringify(server.data)
11543
+ }
11544
+ ]);
11545
+ }
10654
11546
  }
10655
11547
  let feedbackTransportBytes;
10656
11548
  if (scan.haiModules?.includes("@h-ai/feedback")) {
@@ -10676,8 +11568,14 @@ function createDockerSshProvider(config, container) {
10676
11568
  return prepared2;
10677
11569
  Object.assign(kit.data, prepared2.data.settings);
10678
11570
  requestAccessSnapshot = prepared2.data.snapshot;
10679
- if (requestAccessSnapshot)
10680
- env = mergeEnvironmentLayers([env, { HAI_KIT: JSON.stringify(kit.data) }]);
11571
+ if (requestAccessSnapshot) {
11572
+ env = mergeEnvironmentLayers([
11573
+ env,
11574
+ {
11575
+ HAI_KIT: JSON.stringify(kit.data)
11576
+ }
11577
+ ]);
11578
+ }
10681
11579
  }
10682
11580
  const transport = kit.data.transport;
10683
11581
  if (transport && typeof transport === "object") {
@@ -10693,8 +11591,13 @@ function createDockerSshProvider(config, container) {
10693
11591
  }
10694
11592
  }
10695
11593
  }
10696
- if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success)
10697
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_transportKeyRingInvalid", { params: { path: join(scan.envDir ?? appDir, `.env.${options?.environment ?? "prod"}.local`) } }));
11594
+ if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success) {
11595
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_transportKeyRingInvalid", {
11596
+ params: {
11597
+ path: join(scan.envDir ?? appDir, `.env.${options?.environment ?? "prod"}.local`)
11598
+ }
11599
+ }));
11600
+ }
10698
11601
  let webProxy;
10699
11602
  if (hasWebProxy) {
10700
11603
  const http = server.data.http;
@@ -10709,11 +11612,30 @@ function createDockerSshProvider(config, container) {
10709
11612
  try {
10710
11613
  const configured = transport ? transport.maxPlaintextBytes : void 0;
10711
11614
  const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
10712
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, backendPaths, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config.trustedEdgeProxies });
11615
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, {
11616
+ webPort: webContainerPort,
11617
+ maxPlaintextBytes: limits.plaintextBytes,
11618
+ webBasePath,
11619
+ backendPaths,
11620
+ storageBucket: managedStorage?.bucket,
11621
+ storageBasePath,
11622
+ trustedEdgeProxies: config.trustedEdgeProxies
11623
+ });
10713
11624
  } catch (error) {
10714
11625
  return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
10715
11626
  }
10716
- env = mergeEnvironmentLayers([env, { HAI_SERV: JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && (config.ssh || https) ? url : cors?.origin ?? "*" } }) }]);
11627
+ env = mergeEnvironmentLayers([
11628
+ env,
11629
+ {
11630
+ HAI_SERV: JSON.stringify({
11631
+ ...server.data,
11632
+ cors: {
11633
+ ...cors,
11634
+ origin: (!cors?.origin || cors.origin === "*") && (config.ssh || https) ? url : cors?.origin ?? "*"
11635
+ }
11636
+ })
11637
+ }
11638
+ ]);
10717
11639
  } else if (gateway) {
10718
11640
  try {
10719
11641
  const http = server.data.http;
@@ -10721,7 +11643,18 @@ function createDockerSshProvider(config, container) {
10721
11643
  const transport = server.data.transport;
10722
11644
  applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
10723
11645
  }
10724
- webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage?.bucket, storageBasePath, reservedPaths: [http?.apiPrefix ?? "/api/v1", ...storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], maxPlaintextBytes: applicationPlaintextBytes, trustedEdgeProxies: config.trustedEdgeProxies });
11646
+ webProxy = generateApplicationProxyConfig({
11647
+ primaryService,
11648
+ port: containerPort,
11649
+ bucket: managedStorage?.bucket,
11650
+ storageBasePath,
11651
+ reservedPaths: [
11652
+ http?.apiPrefix ?? "/api/v1",
11653
+ ...storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]
11654
+ ],
11655
+ maxPlaintextBytes: applicationPlaintextBytes,
11656
+ trustedEdgeProxies: config.trustedEdgeProxies
11657
+ });
10725
11658
  } catch {
10726
11659
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
10727
11660
  }
@@ -10730,11 +11663,23 @@ function createDockerSshProvider(config, container) {
10730
11663
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
10731
11664
  webProxy = generateStaticWebConfig(containerPort, webBasePath);
10732
11665
  }
10733
- if (https && webProxy)
11666
+ if (https && webProxy) {
10734
11667
  webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
11668
+ }
10735
11669
  if (https && primaryService === "serv" && !hasWebProxy) {
10736
11670
  const cors = server.data.cors;
10737
- env = mergeEnvironmentLayers([env, { HAI_SERV: JSON.stringify({ ...server.data, cors: { ...cors, origin: !cors?.origin || cors.origin === "*" ? url : cors.origin } }) }]);
11671
+ env = mergeEnvironmentLayers([
11672
+ env,
11673
+ {
11674
+ HAI_SERV: JSON.stringify({
11675
+ ...server.data,
11676
+ cors: {
11677
+ ...cors,
11678
+ origin: !cors?.origin || cors.origin === "*" ? url : cors.origin
11679
+ }
11680
+ })
11681
+ }
11682
+ ]);
10738
11683
  }
10739
11684
  const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
10740
11685
  const compressed = requestedOutput.endsWith(".tar.gz");
@@ -10744,20 +11689,43 @@ function createDockerSshProvider(config, container) {
10744
11689
  if (!/^[\w.+-]+$/.test(name))
10745
11690
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleNameInvalid"));
10746
11691
  const artifactDir = resolve(dirname(requestedOutput), name);
10747
- if (existsSync(artifactDir))
10748
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleDirectoryExists", { params: { path: artifactDir } }));
11692
+ if (existsSync(artifactDir)) {
11693
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleDirectoryExists", {
11694
+ params: { path: artifactDir }
11695
+ }));
11696
+ }
10749
11697
  mkdirSync(dirname(artifactDir), { recursive: true, mode: 448 });
10750
11698
  mkdirSync(artifactDir, { mode: 448 });
10751
11699
  chmodSync(artifactDir, 448);
10752
11700
  const envPath = join(artifactDir, ".env.runtime");
10753
11701
  const nodeRuntime = primaryService === "serv" || scan.isSvelteKit;
10754
11702
  const startupOverrides = nodeRuntime ? { NODE_OPTIONS: "", HAI_DEPLOY_PRIVATE_PROXY: "" } : {};
11703
+ if (scan.isSvelteKit) {
11704
+ for (const name2 of [
11705
+ "ORIGIN",
11706
+ "PROTOCOL_HEADER",
11707
+ "HOST_HEADER",
11708
+ "PORT_HEADER",
11709
+ "ADDRESS_HEADER",
11710
+ "XFF_DEPTH"
11711
+ ])
11712
+ startupOverrides[name2] = "";
11713
+ }
10755
11714
  const runtimeEnvironment = serializeEnvFile(env, startupOverrides, nodeRuntime ? "node" : "compose");
10756
- const envVarsSet = [.../* @__PURE__ */ new Set([
10757
- ...Object.keys(parseEnv(runtimeEnvironment)),
10758
- ...nodeRuntime ? ["NODE_OPTIONS"] : [],
10759
- ...hasWebProxy || gateway ? ["HAI_DEPLOY_PRIVATE_PROXY"] : []
10760
- ])];
11715
+ const envVarsSet = [
11716
+ .../* @__PURE__ */ new Set([
11717
+ ...Object.keys(parseEnv(runtimeEnvironment)),
11718
+ ...nodeRuntime ? ["NODE_OPTIONS"] : [],
11719
+ ...hasWebProxy || gateway ? ["HAI_DEPLOY_PRIVATE_PROXY"] : [],
11720
+ ...scan.isSvelteKit ? [
11721
+ "PROTOCOL_HEADER",
11722
+ "HOST_HEADER",
11723
+ "PORT_HEADER",
11724
+ "ADDRESS_HEADER",
11725
+ "XFF_DEPTH"
11726
+ ] : []
11727
+ ])
11728
+ ];
10761
11729
  writeFileSync(envPath, runtimeEnvironment, { mode: 384 });
10762
11730
  if (requestAccessSnapshot)
10763
11731
  writeFileSync(join(artifactDir, "request-access.yml"), requestAccessSnapshot, { mode: 384 });
@@ -10766,7 +11734,12 @@ function createDockerSshProvider(config, container) {
10766
11734
  const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
10767
11735
  if (!environment.success)
10768
11736
  return environment;
10769
- const saved = saveDeploymentEnvironment(environment.data, { HAI_DEPLOY_ENVIRONMENT: environment.data.environment, HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV, ...sidecar.env, ...provisionEnv });
11737
+ const saved = saveDeploymentEnvironment(environment.data, {
11738
+ HAI_DEPLOY_ENVIRONMENT: environment.data.environment,
11739
+ HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV,
11740
+ ...sidecar.env,
11741
+ ...provisionEnv
11742
+ });
10770
11743
  if (!saved.success)
10771
11744
  return saved;
10772
11745
  }
@@ -10792,16 +11765,27 @@ function createDockerSshProvider(config, container) {
10792
11765
  writeFileSync(composePath, composeContent);
10793
11766
  if (webProxy)
10794
11767
  writeFileSync(join(artifactDir, "web.conf"), webProxy);
10795
- const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
11768
+ const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [
11769
+ ...images.map((image) => image.name),
11770
+ ...Object.keys(sidecar.composeServices)
11771
+ ];
10796
11772
  if (gateway && !partial)
10797
11773
  selected.push("gateway");
10798
11774
  if (https?.type === "letsencrypt" && !partial)
10799
11775
  selected.push("certbot");
10800
- if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
10801
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
11776
+ if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length) {
11777
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
11778
+ params: {
11779
+ service: selected.join(", ")
11780
+ }
11781
+ }));
11782
+ }
10802
11783
  for (const name2 of selected) {
10803
- if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2))
10804
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name2 } }));
11784
+ if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2)) {
11785
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
11786
+ params: { service: name2 }
11787
+ }));
11788
+ }
10805
11789
  }
10806
11790
  options?.onProgress?.("packaging");
10807
11791
  const bundle = await createDeploymentBundle({
@@ -10823,14 +11807,28 @@ function createDockerSshProvider(config, container) {
10823
11807
  containerPort,
10824
11808
  managedAppKeys: Object.keys(sidecar.env),
10825
11809
  unmanagedAppKeys: scan.middlewares?.filter((item) => !Object.hasOwn(sidecar.env, item.envPrefix)).map((item) => item.envPrefix),
10826
- accountSources: describeDeploymentVariableSources(options?.envDir ?? scan.envDir ?? appDir, options?.environment ?? "prod", env, ["HAI_ADMIN_DEFAULT_PASSWORD", "HAI_AUDIT_ADMIN_PASSWORD", "HAI_CONFIG_ADMIN_PASSWORD", "HAI_BUSINESS_OPERATOR_PASSWORD"]),
11810
+ accountSources: describeDeploymentVariableSources(options?.envDir ?? scan.envDir ?? appDir, options?.environment ?? "prod", env, [
11811
+ "HAI_ADMIN_DEFAULT_PASSWORD",
11812
+ "HAI_AUDIT_ADMIN_PASSWORD",
11813
+ "HAI_CONFIG_ADMIN_PASSWORD",
11814
+ "HAI_BUSINESS_OPERATOR_PASSWORD"
11815
+ ]),
10827
11816
  healthPaths,
10828
11817
  httpsAssets,
10829
11818
  output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
10830
11819
  cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
10831
11820
  });
10832
- if (options?.bundleOnly)
10833
- return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet, ...bundle });
11821
+ if (options?.bundleOnly) {
11822
+ return ok({
11823
+ url: "",
11824
+ deploymentId,
11825
+ status: "packaged",
11826
+ provider: "docker-ssh",
11827
+ healthCheck: "skipped",
11828
+ envVarsSet,
11829
+ ...bundle
11830
+ });
11831
+ }
10834
11832
  if (!ssh)
10835
11833
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
10836
11834
  const ping = await ssh.ping();
@@ -10850,19 +11848,23 @@ function createDockerSshProvider(config, container) {
10850
11848
  let pendingOutput = "";
10851
11849
  const started = await ssh.exec(
10852
11850
  `mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)} && { rm -rf -- ${shellArgument(`${remoteTar}.contents`)} && rm -f -- ${shellArgument(remoteTar)} || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }`,
10853
- { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
10854
- const parts = output.split("\n");
10855
- for (let index = 0; index < parts.length; index++) {
10856
- pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
10857
- if (index === parts.length - 1)
10858
- break;
10859
- const line = pendingOutput;
10860
- pendingOutput = "";
10861
- const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
10862
- if (stage)
10863
- options?.onProgress?.(stage);
11851
+ {
11852
+ timeoutMs: 6e5,
11853
+ secrets: sidecar.secrets,
11854
+ onOutput: (output) => {
11855
+ const parts = output.split("\n");
11856
+ for (let index = 0; index < parts.length; index++) {
11857
+ pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
11858
+ if (index === parts.length - 1)
11859
+ break;
11860
+ const line = pendingOutput;
11861
+ pendingOutput = "";
11862
+ const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
11863
+ if (stage)
11864
+ options?.onProgress?.(stage);
11865
+ }
10864
11866
  }
10865
- } }
11867
+ }
10866
11868
  );
10867
11869
  if (!started.success) {
10868
11870
  const resultFile = `${remoteDir}/result-${deploymentId}.json`;
@@ -10877,16 +11879,34 @@ function createDockerSshProvider(config, container) {
10877
11879
  } catch {
10878
11880
  }
10879
11881
  }
10880
- return err({ ...started.error, message: `${started.error.message}
10881
- ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown")}`, suggestion: terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown"), ext: { ...started.error.ext, commandExecution: {
10882
- ...state,
10883
- reason: state?.reason ?? "exit",
10884
- localTermination: state?.localTermination ?? "unknown",
10885
- remoteExecution: terminal ? "finished" : "unknown",
10886
- deploymentId,
10887
- resultFile,
10888
- ...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage, remoteRecovery: terminal.recoveryStatus } : {}
10889
- } } });
11882
+ return err({
11883
+ ...started.error,
11884
+ message: `${started.error.message}
11885
+ ${terminal ? deployM("deploy_remoteFinished", {
11886
+ params: {
11887
+ stage: terminal.stage,
11888
+ code: String(terminal.exitCode)
11889
+ }
11890
+ }) : deployM("deploy_remoteUnknown")}`,
11891
+ suggestion: terminal ? deployM("deploy_remoteFinished", {
11892
+ params: {
11893
+ stage: terminal.stage,
11894
+ code: String(terminal.exitCode)
11895
+ }
11896
+ }) : deployM("deploy_remoteUnknown"),
11897
+ ext: {
11898
+ ...started.error.ext,
11899
+ commandExecution: {
11900
+ ...state,
11901
+ reason: state?.reason ?? "exit",
11902
+ localTermination: state?.localTermination ?? "unknown",
11903
+ remoteExecution: terminal ? "finished" : "unknown",
11904
+ deploymentId,
11905
+ resultFile,
11906
+ ...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage, remoteRecovery: terminal.recoveryStatus } : {}
11907
+ }
11908
+ }
11909
+ });
10890
11910
  }
10891
11911
  logger5.info("Running health check", { url: applicationUrl });
10892
11912
  let healthy = true;
@@ -10898,16 +11918,24 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
10898
11918
  }
10899
11919
  if (!healthy) {
10900
11920
  return err(
10901
- { ...HaiDeployError.HEALTH_CHECK_FAILED, message: deployM("deploy_healthCheckFailed", { params: { url: applicationUrl } }), ext: { commandExecution: {
10902
- reason: "exit",
10903
- localTermination: "confirmed",
10904
- remoteExecution: "finished",
10905
- deploymentId,
10906
- resultFile: `${remoteDir}/result-${deploymentId}.json`,
10907
- remoteExitCode: 0,
10908
- remoteStage: "complete",
10909
- remoteRecovery: "not-needed"
10910
- } } }
11921
+ {
11922
+ ...HaiDeployError.HEALTH_CHECK_FAILED,
11923
+ message: deployM("deploy_healthCheckFailed", {
11924
+ params: { url: applicationUrl }
11925
+ }),
11926
+ ext: {
11927
+ commandExecution: {
11928
+ reason: "exit",
11929
+ localTermination: "confirmed",
11930
+ remoteExecution: "finished",
11931
+ deploymentId,
11932
+ resultFile: `${remoteDir}/result-${deploymentId}.json`,
11933
+ remoteExitCode: 0,
11934
+ remoteStage: "complete",
11935
+ remoteRecovery: "not-needed"
11936
+ }
11937
+ }
11938
+ }
10911
11939
  );
10912
11940
  }
10913
11941
  const result = {
@@ -10929,11 +11957,16 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
10929
11957
  const reason = error instanceof Error ? error.message : String(error);
10930
11958
  let safeReason = "Deployment bundle failed";
10931
11959
  try {
10932
- safeReason = maskSecrets(reason, [...Object.values(context.appEnv ?? {}), config.ssh?.password ?? ""].filter(Boolean));
11960
+ safeReason = maskSecrets(reason, [
11961
+ ...Object.values(context.appEnv ?? {}),
11962
+ config.ssh?.password ?? ""
11963
+ ].filter(Boolean));
10933
11964
  } catch {
10934
11965
  }
10935
11966
  logger5.error("Deployment bundle failed", { reason: safeReason });
10936
- return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: safeReason } }));
11967
+ return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", {
11968
+ params: { reason: safeReason }
11969
+ }));
10937
11970
  }
10938
11971
  };
10939
11972
  return { name: "docker-ssh", deploy: deploySafely };
@@ -10944,7 +11977,12 @@ var POLL_INTERVAL = 3e3;
10944
11977
  var MAX_POLL_ATTEMPTS = 60;
10945
11978
  var VercelApiError = class extends Error {
10946
11979
  constructor(status) {
10947
- super(deployM("deploy_apiError", { params: { service: "Vercel", status: String(status) } }));
11980
+ super(deployM("deploy_apiError", {
11981
+ params: {
11982
+ service: "Vercel",
11983
+ status: String(status)
11984
+ }
11985
+ }));
10948
11986
  this.status = status;
10949
11987
  }
10950
11988
  status;
@@ -10953,7 +11991,9 @@ function toDeployError(codeDef, messageKey, error) {
10953
11991
  return {
10954
11992
  ...codeDef,
10955
11993
  message: deployM(messageKey, {
10956
- params: { error: error instanceof Error ? error.message : String(error) }
11994
+ params: {
11995
+ error: error instanceof Error ? error.message : String(error)
11996
+ }
10957
11997
  }),
10958
11998
  cause: error
10959
11999
  };
@@ -11163,7 +12203,11 @@ async function uploadFile(token, content, sha) {
11163
12203
  body: new Blob([uploadContent])
11164
12204
  });
11165
12205
  if (!response.ok && response.status !== 409) {
11166
- throw new Error(deployM("deploy_fileUploadFailed", { params: { status: String(response.status) } }));
12206
+ throw new Error(deployM("deploy_fileUploadFailed", {
12207
+ params: {
12208
+ status: String(response.status)
12209
+ }
12210
+ }));
11167
12211
  }
11168
12212
  }
11169
12213
  async function pollDeploymentStatus(token, deploymentId) {
@@ -11196,7 +12240,9 @@ function createAliyunProvisioner() {
11196
12240
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
11197
12241
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
11198
12242
  if (!akId || !akSecret || !credentials.signName) {
11199
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accessKeyId, accessKeySecret, signName" } }));
12243
+ throw new Error(deployM("deploy_credentialMissing", {
12244
+ params: { fields: "accessKeyId, accessKeySecret, signName" }
12245
+ }));
11200
12246
  }
11201
12247
  accessKey = akId;
11202
12248
  secretKey = akSecret;
@@ -11208,7 +12254,9 @@ function createAliyunProvisioner() {
11208
12254
  return err(
11209
12255
  HaiDeployError.AUTH_FAILED,
11210
12256
  deployM("deploy_authFailed", {
11211
- params: { error: error instanceof Error ? error.message : String(error) }
12257
+ params: {
12258
+ error: error instanceof Error ? error.message : String(error)
12259
+ }
11212
12260
  }),
11213
12261
  error
11214
12262
  );
@@ -11227,7 +12275,15 @@ function createAliyunProvisioner() {
11227
12275
  provisionerName: "aliyun",
11228
12276
  resourceStatus: "reused",
11229
12277
  envVars: {
11230
- HAI_REACH_PROVIDERS: JSON.stringify([{ name: "sms", type: "aliyun-sms", accessKeyId: accessKey, accessKeySecret: secretKey, signName }])
12278
+ HAI_REACH_PROVIDERS: JSON.stringify([
12279
+ {
12280
+ name: "sms",
12281
+ type: "aliyun-sms",
12282
+ accessKeyId: accessKey,
12283
+ accessKeySecret: secretKey,
12284
+ signName
12285
+ }
12286
+ ])
11231
12287
  },
11232
12288
  resourceInfo: "aliyun:verify-only"
11233
12289
  });
@@ -11249,7 +12305,12 @@ async function neonFetch(token, path, options) {
11249
12305
  }
11250
12306
  });
11251
12307
  if (!response.ok) {
11252
- throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(response.status) } }));
12308
+ throw new Error(deployM("deploy_apiError", {
12309
+ params: {
12310
+ service: "Neon",
12311
+ status: String(response.status)
12312
+ }
12313
+ }));
11253
12314
  }
11254
12315
  return response.json();
11255
12316
  }
@@ -11269,10 +12330,14 @@ async function getNeonConnectionUri(token, projectId) {
11269
12330
  const data = await neonFetch(
11270
12331
  token,
11271
12332
  `/projects/${projectId}/connection_uri?${query.toString()}`,
11272
- { headers: { "Content-Type": "application/json" } }
12333
+ {
12334
+ headers: { "Content-Type": "application/json" }
12335
+ }
11273
12336
  );
11274
12337
  if (!data.uri) {
11275
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
12338
+ throw new Error(deployM("deploy_provisionNoResult", {
12339
+ params: { service: "Neon" }
12340
+ }));
11276
12341
  }
11277
12342
  return data.uri;
11278
12343
  }
@@ -11282,7 +12347,11 @@ function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
11282
12347
  provisionerName: "neon",
11283
12348
  resourceStatus,
11284
12349
  envVars: {
11285
- HAI_DB: JSON.stringify({ type: "postgresql", url: connectionUri, database: decodeURIComponent(new URL(connectionUri).pathname.slice(1)) })
12350
+ HAI_DB: JSON.stringify({
12351
+ type: "postgresql",
12352
+ url: connectionUri,
12353
+ database: decodeURIComponent(new URL(connectionUri).pathname.slice(1))
12354
+ })
11286
12355
  },
11287
12356
  resourceInfo: `neon-project:${projectId}`
11288
12357
  };
@@ -11297,7 +12366,9 @@ function createNeonProvisioner() {
11297
12366
  try {
11298
12367
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
11299
12368
  if (!apiToken) {
11300
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "token" } }));
12369
+ throw new Error(deployM("deploy_credentialMissing", {
12370
+ params: { fields: "token" }
12371
+ }));
11301
12372
  }
11302
12373
  await listNeonProjects(apiToken);
11303
12374
  token = apiToken;
@@ -11308,7 +12379,9 @@ function createNeonProvisioner() {
11308
12379
  return err(
11309
12380
  HaiDeployError.AUTH_FAILED,
11310
12381
  deployM("deploy_authFailed", {
11311
- params: { error: error instanceof Error ? error.message : String(error) }
12382
+ params: {
12383
+ error: error instanceof Error ? error.message : String(error)
12384
+ }
11312
12385
  }),
11313
12386
  error
11314
12387
  );
@@ -11349,7 +12422,9 @@ function createNeonProvisioner() {
11349
12422
  resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
11350
12423
  const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
11351
12424
  if (!projectId || !connectionUri) {
11352
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
12425
+ throw new Error(deployM("deploy_provisionNoResult", {
12426
+ params: { service: "Neon" }
12427
+ }));
11353
12428
  }
11354
12429
  logger8.info("Neon database provisioned", { projectId });
11355
12430
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
@@ -11359,7 +12434,9 @@ function createNeonProvisioner() {
11359
12434
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
11360
12435
  projectName: appName,
11361
12436
  stage: "provision:db",
11362
- resources: [{ serviceType: "db", provisionerName: "neon", ...resource }]
12437
+ resources: [
12438
+ { serviceType: "db", provisionerName: "neon", ...resource }
12439
+ ]
11363
12440
  });
11364
12441
  }
11365
12442
  return err(
@@ -11392,13 +12469,20 @@ function createR2Provisioner() {
11392
12469
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
11393
12470
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
11394
12471
  if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
11395
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" } }));
12472
+ throw new Error(deployM("deploy_credentialMissing", {
12473
+ params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" }
12474
+ }));
11396
12475
  }
11397
12476
  const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
11398
12477
  headers: { Authorization: `Bearer ${apiTok}` }
11399
12478
  });
11400
12479
  if (!res.ok) {
11401
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(res.status) } }));
12480
+ throw new Error(deployM("deploy_apiError", {
12481
+ params: {
12482
+ service: "Cloudflare",
12483
+ status: String(res.status)
12484
+ }
12485
+ }));
11402
12486
  }
11403
12487
  accountId = acctId;
11404
12488
  token = apiTok;
@@ -11411,7 +12495,9 @@ function createR2Provisioner() {
11411
12495
  return err(
11412
12496
  HaiDeployError.AUTH_FAILED,
11413
12497
  deployM("deploy_authFailed", {
11414
- params: { error: error instanceof Error ? error.message : String(error) }
12498
+ params: {
12499
+ error: error instanceof Error ? error.message : String(error)
12500
+ }
11415
12501
  }),
11416
12502
  error
11417
12503
  );
@@ -11439,7 +12525,12 @@ function createR2Provisioner() {
11439
12525
  }
11440
12526
  );
11441
12527
  if (!createRes.ok && createRes.status !== 409) {
11442
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
12528
+ throw new Error(deployM("deploy_apiError", {
12529
+ params: {
12530
+ service: "Cloudflare",
12531
+ status: String(createRes.status)
12532
+ }
12533
+ }));
11443
12534
  }
11444
12535
  logger9.info("R2 bucket provisioned", { bucketName });
11445
12536
  return ok({
@@ -11487,13 +12578,20 @@ function createResendProvisioner() {
11487
12578
  try {
11488
12579
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
11489
12580
  if (!apiToken || !credentials.from) {
11490
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "apiKey, from" } }));
12581
+ throw new Error(deployM("deploy_credentialMissing", {
12582
+ params: { fields: "apiKey, from" }
12583
+ }));
11491
12584
  }
11492
12585
  const res = await fetch(`${RESEND_API}/domains`, {
11493
12586
  headers: { Authorization: `Bearer ${apiToken}` }
11494
12587
  });
11495
12588
  if (!res.ok) {
11496
- throw new Error(deployM("deploy_apiError", { params: { service: "Resend", status: String(res.status) } }));
12589
+ throw new Error(deployM("deploy_apiError", {
12590
+ params: {
12591
+ service: "Resend",
12592
+ status: String(res.status)
12593
+ }
12594
+ }));
11497
12595
  }
11498
12596
  token = apiToken;
11499
12597
  from = credentials.from;
@@ -11504,7 +12602,9 @@ function createResendProvisioner() {
11504
12602
  return err(
11505
12603
  HaiDeployError.AUTH_FAILED,
11506
12604
  deployM("deploy_authFailed", {
11507
- params: { error: error instanceof Error ? error.message : String(error) }
12605
+ params: {
12606
+ error: error instanceof Error ? error.message : String(error)
12607
+ }
11508
12608
  }),
11509
12609
  error
11510
12610
  );
@@ -11523,7 +12623,18 @@ function createResendProvisioner() {
11523
12623
  provisionerName: "resend",
11524
12624
  resourceStatus: "reused",
11525
12625
  envVars: {
11526
- HAI_REACH_PROVIDERS: JSON.stringify([{ name: "email", type: "smtp", host: "smtp.resend.com", port: 465, secure: true, user: "resend", pass: token, from }])
12626
+ HAI_REACH_PROVIDERS: JSON.stringify([
12627
+ {
12628
+ name: "email",
12629
+ type: "smtp",
12630
+ host: "smtp.resend.com",
12631
+ port: 465,
12632
+ secure: true,
12633
+ user: "resend",
12634
+ pass: token,
12635
+ from
12636
+ }
12637
+ ])
11527
12638
  },
11528
12639
  resourceInfo: "resend:verify-only"
11529
12640
  });
@@ -11545,7 +12656,12 @@ async function upstashFetch(email, apiKey, path, options) {
11545
12656
  }
11546
12657
  });
11547
12658
  if (!response.ok) {
11548
- throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(response.status) } }));
12659
+ throw new Error(deployM("deploy_apiError", {
12660
+ params: {
12661
+ service: "Upstash",
12662
+ status: String(response.status)
12663
+ }
12664
+ }));
11549
12665
  }
11550
12666
  return response.json();
11551
12667
  }
@@ -11562,7 +12678,9 @@ async function getUpstashDatabase(email, apiKey, databaseId) {
11562
12678
  function buildUpstashProvisionResult(database, resourceStatus) {
11563
12679
  const databaseId = database.database_id ?? "";
11564
12680
  if (!databaseId || !database.endpoint || !database.password || !database.port) {
11565
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
12681
+ throw new Error(deployM("deploy_provisionNoResult", {
12682
+ params: { service: "Upstash" }
12683
+ }));
11566
12684
  }
11567
12685
  return {
11568
12686
  serviceType: "cache",
@@ -11586,7 +12704,9 @@ function createUpstashProvisioner() {
11586
12704
  const userEmail = credentials.email ?? "";
11587
12705
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
11588
12706
  if (!userEmail || !userKey) {
11589
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "email, api_key" } }));
12707
+ throw new Error(deployM("deploy_credentialMissing", {
12708
+ params: { fields: "email, api_key" }
12709
+ }));
11590
12710
  }
11591
12711
  await listUpstashDatabases(userEmail, userKey);
11592
12712
  email = userEmail;
@@ -11598,7 +12718,9 @@ function createUpstashProvisioner() {
11598
12718
  return err(
11599
12719
  HaiDeployError.AUTH_FAILED,
11600
12720
  deployM("deploy_authFailed", {
11601
- params: { error: error instanceof Error ? error.message : String(error) }
12721
+ params: {
12722
+ error: error instanceof Error ? error.message : String(error)
12723
+ }
11602
12724
  }),
11603
12725
  error
11604
12726
  );
@@ -11619,12 +12741,17 @@ function createUpstashProvisioner() {
11619
12741
  if (existingDatabase !== null) {
11620
12742
  const databaseId = existingDatabase.database_id ?? "";
11621
12743
  if (!databaseId) {
11622
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
12744
+ throw new Error(deployM("deploy_provisionNoResult", {
12745
+ params: { service: "Upstash" }
12746
+ }));
11623
12747
  }
11624
12748
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
11625
12749
  const detail = await getUpstashDatabase(email, apiKey, databaseId);
11626
12750
  logger11.info("Upstash Redis reused", { databaseId, databaseName });
11627
- return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
12751
+ return ok(buildUpstashProvisionResult({
12752
+ ...existingDatabase,
12753
+ ...detail
12754
+ }, "reused"));
11628
12755
  }
11629
12756
  const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
11630
12757
  method: "POST",
@@ -11644,7 +12771,9 @@ function createUpstashProvisioner() {
11644
12771
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
11645
12772
  projectName: appName,
11646
12773
  stage: "provision:cache",
11647
- resources: [{ serviceType: "cache", provisionerName: "upstash", ...resource }]
12774
+ resources: [
12775
+ { serviceType: "cache", provisionerName: "upstash", ...resource }
12776
+ ]
11648
12777
  });
11649
12778
  }
11650
12779
  return err(
@@ -11678,7 +12807,9 @@ function createProviderByType(type) {
11678
12807
  case "vercel":
11679
12808
  return createVercelProvider();
11680
12809
  default:
11681
- throw new Error(deployM("deploy_unsupportedProviderType", { params: { type } }));
12810
+ throw new Error(deployM("deploy_unsupportedProviderType", {
12811
+ params: { type }
12812
+ }));
11682
12813
  }
11683
12814
  }
11684
12815
  function createProvisionerByName(provisioner) {
@@ -11694,7 +12825,9 @@ function createProvisionerByName(provisioner) {
11694
12825
  case "aliyun":
11695
12826
  return createAliyunProvisioner();
11696
12827
  default:
11697
- throw new Error(deployM("deploy_unsupportedProvisionerType", { params: { type: provisioner } }));
12828
+ throw new Error(deployM("deploy_unsupportedProvisionerType", {
12829
+ params: { type: provisioner }
12830
+ }));
11698
12831
  }
11699
12832
  }
11700
12833
  function extractCredentials(serviceConfig) {
@@ -11727,8 +12860,12 @@ function mergeProvisionEnv(results, scan, options) {
11727
12860
  return item.service === prov.serviceType && (!mode || mode === "default");
11728
12861
  });
11729
12862
  if (targets.length) {
11730
- for (const target of targets)
11731
- reachProviders[reachRequirements.indexOf(target)] = { ...mergeManagedMiddlewareConfig(target, provider), name: target.config.name ?? provider.name };
12863
+ for (const target of targets) {
12864
+ reachProviders[reachRequirements.indexOf(target)] = {
12865
+ ...mergeManagedMiddlewareConfig(target, provider),
12866
+ name: target.config.name ?? provider.name
12867
+ };
12868
+ }
11732
12869
  } else {
11733
12870
  reachProviders.push(provider);
11734
12871
  }
@@ -11750,7 +12887,9 @@ var deploy = {
11750
12887
  logger12.warn("Deploy init already in progress, skipping concurrent call");
11751
12888
  return err(
11752
12889
  HaiDeployError.CONFIG_ERROR,
11753
- deployM("deploy_configError", { params: { error: "init already in progress" } })
12890
+ deployM("deploy_configError", {
12891
+ params: { error: "init already in progress" }
12892
+ })
11754
12893
  );
11755
12894
  }
11756
12895
  initInProgress = true;
@@ -11765,7 +12904,9 @@ var deploy = {
11765
12904
  logger12.error("Deploy config validation failed", { error: parseResult.error.message });
11766
12905
  return err(
11767
12906
  HaiDeployError.CONFIG_ERROR,
11768
- deployM("deploy_configError", { params: { error: parseResult.error.message } }),
12907
+ deployM("deploy_configError", {
12908
+ params: { error: parseResult.error.message }
12909
+ }),
11769
12910
  parseResult.error
11770
12911
  );
11771
12912
  }
@@ -11813,7 +12954,9 @@ var deploy = {
11813
12954
  currentConfig = parsed;
11814
12955
  logger12.info("Deploy module initialized", {
11815
12956
  provider: parsed.provider.type,
11816
- provisioners: [...currentProvisioners.keys()]
12957
+ provisioners: [
12958
+ ...currentProvisioners.keys()
12959
+ ]
11817
12960
  });
11818
12961
  return ok(void 0);
11819
12962
  } catch (error) {
@@ -11825,7 +12968,9 @@ var deploy = {
11825
12968
  return err(
11826
12969
  HaiDeployError.CONFIG_ERROR,
11827
12970
  deployM("deploy_configError", {
11828
- params: { error: error instanceof Error ? error.message : String(error) }
12971
+ params: {
12972
+ error: error instanceof Error ? error.message : String(error)
12973
+ }
11829
12974
  }),
11830
12975
  error
11831
12976
  );
@@ -11879,7 +13024,12 @@ var deploy = {
11879
13024
  resources: [
11880
13025
  ...results.map(({ envVars: _envVars, ...resource }) => resource),
11881
13026
  ...getDeployRecovery(provResult.error)?.resources ?? [
11882
- { serviceType, provisionerName: provisioner.name, resourceInfo: projectName, resourceStatus: "unknown" }
13027
+ {
13028
+ serviceType,
13029
+ provisionerName: provisioner.name,
13030
+ resourceInfo: projectName,
13031
+ resourceStatus: "unknown"
13032
+ }
11883
13033
  ]
11884
13034
  ]
11885
13035
  });
@@ -11917,7 +13067,9 @@ var deploy = {
11917
13067
  connectionVariables.add(match[1]);
11918
13068
  }
11919
13069
  } catch {
11920
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path: projectFile, line: "1" } }));
13070
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
13071
+ params: { path: projectFile, line: "1" }
13072
+ }));
11921
13073
  }
11922
13074
  }
11923
13075
  const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => key !== "HAI_DEPLOY" && !connectionVariables.has(key) && !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
@@ -11925,7 +13077,9 @@ var deploy = {
11925
13077
  options = {
11926
13078
  ...options,
11927
13079
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
11928
- ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
13080
+ ...options?.skipMiddleware ? {
13081
+ serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
13082
+ } : {}
11929
13083
  };
11930
13084
  if (options.selectedServices) {
11931
13085
  options.serviceModes = {
@@ -11961,7 +13115,10 @@ var deploy = {
11961
13115
  return err(provResults.error);
11962
13116
  }
11963
13117
  recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
11964
- allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
13118
+ allEnvVars = {
13119
+ ...allEnvVars,
13120
+ ...mergeProvisionEnv(provResults.data, scan, options)
13121
+ };
11965
13122
  if (options?.environment) {
11966
13123
  const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
11967
13124
  if (!saved.success)
@@ -12037,7 +13194,16 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
12037
13194
  }
12038
13195
  provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
12039
13196
  }
12040
- return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
13197
+ return provider.deploy({
13198
+ appDir,
13199
+ artifact,
13200
+ artifactDir,
13201
+ projectName,
13202
+ scan,
13203
+ provisionEnv,
13204
+ appEnv,
13205
+ options
13206
+ });
12041
13207
  }
12042
13208
  z.enum(["sqlite", "postgresql", "mysql"]);
12043
13209
  var ReldbOperationLogConfigSchema = z.object({
@@ -12070,7 +13236,12 @@ var MysqlPoolConfigSchema = z.object({
12070
13236
  }).strict();
12071
13237
  var SslConfigSchema = z.union([
12072
13238
  z.boolean(),
12073
- z.enum(["require", "prefer", "allow", "disable"]),
13239
+ z.enum([
13240
+ "require",
13241
+ "prefer",
13242
+ "allow",
13243
+ "disable"
13244
+ ]),
12074
13245
  z.record(z.string(), z.unknown())
12075
13246
  ]);
12076
13247
  var MysqlSslConfigSchema = z.union([
@@ -12086,7 +13257,9 @@ var MysqlSslConfigSchema = z.union([
12086
13257
  cert: z.string().min(1).optional(),
12087
13258
  /** MySQL 客户端私钥,PEM 内容;必须同时提供 cert */
12088
13259
  key: z.string().min(1).optional()
12089
- }).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), { path: ["key"] })
13260
+ }).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), {
13261
+ path: ["key"]
13262
+ })
12090
13263
  ]);
12091
13264
  var SqliteOptionsSchema = z.object({
12092
13265
  /** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
@@ -12148,7 +13321,10 @@ var PostgresqlConfigSchema = NetworkConfigSchema.extend({
12148
13321
  var MysqlConfigSchema = NetworkConfigSchema.extend({
12149
13322
  type: z.literal("mysql"),
12150
13323
  /** 网络数据库URL;MySQL空串表示使用分字段,非空仅mysql://且拒绝query/fragment,优先于host/port/database/user/password */
12151
- url: z.union([z.literal("").transform(() => void 0), z.url({ protocol: /^mysql$/ }).refine((value) => !value.includes("?") && !value.includes("#"))]).optional(),
13324
+ url: z.union([
13325
+ z.literal("").transform(() => void 0),
13326
+ z.url({ protocol: /^mysql$/ }).refine((value) => !value.includes("?") && !value.includes("#"))
13327
+ ]).optional(),
12152
13328
  /** 数据库:SQLite文件路径或:memory:;PostgreSQL必填名称;MySQL无URL时必填,URL模式从URL读取 */
12153
13329
  database: z.string().min(1).optional(),
12154
13330
  /** TLS:PostgreSQL使用其模式;MySQL仅false或严格对象,空对象默认校验证书链和DNS身份 */
@@ -12158,7 +13334,9 @@ var MysqlConfigSchema = NetworkConfigSchema.extend({
12158
13334
  /** MySQL 特定选项 */
12159
13335
  mysql: MysqlOptionsSchema.optional(),
12160
13336
  ...OperationLogConfigFieldSchema
12161
- }).refine((config) => Boolean(config.url) || Boolean(config.database), { path: ["database"] });
13337
+ }).refine((config) => Boolean(config.url) || Boolean(config.database), {
13338
+ path: ["database"]
13339
+ });
12162
13340
  var ReldbConfigSchema = z.discriminatedUnion("type", [
12163
13341
  SqliteConfigSchema,
12164
13342
  PostgresqlConfigSchema,
@@ -12168,8 +13346,12 @@ function canKeepMiddleware(requirement, backend, environment) {
12168
13346
  const privatePath = join(environment.envDir, `.env.${environment.environment}.local`);
12169
13347
  let targetValues = environment.targetValues;
12170
13348
  try {
12171
- if (existsSync(privatePath))
12172
- targetValues = core.config.mergeEnv([targetValues, core.config.parseEnv(readFileSync(privatePath, "utf8"))]);
13349
+ if (existsSync(privatePath)) {
13350
+ targetValues = core.config.mergeEnv([
13351
+ targetValues,
13352
+ core.config.parseEnv(readLocalFile(privatePath).toString("utf8"))
13353
+ ]);
13354
+ }
12173
13355
  } catch {
12174
13356
  return false;
12175
13357
  }
@@ -12179,19 +13361,31 @@ function canKeepMiddleware(requirement, backend, environment) {
12179
13361
  function empty(value2) {
12180
13362
  if (Array.isArray(value2))
12181
13363
  return value2.map(empty);
12182
- if (value2 && typeof value2 === "object")
12183
- return Object.fromEntries(Object.entries(value2).map(([name, child]) => [name, empty(child)]));
13364
+ if (value2 && typeof value2 === "object") {
13365
+ return Object.fromEntries(Object.entries(value2).map(([name, child]) => [
13366
+ name,
13367
+ empty(child)
13368
+ ]));
13369
+ }
12184
13370
  return void 0;
12185
13371
  }
12186
13372
  let seed = empty(requirement.config);
12187
- for (const part of [...requirement.configPath].reverse())
13373
+ for (const part of [...requirement.configPath].reverse()) {
12188
13374
  seed = /^\d+$/.test(part) ? Array.from({ length: Number(part) + 1 }, (_, index) => index === Number(part) ? seed : null) : { [part]: seed };
12189
- const resolved = core.config.resolve({ name: requirement.configName, value: seed, environment: targetValues, shape: structures[requirement.configName], missing: "empty" });
13375
+ }
13376
+ const resolved = core.config.resolve({
13377
+ name: requirement.configName,
13378
+ value: seed,
13379
+ environment: targetValues,
13380
+ shape: structures[requirement.configName],
13381
+ missing: "empty"
13382
+ });
12190
13383
  if (!resolved.success)
12191
13384
  return false;
12192
13385
  let value = resolved.data;
12193
- for (const part of requirement.configPath)
13386
+ for (const part of requirement.configPath) {
12194
13387
  value = value && typeof value === "object" ? value[part] : void 0;
13388
+ }
12195
13389
  if (!value || typeof value !== "object" || Array.isArray(value))
12196
13390
  return false;
12197
13391
  const target = value;
@@ -12208,30 +13402,39 @@ function validateMiddlewareField(options) {
12208
13402
  }
12209
13403
  function middlewareFieldProblem(options) {
12210
13404
  const { value, field: field2, backend } = options;
12211
- if (value === "")
13405
+ if (value === "") {
12212
13406
  return field2.required ? deployM("deploy_middlewareRequired") : void 0;
12213
- if (field2.kind === "port")
12214
- return /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535 ? void 0 : deployM("deploy_middlewareInteger", { params: { min: 1, max: 65535 } });
13407
+ }
13408
+ if (field2.kind === "port") {
13409
+ return /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535 ? void 0 : deployM("deploy_middlewareInteger", {
13410
+ params: { min: 1, max: 65535 }
13411
+ });
13412
+ }
12215
13413
  if (field2.kind === "number") {
12216
13414
  const min = field2.minimum ?? 0;
12217
13415
  const max = field2.maximum ?? Number.MAX_SAFE_INTEGER;
12218
- return /^\d+$/.test(value) && Number(value) >= min && Number(value) <= max ? void 0 : deployM("deploy_middlewareInteger", { params: { min, max } });
13416
+ return /^\d+$/.test(value) && Number(value) >= min && Number(value) <= max ? void 0 : deployM("deploy_middlewareInteger", {
13417
+ params: { min, max }
13418
+ });
12219
13419
  }
12220
- if (field2.kind === "boolean")
13420
+ if (field2.kind === "boolean") {
12221
13421
  return ["true", "false"].includes(value) ? void 0 : deployM("deploy_middlewareBoolean");
13422
+ }
12222
13423
  if (backend === "s3" && field2.name === "endpoint") {
12223
13424
  const endpoint = URL.parse(value);
12224
13425
  return endpoint?.hostname && ["http:", "https:"].includes(endpoint.protocol) ? void 0 : deployM("deploy_middlewareS3Endpoint");
12225
13426
  }
12226
- if (field2.kind === "url")
13427
+ if (field2.kind === "url") {
12227
13428
  return URL.canParse(value) ? void 0 : deployM("deploy_middlewareUrl");
13429
+ }
12228
13430
  if (field2.name === "ssl") {
12229
13431
  try {
12230
13432
  const document = parseDocument(value);
12231
13433
  if (document.errors.length)
12232
13434
  return deployM("deploy_middlewareTlsSyntax");
12233
- if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success)
13435
+ if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success) {
12234
13436
  return deployM(backend === "mysql" ? "deploy_middlewareMysqlTls" : "deploy_middlewarePostgresTls");
13437
+ }
12235
13438
  } catch {
12236
13439
  return deployM("deploy_middlewareTlsSyntax");
12237
13440
  }
@@ -12253,8 +13456,11 @@ function parseMiddlewareField(options) {
12253
13456
  if (options.managed && (options.field.secret || options.field.managed) && options.value === "")
12254
13457
  return ok(void 0);
12255
13458
  const problem = middlewareFieldProblem(options);
12256
- if (problem)
12257
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldError", { params: { field: options.field.name, reason: problem } }));
13459
+ if (problem) {
13460
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldError", {
13461
+ params: { field: options.field.name, reason: problem }
13462
+ }));
13463
+ }
12258
13464
  const { value, field: field2 } = options;
12259
13465
  if (value === "")
12260
13466
  return ok(void 0);
@@ -12272,8 +13478,13 @@ function saveMiddlewareEnvironment(environment, options) {
12272
13478
  const configuration = requirement.service === "ldap" ? options.configuration : { ...options.configuration, type: backend };
12273
13479
  if (requirement.service === "db") {
12274
13480
  const validated = ReldbConfigSchema.safeParse(configuration);
12275
- if (!validated.success)
12276
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldInvalid", { params: { fields: validated.error.issues.map((issue) => `db.${issue.path.join(".")}`).join(", ") } }));
13481
+ if (!validated.success) {
13482
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldInvalid", {
13483
+ params: {
13484
+ fields: validated.error.issues.map((issue) => `db.${issue.path.join(".")}`).join(", ")
13485
+ }
13486
+ }));
13487
+ }
12277
13488
  }
12278
13489
  if (requirement.service === "ldap") {
12279
13490
  updates.HAI_IAM_LOGIN_LDAP = backend !== "disabled";
@@ -12287,17 +13498,26 @@ function saveMiddlewareEnvironment(environment, options) {
12287
13498
  mkdirSync(directory, { recursive: true });
12288
13499
  const path = join(directory, `_${requirement.configName}.yml`);
12289
13500
  const lineCounter = new LineCounter();
12290
- const document = parseDocument(existsSync(path) ? readFileSync(path, "utf8") : "{}\n", { lineCounter, prettyErrors: false });
12291
- if (document.errors.length)
12292
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
12293
- if (backend !== "disabled" && requirement.configPath.length && !document.hasIn(requirement.configPath))
13501
+ const document = parseDocument(existsSync(path) ? readLocalFile(path).toString("utf8") : "{}\n", { lineCounter, prettyErrors: false });
13502
+ if (document.errors.length) {
13503
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
13504
+ params: {
13505
+ path,
13506
+ line: String(lineCounter.linePos(document.errors[0].pos[0]).line)
13507
+ }
13508
+ }));
13509
+ }
13510
+ if (backend !== "disabled" && requirement.configPath.length && !document.hasIn(requirement.configPath)) {
12294
13511
  document.setIn(requirement.configPath, document.createNode(requirement.config.name ? { name: requirement.config.name, type: backend } : {}));
13512
+ }
12295
13513
  if (requirement.service === "ldap" && !document.hasIn(["login", "ldap"]))
12296
13514
  document.setIn(["login", "ldap"], false);
12297
13515
  const content = document.toString();
12298
- if (!existsSync(path) || readFileSync(path, "utf8") !== content)
12299
- writeFileSync(path, content);
12300
- return saveDeploymentEnvironment(environment, updates, { replace: options.configuration && backend !== "disabled" ? [requirement.envPrefix] : [] });
13516
+ if (!existsSync(path) || readLocalFile(path).toString("utf8") !== content)
13517
+ writeLocalFile(path, content);
13518
+ return saveDeploymentEnvironment(environment, updates, {
13519
+ replace: options.configuration && backend !== "disabled" ? [requirement.envPrefix] : []
13520
+ });
12301
13521
  } catch {
12302
13522
  return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
12303
13523
  }
@@ -12305,46 +13525,253 @@ function saveMiddlewareEnvironment(environment, options) {
12305
13525
 
12306
13526
  // src/deploy-middleware.ts
12307
13527
  var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
12308
- var required = (name, defaultValue, kind) => ({ ...field(name, defaultValue, kind), required: true });
13528
+ function required(name, defaultValue, kind) {
13529
+ return {
13530
+ ...field(name, defaultValue, kind),
13531
+ required: true
13532
+ };
13533
+ }
12309
13534
  var DEPLOY_MIDDLEWARE = {
12310
13535
  db: [
12311
- { backend: "sqlite", label: "SQLite", purpose: "deploy_purposeSqlite", fields: [required("database", "./data/app.db")] },
13536
+ {
13537
+ backend: "sqlite",
13538
+ label: "SQLite",
13539
+ purpose: "deploy_purposeSqlite",
13540
+ fields: [
13541
+ required("database", "./data/app.db")
13542
+ ]
13543
+ },
12312
13544
  ...["postgresql", "mysql"].map((backend) => ({
12313
13545
  backend,
12314
13546
  label: backend === "mysql" ? "MySQL" : "PostgreSQL",
12315
13547
  purpose: "deploy_purposeDatabase",
12316
13548
  provisioner: backend === "postgresql" ? "neon" : void 0,
12317
13549
  cloudFields: backend === "postgresql" ? provisionerFields("neon") : void 0,
12318
- fields: [field("url", "", "url", true), required("host", "localhost"), required("port", backend === "mysql" ? "3306" : "5432", "port"), required("database", "hai"), field("user", "hai"), field("password", "", void 0, true), field("ssl", "false")]
13550
+ fields: [
13551
+ field("url", "", "url", true),
13552
+ required("host", "localhost"),
13553
+ required("port", backend === "mysql" ? "3306" : "5432", "port"),
13554
+ required("database", "hai"),
13555
+ field("user", "hai"),
13556
+ field("password", "", void 0, true),
13557
+ field("ssl", "false")
13558
+ ]
12319
13559
  }))
12320
13560
  ],
12321
13561
  cache: [
12322
- { backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
12323
- { backend: "redis", label: "Redis", purpose: "deploy_purposeCache", provisioner: "upstash", cloudFields: provisionerFields("upstash"), fields: [field("url", "", "url", true), required("host", "localhost"), required("port", "6379", "port"), field("password", "", void 0, true), { ...field("db", "0", "number"), maximum: 15 }, field("tls", "false", "boolean"), field("keyPrefix"), field("connectTimeout", "10000", "number"), field("commandTimeout", "5000", "number")] }
13562
+ {
13563
+ backend: "memory",
13564
+ label: "Memory",
13565
+ purpose: "deploy_purposeMemory",
13566
+ fields: []
13567
+ },
13568
+ {
13569
+ backend: "redis",
13570
+ label: "Redis",
13571
+ purpose: "deploy_purposeCache",
13572
+ provisioner: "upstash",
13573
+ cloudFields: provisionerFields("upstash"),
13574
+ fields: [
13575
+ field("url", "", "url", true),
13576
+ required("host", "localhost"),
13577
+ required("port", "6379", "port"),
13578
+ field("password", "", void 0, true),
13579
+ {
13580
+ ...field("db", "0", "number"),
13581
+ maximum: 15
13582
+ },
13583
+ field("tls", "false", "boolean"),
13584
+ field("keyPrefix"),
13585
+ field("connectTimeout", "10000", "number"),
13586
+ field("commandTimeout", "5000", "number")
13587
+ ]
13588
+ }
12324
13589
  ],
12325
13590
  storage: [
12326
- { backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required("root", "./data/storage")] },
12327
- { backend: "s3", label: "S3 / RustFS / MinIO / Cloudflare R2", purpose: "deploy_purposeStorage", provisioner: "cloudflare-r2", cloudFields: provisionerFields("cloudflare-r2"), containerOptions: [{ field: "s3Engine", variable: "HAI_DEPLOY_CONTAINER_S3ENGINE", purpose: "deploy_s3Engine", defaultValue: "rustfs", choices: [{ label: "RustFS", value: "rustfs" }, { label: "MinIO", value: "minio" }] }], fields: [field("endpoint", "", "url"), required("bucket", "hai"), required("region", "us-east-1"), { ...required("accessKeyId"), managed: true }, { ...required("secretAccessKey"), secret: true }, field("forcePathStyle", "true", "boolean")] }
13591
+ {
13592
+ backend: "local",
13593
+ label: "Local filesystem",
13594
+ purpose: "deploy_purposeLocal",
13595
+ fields: [
13596
+ required("root", "./data/storage")
13597
+ ]
13598
+ },
13599
+ {
13600
+ backend: "s3",
13601
+ label: "S3 / RustFS / MinIO / Cloudflare R2",
13602
+ purpose: "deploy_purposeStorage",
13603
+ provisioner: "cloudflare-r2",
13604
+ cloudFields: provisionerFields("cloudflare-r2"),
13605
+ containerOptions: [
13606
+ {
13607
+ field: "s3Engine",
13608
+ variable: "HAI_DEPLOY_CONTAINER_S3ENGINE",
13609
+ purpose: "deploy_s3Engine",
13610
+ defaultValue: "rustfs",
13611
+ choices: [
13612
+ { label: "RustFS", value: "rustfs" },
13613
+ { label: "MinIO", value: "minio" }
13614
+ ]
13615
+ }
13616
+ ],
13617
+ fields: [
13618
+ field("endpoint", "", "url"),
13619
+ required("bucket", "hai"),
13620
+ required("region", "us-east-1"),
13621
+ {
13622
+ ...required("accessKeyId"),
13623
+ managed: true
13624
+ },
13625
+ {
13626
+ ...required("secretAccessKey"),
13627
+ secret: true
13628
+ },
13629
+ field("forcePathStyle", "true", "boolean")
13630
+ ]
13631
+ }
12328
13632
  ],
12329
13633
  vecdb: [
12330
- { backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required("path", "./data/vectors")] },
12331
- { backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
12332
- { backend: "pgvector", label: "PostgreSQL / pgvector", purpose: "deploy_purposeVector", fields: [field("url", "", "url", true), required("host", "localhost"), required("port", "5432", "port"), required("database", "hai"), field("user", "hai"), field("password", "", void 0, true)] },
12333
- { backend: "chroma", label: "Chroma", purpose: "deploy_purposeVector", fields: [required("url", "http://localhost:8000", "url")] }
13634
+ {
13635
+ backend: "lancedb",
13636
+ label: "LanceDB",
13637
+ purpose: "deploy_purposeLocal",
13638
+ fields: [
13639
+ required("path", "./data/vectors")
13640
+ ]
13641
+ },
13642
+ {
13643
+ backend: "qdrant",
13644
+ label: "Qdrant",
13645
+ purpose: "deploy_purposeVector",
13646
+ fields: [
13647
+ required("url", "http://localhost:6333", "url"),
13648
+ field("apiKey", "", void 0, true)
13649
+ ]
13650
+ },
13651
+ {
13652
+ backend: "pgvector",
13653
+ label: "PostgreSQL / pgvector",
13654
+ purpose: "deploy_purposeVector",
13655
+ fields: [
13656
+ field("url", "", "url", true),
13657
+ required("host", "localhost"),
13658
+ required("port", "5432", "port"),
13659
+ required("database", "hai"),
13660
+ field("user", "hai"),
13661
+ field("password", "", void 0, true)
13662
+ ]
13663
+ },
13664
+ {
13665
+ backend: "chroma",
13666
+ label: "Chroma",
13667
+ purpose: "deploy_purposeVector",
13668
+ fields: [
13669
+ required("url", "http://localhost:8000", "url")
13670
+ ]
13671
+ }
12334
13672
  ],
12335
13673
  ldap: [
12336
- { backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
12337
- { backend: "ldap", label: "LDAP / OpenLDAP", purpose: "deploy_purposeLdap", fields: [required("url", "ldap://localhost:389", "url"), required("bindDn", "cn=admin,dc=example,dc=org"), { ...required("bindPassword"), secret: true }, required("searchBase", "dc=example,dc=org"), field("searchFilter", "(uid={{username}})"), field("usernameAttribute", "uid"), field("emailAttribute", "mail"), field("displayNameAttribute", "cn"), field("useTls", "false", "boolean"), { ...field("connectTimeout", "5000", "number"), minimum: 1e3 }] }
13674
+ {
13675
+ backend: "disabled",
13676
+ label: "Disabled",
13677
+ purpose: "deploy_purposeDisabled",
13678
+ fields: []
13679
+ },
13680
+ {
13681
+ backend: "ldap",
13682
+ label: "LDAP / OpenLDAP",
13683
+ purpose: "deploy_purposeLdap",
13684
+ fields: [
13685
+ required("url", "ldap://localhost:389", "url"),
13686
+ required("bindDn", "cn=admin,dc=example,dc=org"),
13687
+ {
13688
+ ...required("bindPassword"),
13689
+ secret: true
13690
+ },
13691
+ required("searchBase", "dc=example,dc=org"),
13692
+ field("searchFilter", "(uid={{username}})"),
13693
+ field("usernameAttribute", "uid"),
13694
+ field("emailAttribute", "mail"),
13695
+ field("displayNameAttribute", "cn"),
13696
+ field("useTls", "false", "boolean"),
13697
+ {
13698
+ ...field("connectTimeout", "5000", "number"),
13699
+ minimum: 1e3
13700
+ }
13701
+ ]
13702
+ }
12338
13703
  ],
12339
13704
  email: [
12340
- { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
12341
- { backend: "smtp", label: "SMTP", purpose: "deploy_purposeEmail", provisioner: "resend", cloudFields: provisionerFields("resend"), fields: [required("host", "localhost"), required("port", "465", "port"), field("secure", "true", "boolean"), field("user"), field("pass", "", void 0, true), required("from", "noreply@example.com")] },
12342
- { backend: "api", containerSupported: false, label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required("url", "", "url"), field("method", "POST"), field("headers", "", void 0, true), field("timeout", "10000", "number")] }
13705
+ {
13706
+ backend: "console",
13707
+ label: "Console",
13708
+ purpose: "deploy_purposeConsole",
13709
+ fields: []
13710
+ },
13711
+ {
13712
+ backend: "smtp",
13713
+ label: "SMTP",
13714
+ purpose: "deploy_purposeEmail",
13715
+ provisioner: "resend",
13716
+ cloudFields: provisionerFields("resend"),
13717
+ fields: [
13718
+ required("host", "localhost"),
13719
+ required("port", "465", "port"),
13720
+ field("secure", "true", "boolean"),
13721
+ field("user"),
13722
+ field("pass", "", void 0, true),
13723
+ required("from", "noreply@example.com")
13724
+ ]
13725
+ },
13726
+ {
13727
+ backend: "api",
13728
+ containerSupported: false,
13729
+ label: "HTTP notification API",
13730
+ purpose: "deploy_purposeEmail",
13731
+ fields: [
13732
+ required("url", "", "url"),
13733
+ field("method", "POST"),
13734
+ field("headers", "", void 0, true),
13735
+ field("timeout", "10000", "number")
13736
+ ]
13737
+ }
12343
13738
  ],
12344
13739
  sms: [
12345
- { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
12346
- { backend: "aliyun-sms", containerSupported: false, label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", cloudFields: provisionerFields("aliyun"), fields: [required("accessKeyId"), { ...required("accessKeySecret"), secret: true }, required("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] },
12347
- { backend: "api", containerSupported: false, label: "HTTP notification API", purpose: "deploy_purposeSms", fields: [required("url", "", "url"), field("method", "POST"), field("headers", "", void 0, true), field("timeout", "10000", "number")] }
13740
+ {
13741
+ backend: "console",
13742
+ label: "Console",
13743
+ purpose: "deploy_purposeConsole",
13744
+ fields: []
13745
+ },
13746
+ {
13747
+ backend: "aliyun-sms",
13748
+ containerSupported: false,
13749
+ label: "Aliyun SMS",
13750
+ purpose: "deploy_purposeSms",
13751
+ provisioner: "aliyun",
13752
+ cloudFields: provisionerFields("aliyun"),
13753
+ fields: [
13754
+ required("accessKeyId"),
13755
+ {
13756
+ ...required("accessKeySecret"),
13757
+ secret: true
13758
+ },
13759
+ required("signName"),
13760
+ field("endpoint", "dysmsapi.aliyuncs.com")
13761
+ ]
13762
+ },
13763
+ {
13764
+ backend: "api",
13765
+ containerSupported: false,
13766
+ label: "HTTP notification API",
13767
+ purpose: "deploy_purposeSms",
13768
+ fields: [
13769
+ required("url", "", "url"),
13770
+ field("method", "POST"),
13771
+ field("headers", "", void 0, true),
13772
+ field("timeout", "10000", "number")
13773
+ ]
13774
+ }
12348
13775
  ]
12349
13776
  };
12350
13777
  function describeMiddlewareBackend(backend) {
@@ -12354,8 +13781,15 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12354
13781
  if ("projectName" in options && options.projectName !== void 0)
12355
13782
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
12356
13783
  const parsed = DeployConfigSchema.safeParse(input);
12357
- if (!parsed.success)
12358
- return err({ ...HaiDeployError.CONFIG_ERROR, message: deployM("deploy_artifactInvalid"), ext: { fields: parsed.error.issues.map((issue) => issue.path.join(".")) } });
13784
+ if (!parsed.success) {
13785
+ return err({
13786
+ ...HaiDeployError.CONFIG_ERROR,
13787
+ message: deployM("deploy_artifactInvalid"),
13788
+ ext: {
13789
+ fields: parsed.error.issues.map((issue) => issue.path.join("."))
13790
+ }
13791
+ });
13792
+ }
12359
13793
  const config = parsed.data;
12360
13794
  const scan = await scanApp(appDir, { environment: options.environment ?? config.environment, envDir: options.envDir });
12361
13795
  if (!scan.success)
@@ -12382,21 +13816,51 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12382
13816
  const compose = validateComposeServices(config.container, artifact);
12383
13817
  if (!compose.success)
12384
13818
  return compose;
12385
- images = [.../* @__PURE__ */ new Set([artifact.primaryService, ...artifact.hasWeb ? ["web"] : []])];
13819
+ images = [
13820
+ .../* @__PURE__ */ new Set([
13821
+ artifact.primaryService,
13822
+ ...artifact.hasWeb ? ["web"] : []
13823
+ ])
13824
+ ];
12386
13825
  } else {
12387
13826
  const file = join(root, "hai.json");
12388
- const project = existsSync(file) ? z.object({ build: z.object({ images: z.array(z.object({ name: DeploymentArtifactSchema.shape.primaryService })).min(1).max(2).refine((items) => new Set(items.map((item) => item.name)).size === items.length).optional() }).optional() }).parse(JSON.parse(readFileSync(file, "utf8"))) : void 0;
12389
- images = project?.build?.images?.map((image) => image.name) ?? [app.isSvelteKit ? "web" : "serv"];
12390
- }
12391
- const middlewares = (app.middlewares ?? []).filter((item) => !["sqlite", "local", "memory", "disabled", "console", "lancedb"].includes(item.backend));
12392
- const available = [.../* @__PURE__ */ new Set([...images, ...app.requiredServices])];
12393
- const selected = [...options.selectedServices ?? (artifact?.partial ? artifact.images.map((image) => image.name) : available)];
13827
+ const project = existsSync(file) ? z.object({
13828
+ build: z.object({
13829
+ images: z.array(z.object({ name: DeploymentArtifactSchema.shape.primaryService })).min(1).max(2).refine((items) => new Set(items.map((item) => item.name)).size === items.length).optional()
13830
+ }).optional()
13831
+ }).parse(JSON.parse(readFileSync(file, "utf8"))) : void 0;
13832
+ images = project?.build?.images?.map((image) => image.name) ?? [
13833
+ app.isSvelteKit ? "web" : "serv"
13834
+ ];
13835
+ }
13836
+ const middlewares = (app.middlewares ?? []).filter((item) => ![
13837
+ "sqlite",
13838
+ "local",
13839
+ "memory",
13840
+ "disabled",
13841
+ "console",
13842
+ "lancedb"
13843
+ ].includes(item.backend));
13844
+ const available = [
13845
+ .../* @__PURE__ */ new Set([
13846
+ ...images,
13847
+ ...app.requiredServices
13848
+ ])
13849
+ ];
13850
+ const selected = [
13851
+ ...options.selectedServices ?? (artifact?.partial ? artifact.images.map((image) => image.name) : available)
13852
+ ];
12394
13853
  if (!selected.length || new Set(selected).size !== selected.length || selected.some((name) => !available.includes(name)))
12395
13854
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
12396
13855
  if (artifact && selected.some((name) => images.includes(name) && !artifact.images.some((image) => image.name === name)))
12397
13856
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
12398
13857
  const remoteDirectory = provider.type === "docker-ssh" ? `${provider.remote.baseDir}/${projectName}` : void 0;
12399
- const baseline = { required: Boolean(artifact?.partial || options.selectedServices), state: "not-inspected", current: remoteDirectory ? `${remoteDirectory}/current` : void 0, release: void 0 };
13858
+ const baseline = {
13859
+ required: Boolean(artifact?.partial || options.selectedServices),
13860
+ state: "not-inspected",
13861
+ current: remoteDirectory ? `${remoteDirectory}/current` : void 0,
13862
+ release: void 0
13863
+ };
12400
13864
  if (options.inspectRemote) {
12401
13865
  if (provider.type !== "docker-ssh" || !provider.ssh || !remoteDirectory)
12402
13866
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
@@ -12412,13 +13876,27 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12412
13876
  baseline.release = release || void 0;
12413
13877
  }
12414
13878
  }
12415
- const configFiles = [".env", ".env.local", `.env.${environment.data.environment}`, `.env.${environment.data.environment}.local`].map((name) => join(environment.data.envDir, name)).filter(existsSync);
13879
+ const configFiles = [
13880
+ ".env",
13881
+ ".env.local",
13882
+ `.env.${environment.data.environment}`,
13883
+ `.env.${environment.data.environment}.local`
13884
+ ].map((name) => join(environment.data.envDir, name)).filter(existsSync);
12416
13885
  const configDirectory = join(directory, "config");
12417
13886
  if (existsSync(configDirectory))
12418
13887
  configFiles.push(...readdirSync(configDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)).map((entry) => join(configDirectory, entry.name)).sort());
12419
- const serviceModes = { ...options.skipMiddleware ? {} : options.serviceModes, ...Object.fromEntries(app.requiredServices.filter((service) => options.skipMiddleware || !selected.includes(service)).map((service) => [service, "keep"])) };
13888
+ const serviceModes = {
13889
+ ...options.skipMiddleware ? {} : options.serviceModes,
13890
+ ...Object.fromEntries(app.requiredServices.filter((service) => options.skipMiddleware || !selected.includes(service)).map((service) => [service, "keep"]))
13891
+ };
12420
13892
  const effective = resolveSidecarOptions(app, { ...options, selectedServices: selected, serviceModes });
12421
- const provisionKeys = { db: "HAI_DB", cache: "HAI_CACHE", storage: "HAI_STORAGE", email: "HAI_REACH_PROVIDERS", sms: "HAI_REACH_PROVIDERS" };
13893
+ const provisionKeys = {
13894
+ db: "HAI_DB",
13895
+ cache: "HAI_CACHE",
13896
+ storage: "HAI_STORAGE",
13897
+ email: "HAI_REACH_PROVIDERS",
13898
+ sms: "HAI_REACH_PROVIDERS"
13899
+ };
12422
13900
  const provisionEnv = {};
12423
13901
  const provisioners = /* @__PURE__ */ new Map();
12424
13902
  for (const service of Object.keys(provisionKeys)) {
@@ -12430,7 +13908,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12430
13908
  }
12431
13909
  const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
12432
13910
  const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
12433
- const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (sidecar?.services.includes("storage") || !!https && (artifact.primaryService === "serv" || app.isSvelteKit));
13911
+ const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (app.isSvelteKit || sidecar?.services.includes("storage") || !!https && artifact.primaryService === "serv");
12434
13912
  if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
12435
13913
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
12436
13914
  if (gateway) {
@@ -12449,17 +13927,54 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12449
13927
  root,
12450
13928
  serviceDir: directory,
12451
13929
  environment: environment.data.environment,
12452
- target: provider.type === "docker-ssh" ? { provider: provider.type, host: provider.ssh?.host, port: provider.ssh?.port, directory: remoteDirectory, runtime: provider.remote.runtime, hostPort: provider.expose.hostPort, https: https ? { type: https.type, domain: https.domain, port: https.port } : void 0 } : { provider: provider.type },
12453
- artifact: artifact ? { directory: artifactDir, version: artifact.appVersion, buildId: artifact.buildId, verified: true, images: artifact.images.map((image) => ({ name: image.name, reference: image.imageRef, sha256: image.sha256 })) } : void 0,
13930
+ target: provider.type === "docker-ssh" ? {
13931
+ provider: provider.type,
13932
+ host: provider.ssh?.host,
13933
+ port: provider.ssh?.port,
13934
+ directory: remoteDirectory,
13935
+ runtime: provider.remote.runtime,
13936
+ hostPort: provider.expose.hostPort,
13937
+ https: https ? { type: https.type, domain: https.domain, port: https.port } : void 0
13938
+ } : { provider: provider.type },
13939
+ artifact: artifact ? {
13940
+ directory: artifactDir,
13941
+ version: artifact.appVersion,
13942
+ buildId: artifact.buildId,
13943
+ verified: true,
13944
+ images: artifact.images.map((image) => ({ name: image.name, reference: image.imageRef, sha256: image.sha256 }))
13945
+ } : void 0,
12454
13946
  selected,
12455
13947
  retained: available.filter((name) => !selected.includes(name)),
12456
- containers: provider.type === "docker-ssh" ? [...selected.filter((name) => images.includes(name) || name === "gateway").map((name) => `hai-${projectName}-${name}`), ...services.flatMap(([, service]) => typeof service.container_name === "string" ? [service.container_name] : [])] : [],
12457
- composeManagedServices: [...services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name), ...selected.includes("certbot") ? ["certbot"] : []],
13948
+ containers: provider.type === "docker-ssh" ? [
13949
+ ...selected.filter((name) => images.includes(name) || name === "gateway").map((name) => `hai-${projectName}-${name}`),
13950
+ ...services.flatMap(([, service]) => typeof service.container_name === "string" ? [service.container_name] : [])
13951
+ ] : [],
13952
+ composeManagedServices: [
13953
+ ...services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name),
13954
+ ...selected.includes("certbot") ? ["certbot"] : []
13955
+ ],
12458
13956
  logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
12459
- middleware: middlewares.map((item) => ({ service: item.service, backend: item.backend, configuration: item.envPrefix, action: selected.includes(item.service) ? effective.serviceModes?.[item.envPrefix] ?? effective.serviceModes?.[item.service] ?? "default" : "keep", provisioner: provisioners.get(item.service) })),
12460
- configuration: { files: [join(root, "hai.json"), ...configFiles].filter(existsSync), environmentKeys: Object.keys(environment.data.values).sort() },
13957
+ middleware: middlewares.map((item) => ({
13958
+ service: item.service,
13959
+ backend: item.backend,
13960
+ configuration: item.envPrefix,
13961
+ action: selected.includes(item.service) ? effective.serviceModes?.[item.envPrefix] ?? effective.serviceModes?.[item.service] ?? "default" : "keep",
13962
+ provisioner: provisioners.get(item.service)
13963
+ })),
13964
+ configuration: {
13965
+ files: [
13966
+ join(root, "hai.json"),
13967
+ ...configFiles
13968
+ ].filter(existsSync),
13969
+ environmentKeys: Object.keys(environment.data.values).sort()
13970
+ },
12461
13971
  baseline,
12462
- recovery: { releases: remoteDirectory ? `${remoteDirectory}/releases` : void 0, point: baseline.release ?? "previous-successful-release", preservesVolumes: true, databaseRollback: false }
13972
+ recovery: {
13973
+ releases: remoteDirectory ? `${remoteDirectory}/releases` : void 0,
13974
+ point: baseline.release ?? "previous-successful-release",
13975
+ preservesVolumes: true,
13976
+ databaseRollback: false
13977
+ }
12463
13978
  });
12464
13979
  } catch {
12465
13980
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
@@ -12470,7 +13985,11 @@ var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(v
12470
13985
  var importSchema = z.strictObject({
12471
13986
  version: z.literal(1),
12472
13987
  projectName: nameSchema,
12473
- credentials: z.strictObject({ DB_PASSWORD: passwordSchema.optional(), STORAGE_PASSWORD: passwordSchema.optional(), LDAP_PASSWORD: passwordSchema.optional() }),
13988
+ credentials: z.strictObject({
13989
+ DB_PASSWORD: passwordSchema.optional(),
13990
+ STORAGE_PASSWORD: passwordSchema.optional(),
13991
+ LDAP_PASSWORD: passwordSchema.optional()
13992
+ }),
12474
13993
  services: z.array(z.strictObject({
12475
13994
  service: z.enum(["db", "vecdb", "storage", "ldap"]),
12476
13995
  container: nameSchema,
@@ -12484,7 +14003,12 @@ var importSchema = z.strictObject({
12484
14003
  bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
12485
14004
  })).min(1).max(4)
12486
14005
  });
12487
- var groups2 = { db: "DB_PASSWORD", vecdb: "DB_PASSWORD", storage: "STORAGE_PASSWORD", ldap: "LDAP_PASSWORD" };
14006
+ var groups2 = {
14007
+ db: "DB_PASSWORD",
14008
+ vecdb: "DB_PASSWORD",
14009
+ storage: "STORAGE_PASSWORD",
14010
+ ldap: "LDAP_PASSWORD"
14011
+ };
12488
14012
  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;
12489
14013
  try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
12490
14014
  if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
@@ -12516,11 +14040,24 @@ async function importDeploymentSecrets(options) {
12516
14040
  throw new Error("input");
12517
14041
  const input = privateFile(resolve(options.inputFile));
12518
14042
  const parsed = importSchema.safeParse(JSON.parse(input));
12519
- if (!parsed.success)
12520
- return err({ ...HaiDeployError.CONFIG_ERROR, message: deployM("deploy_secretImportFailed", { params: { phase } }), ext: { phase, state: "unchanged", fields: parsed.error.issues.map((issue) => issue.path.join(".")) } });
14043
+ if (!parsed.success) {
14044
+ return err({
14045
+ ...HaiDeployError.CONFIG_ERROR,
14046
+ message: deployM("deploy_secretImportFailed", {
14047
+ params: { phase }
14048
+ }),
14049
+ ext: {
14050
+ phase,
14051
+ state: "unchanged",
14052
+ fields: parsed.error.issues.map((issue) => issue.path.join("."))
14053
+ }
14054
+ });
14055
+ }
12521
14056
  const config = parsed.data;
12522
14057
  const project = `hai-${config.projectName}`;
12523
- const selectedGroups = [...new Set(config.services.map((service) => groups2[service.service]))].sort();
14058
+ const selectedGroups = [
14059
+ ...new Set(config.services.map((service) => groups2[service.service]))
14060
+ ].sort();
12524
14061
  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])) {
12525
14062
  throw new Error("input");
12526
14063
  }
@@ -12560,23 +14097,89 @@ async function importDeploymentSecrets(options) {
12560
14097
  const application = await inspectContainer(options.runtime, helper2, secrets);
12561
14098
  if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
12562
14099
  throw new Error("identity");
12563
- const result = await runCommand(options.runtime, ["exec", "-i", helper2, "node", "-e", s3VerificationScript], { input: JSON.stringify({ endpoint: `http://${service.container}:9000`, bucket: service.bucket, accessKeyId: service.accessKeyId, secret: password, region: service.region }), secrets, timeoutMs: 15e3, maxCaptureChars: 1024 });
14100
+ const result = await runCommand(options.runtime, [
14101
+ "exec",
14102
+ "-i",
14103
+ helper2,
14104
+ "node",
14105
+ "-e",
14106
+ s3VerificationScript
14107
+ ], {
14108
+ input: JSON.stringify({
14109
+ endpoint: `http://${service.container}:9000`,
14110
+ bucket: service.bucket,
14111
+ accessKeyId: service.accessKeyId,
14112
+ secret: password,
14113
+ region: service.region
14114
+ }),
14115
+ secrets,
14116
+ timeoutMs: 15e3,
14117
+ maxCaptureChars: 1024
14118
+ });
12564
14119
  verified = result.success && result.data.stdout.trim() === "VERIFIED";
12565
14120
  } else {
12566
- const args = service.kind === "ldap" ? ["exec", "-i", service.container, "ldapwhoami", "-x", "-H", "ldap://127.0.0.1:389", "-D", service.bindDn, "-y", "/dev/stdin"] : ["exec", "-i", service.container, "sh", "-c", 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"', "hai-secret-check", service.username, service.database, service.container];
12567
- const result = await runCommand(options.runtime, args, { input: service.kind === "ldap" ? password : `${password}
12568
- `, secrets, timeoutMs: 15e3, maxCaptureChars: 1024 });
14121
+ const args = service.kind === "ldap" ? [
14122
+ "exec",
14123
+ "-i",
14124
+ service.container,
14125
+ "ldapwhoami",
14126
+ "-x",
14127
+ "-H",
14128
+ "ldap://127.0.0.1:389",
14129
+ "-D",
14130
+ service.bindDn,
14131
+ "-y",
14132
+ "/dev/stdin"
14133
+ ] : [
14134
+ "exec",
14135
+ "-i",
14136
+ service.container,
14137
+ "sh",
14138
+ "-c",
14139
+ 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"',
14140
+ "hai-secret-check",
14141
+ service.username,
14142
+ service.database,
14143
+ service.container
14144
+ ];
14145
+ const result = await runCommand(options.runtime, args, {
14146
+ input: service.kind === "ldap" ? password : `${password}
14147
+ `,
14148
+ secrets,
14149
+ timeoutMs: 15e3,
14150
+ maxCaptureChars: 1024
14151
+ });
12569
14152
  verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
12570
14153
  if (verified && service.kind === "mysql") {
12571
- const rootCheck = await runCommand(options.runtime, [...args.slice(0, -3), "root", service.database, service.container], { input: `${password}
12572
- `, secrets, timeoutMs: 15e3, maxCaptureChars: 1024 });
14154
+ const rootCheck = await runCommand(options.runtime, [
14155
+ ...args.slice(0, -3),
14156
+ "root",
14157
+ service.database,
14158
+ service.container
14159
+ ], {
14160
+ input: `${password}
14161
+ `,
14162
+ secrets,
14163
+ timeoutMs: 15e3,
14164
+ maxCaptureChars: 1024
14165
+ });
12573
14166
  verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
12574
14167
  const invalidPassword = `hai-import-invalid-${randomUUID()}`;
12575
14168
  for (const username of [service.username, "root"]) {
12576
14169
  if (!verified)
12577
14170
  break;
12578
- const negative = await runCommand(options.runtime, [...args.slice(0, -3), username, service.database, service.container], { input: `${invalidPassword}
12579
- `, secrets: [...secrets, invalidPassword], timeoutMs: 15e3, maxCaptureChars: 1024 });
14171
+ const negative = await runCommand(options.runtime, [
14172
+ ...args.slice(0, -3),
14173
+ username,
14174
+ service.database,
14175
+ service.container
14176
+ ], {
14177
+ input: `${invalidPassword}
14178
+ `,
14179
+ secrets: [...secrets, invalidPassword],
14180
+ timeoutMs: 15e3,
14181
+ maxCaptureChars: 1024
14182
+ });
12580
14183
  verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
12581
14184
  }
12582
14185
  }
@@ -12586,8 +14189,15 @@ async function importDeploymentSecrets(options) {
12586
14189
  phase = "identity";
12587
14190
  }
12588
14191
  phase = "state";
12589
- const uniqueVolumes = [...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
12590
- const lines = ["FORMAT_VERSION=2", `PROJECT=${project}`, ...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`), ...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)];
14192
+ const uniqueVolumes = [
14193
+ ...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()
14194
+ ].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
14195
+ const lines = [
14196
+ "FORMAT_VERSION=2",
14197
+ `PROJECT=${project}`,
14198
+ ...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`),
14199
+ ...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)
14200
+ ];
12591
14201
  let changed = false;
12592
14202
  if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
12593
14203
  const existing = readImportedState(privateFile(stateFile));
@@ -12600,9 +14210,24 @@ async function importDeploymentSecrets(options) {
12600
14210
  throw new Error("lock");
12601
14211
  changed = true;
12602
14212
  }
12603
- return ok({ projectName: config.projectName, stateVersion: 2, groups: selectedGroups, volumes: uniqueVolumes.map((volume) => volume.name), changed });
14213
+ return ok({
14214
+ projectName: config.projectName,
14215
+ stateVersion: 2,
14216
+ groups: selectedGroups,
14217
+ volumes: uniqueVolumes.map((volume) => volume.name),
14218
+ changed
14219
+ });
12604
14220
  } catch {
12605
- return err({ ...HaiDeployError.CONFIG_ERROR, message: deployM("deploy_secretImportFailed", { params: { phase } }), ext: { phase, state: commitAttempted ? "unknown" : "unchanged" } });
14221
+ return err({
14222
+ ...HaiDeployError.CONFIG_ERROR,
14223
+ message: deployM("deploy_secretImportFailed", {
14224
+ params: { phase }
14225
+ }),
14226
+ ext: {
14227
+ phase,
14228
+ state: commitAttempted ? "unknown" : "unchanged"
14229
+ }
14230
+ });
12606
14231
  } finally {
12607
14232
  await lock?.release();
12608
14233
  }
@@ -12638,19 +14263,26 @@ function isSymbolicLink(path) {
12638
14263
  }
12639
14264
  }
12640
14265
  function privateFile(path) {
12641
- const stat = lstatSync(path);
12642
- if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || process.platform !== "win32" && (stat.mode & 63 || process.getuid?.() !== 0 && stat.uid !== process.getuid?.()))
12643
- throw new Error("private-file");
12644
- return readFileSync(path, "utf8");
14266
+ return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
12645
14267
  }
12646
14268
  async function inspectContainer(runtime, container, secrets) {
12647
- const result = await runCommand(runtime, ["inspect", "--format", "{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}", container], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
14269
+ const result = await runCommand(runtime, [
14270
+ "inspect",
14271
+ "--format",
14272
+ "{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}",
14273
+ container
14274
+ ], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
12648
14275
  if (!result.success || result.data.stdoutTruncated)
12649
14276
  throw new Error("identity");
12650
14277
  const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
12651
14278
  const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
12652
- const mounts = z.array(z.object({ Type: z.string(), Name: z.string().optional() })).parse(JSON.parse(mountsText));
12653
- const networks = z.record(z.string(), z.object({ NetworkID: z.string() })).parse(JSON.parse(networksText) ?? {});
14279
+ const mounts = z.array(z.object({
14280
+ Type: z.string(),
14281
+ Name: z.string().optional()
14282
+ })).parse(JSON.parse(mountsText));
14283
+ const networks = z.record(z.string(), z.object({
14284
+ NetworkID: z.string()
14285
+ })).parse(JSON.parse(networksText) ?? {});
12654
14286
  const dockerProject = labels["com.docker.compose.project"] || void 0;
12655
14287
  const podmanProject = labels["io.podman.compose.project"] || void 0;
12656
14288
  const dockerService = labels["com.docker.compose.service"] || void 0;
@@ -12658,11 +14290,27 @@ async function inspectContainer(runtime, container, secrets) {
12658
14290
  if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
12659
14291
  throw new Error("identity");
12660
14292
  }
12661
- return { project: dockerProject ?? podmanProject, service: dockerService ?? podmanService, running: runningText === "true", volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []), networks: Object.values(networks).map((network) => network.NetworkID) };
14293
+ return {
14294
+ project: dockerProject ?? podmanProject,
14295
+ service: dockerService ?? podmanService,
14296
+ running: runningText === "true",
14297
+ volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []),
14298
+ networks: Object.values(networks).map((network) => network.NetworkID)
14299
+ };
12662
14300
  }
12663
14301
  async function acquireImportLock(path, root) {
12664
14302
  return new Promise((resolveLock, reject) => {
12665
- const child = spawn("flock", ["-n", path, process.execPath, "-e", importCommitScript, root], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
14303
+ const child = spawn("flock", [
14304
+ "-n",
14305
+ path,
14306
+ process.execPath,
14307
+ "-e",
14308
+ importCommitScript,
14309
+ root
14310
+ ], {
14311
+ stdio: ["pipe", "pipe", "ignore"],
14312
+ windowsHide: true
14313
+ });
12666
14314
  let acquired = false;
12667
14315
  let exited = false;
12668
14316
  let output = "";