@h-ai/deploy 1.0.0-beta.18 → 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,13 +17,15 @@ 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 字符。 */
@@ -33,7 +36,7 @@ var DeploymentArtifactSchema = z.object({
33
36
  source: z.object({
34
37
  /** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
35
38
  commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
36
- /** 工程目录有未提交改动时为 true;无法检查时为 null。 */
39
+ /** 工程及所选镜像上下文/构建定义有未提交改动时为 true;跨仓库或无法检查时为 null。 */
37
40
  dirty: z.boolean().nullable()
38
41
  }).optional(),
39
42
  /** 只包含所选镜像的构建不能用于首次安装。 */
@@ -561,8 +564,13 @@ var SshConfigSchema = z.object({
561
564
  }).superRefine((ssh, context) => {
562
565
  if (ssh.auth === "key" || ssh.auth === void 0 && !ssh.password)
563
566
  return;
564
- if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
565
- 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
+ }
566
574
  });
567
575
  var HttpsDomainSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
568
576
  var httpsOptions = {
@@ -599,8 +607,13 @@ var PortExposeSchema = z.object({
599
607
  /** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
600
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")
601
609
  }).superRefine((expose, context) => {
602
- if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
603
- 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
+ }
604
617
  });
605
618
  var ExposeSchema = PortExposeSchema;
606
619
  var DockerSshProviderConfigSchema = z.object({
@@ -617,7 +630,12 @@ var DockerSshProviderConfigSchema = z.object({
617
630
  /** 应用暴露方式 */
618
631
  expose: ExposeSchema,
619
632
  /** Nginx 前方可信负载均衡的 IP/CIDR;省略时不接受外部转发头。 */
620
- 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()
621
639
  });
622
640
  var ProviderConfigSchema = z.discriminatedUnion("type", [
623
641
  VercelProviderConfigSchema,
@@ -658,23 +676,52 @@ var ServicesConfigSchema = z.object({
658
676
  sms: z.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
659
677
  }).optional();
660
678
  function provisionerFields(provisioner) {
661
- 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);
662
686
  return schema ? Object.keys(schema.shape).filter((name) => name !== "provisioner") : [];
663
687
  }
664
- 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
+ ]);
665
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;
666
703
  var ComposeServiceConfigSchema = z.strictObject({
667
704
  /** 容器内的主机名到 IPv4 或 IPv6 地址映射,不能覆盖框架内部服务名。 */
668
- 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
+ ], {
669
709
  error: () => deployM("deploy_composeHostAddressInvalid")
670
710
  })).superRefine((hosts, ctx) => {
671
711
  const seen = /* @__PURE__ */ new Set();
672
712
  for (const host of Object.keys(hosts)) {
673
713
  const key = host.toLowerCase();
674
714
  if (host.length > 253 || !composeHostname.test(host)) {
675
- ctx.addIssue({ code: "custom", message: deployM("deploy_composeHostnameInvalid") });
715
+ ctx.addIssue({
716
+ code: "custom",
717
+ message: deployM("deploy_composeHostnameInvalid")
718
+ });
676
719
  } else if (reservedComposeHosts.has(key) || seen.has(key)) {
677
- 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
+ });
678
725
  }
679
726
  seen.add(key);
680
727
  }
@@ -712,8 +759,12 @@ function validateComposeServices(container, artifact) {
712
759
  const field2 = `deploy.container.compose.services.${unavailable}`;
713
760
  return err({
714
761
  ...HaiDeployError.CONFIG_ERROR,
715
- message: deployM("deploy_composeServiceUnavailable", { params: { field: field2 } }),
716
- ext: { fields: [field2] }
762
+ message: deployM("deploy_composeServiceUnavailable", {
763
+ params: { field: field2 }
764
+ }),
765
+ ext: {
766
+ fields: [field2]
767
+ }
717
768
  });
718
769
  }
719
770
  function resolveImageReference(reference, source) {
@@ -730,8 +781,11 @@ function resolveImageReference(reference, source) {
730
781
  parts.unshift("library");
731
782
  const mappings = source.type === "cn" ? { "docker.io": "docker.1ms.run", "ghcr.io": "ghcr.1ms.run" } : source.registries;
732
783
  const replacement = mappings?.[registry];
733
- if (!replacement || typeof replacement !== "string")
734
- 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
+ }
735
789
  if (replacement.split("/").some((part) => part === "." || part === ".."))
736
790
  throw new Error(deployM("deploy_downloadInvalid"));
737
791
  let url;
@@ -748,20 +802,32 @@ function resolveImageReference(reference, source) {
748
802
  function systemProxy() {
749
803
  if (process.platform === "win32") {
750
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";
751
- 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 }));
752
811
  if (value.pac)
753
812
  throw new Error(deployM("deploy_proxySystemUnsupported"));
754
813
  if (!value.enabled || !value.server)
755
814
  return {};
756
815
  const entries = Object.fromEntries(value.server.split(";").map((item) => item.includes("=") ? item.split("=", 2) : ["all", item]));
757
- 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
+ };
758
821
  }
759
822
  if (process.platform === "darwin") {
760
823
  const output = execFileSync("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 5e3 });
761
824
  const get = (key) => output.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
762
825
  if (get("ProxyAutoConfigEnable") === "1" || get("ProxyAutoDiscoveryEnable") === "1")
763
826
  throw new Error(deployM("deploy_proxySystemUnsupported"));
764
- 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
+ };
765
831
  }
766
832
  throw new Error(deployM("deploy_proxySystemUnsupported"));
767
833
  }
@@ -789,7 +855,18 @@ function resolveDownloadProxy(config) {
789
855
  const bypass = mode === "none" ? "" : config?.bypass ?? values.bypass ?? "";
790
856
  if (/[\r\n\0]/.test(bypass))
791
857
  throw new Error(deployM("deploy_downloadInvalid"));
792
- 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
+ };
793
870
  }
794
871
 
795
872
  // src/deploy-config-shapes.json
@@ -6129,13 +6206,95 @@ var deploy_config_shapes_default = {
6129
6206
  }
6130
6207
  }
6131
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
+ }
6132
6291
 
6133
6292
  // src/deploy-environment.ts
6134
6293
  var shapes = deploy_config_shapes_default;
6135
6294
  function environmentFile(path) {
6136
6295
  const values = {};
6137
6296
  if (existsSync(path)) {
6138
- 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")))) {
6139
6298
  if (value !== void 0)
6140
6299
  values[name] = value;
6141
6300
  }
@@ -6146,7 +6305,15 @@ function mergeEnvironmentLayers(layers, prefix) {
6146
6305
  return core.config.mergeEnv(layers.map((layer) => Object.fromEntries(Object.entries(layer).filter(([name]) => !prefix || name === prefix || name.startsWith(`${prefix}_`)))));
6147
6306
  }
6148
6307
  function describeDeploymentVariableSources(directory, environment, values, names) {
6149
- 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
+ }));
6150
6317
  return names.filter((name) => values[name]).map((name) => {
6151
6318
  const source = process.env[name] !== void 0 ? "process.env" : files.find((file) => Object.hasOwn(file.values, name))?.path;
6152
6319
  return `${name}: ${source ?? ".env.runtime"}`;
@@ -6161,32 +6328,65 @@ function loadDeploymentConfig(name, path, values) {
6161
6328
  const project = JSON.parse(content);
6162
6329
  input = project && typeof project === "object" && !Array.isArray(project) ? project[name] : void 0;
6163
6330
  } catch {
6164
- 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
+ }));
6165
6339
  }
6166
- if (!input || typeof input !== "object" || Array.isArray(input))
6167
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
6168
6340
  } else {
6169
6341
  const lineCounter = new LineCounter();
6170
6342
  const document = parseDocument(content, { lineCounter, prettyErrors: false });
6171
- if (document.errors.length)
6172
- 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
+ }
6173
6351
  input = document.toJS();
6174
6352
  }
6175
- 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
+ });
6176
6360
  if (!resolved.success) {
6177
6361
  const cause = resolved.error.cause;
6178
6362
  const fields = cause && typeof cause === "object" && "fields" in cause ? cause.fields : void 0;
6179
6363
  const safeFields = Array.isArray(fields) ? fields.filter((field2) => typeof field2 === "string" && /^HAI_[A-Z0-9_]+$/.test(field2)).join(", ") : "";
6180
- if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? ""))
6181
- return err({ ...resolved.error, message: deployM("deploy_reachEmptyProviderOverride", { params: { path, field: safeFields } }) });
6182
- 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
+ });
6183
6378
  }
6184
6379
  const config = resolved.data;
6185
- if (config !== null && (typeof config !== "object" || Array.isArray(config)))
6186
- 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
+ }
6187
6385
  return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
6188
6386
  } catch {
6189
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
6387
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
6388
+ params: { path }
6389
+ }));
6190
6390
  }
6191
6391
  }
6192
6392
  function loadDeploymentEnvironment(options) {
@@ -6195,8 +6395,11 @@ function loadDeploymentEnvironment(options) {
6195
6395
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
6196
6396
  const envDir = resolve(options.envDir);
6197
6397
  const target = join(envDir, `.env.${environment}`);
6198
- if (options.requireFile !== false && !existsSync(target))
6199
- 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
+ }
6200
6403
  const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
6201
6404
  if (!loaded.success)
6202
6405
  return loaded;
@@ -6210,25 +6413,40 @@ function loadDeploymentEnvironment(options) {
6210
6413
  values.HAI_DEPLOY_ENVIRONMENT = environment;
6211
6414
  if (values.HAI_CORE_ENV === void 0) {
6212
6415
  const fallback = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
6213
- 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
+ });
6214
6422
  const settings = configured.success && configured.data && typeof configured.data === "object" ? configured.data : void 0;
6215
6423
  values.HAI_CORE_ENV = typeof settings?.env === "string" ? settings.env : fallback;
6216
6424
  }
6217
- return ok({ environment, envDir, values, targetValues: selected });
6425
+ return ok({
6426
+ environment,
6427
+ envDir,
6428
+ values,
6429
+ targetValues: selected
6430
+ });
6218
6431
  } catch {
6219
6432
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
6220
6433
  }
6221
6434
  }
6222
6435
  var reachGatewayEnvironment = /^HAI_REACH_PROVIDERS_\d+_GATEWAY$/i;
6223
6436
  function isSecret(name, value) {
6437
+ if (name === "headers" || /^HAI_REACH_PROVIDERS_\d+_HEADERS(?:_|$)/i.test(name))
6438
+ return true;
6224
6439
  if (/^HAI_REACH_PROVIDERS_\d+_GATEWAY_(?:HEADERS|BODY)(?:_|$)/i.test(name))
6225
6440
  return true;
6226
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"))
6227
6442
  return true;
6228
6443
  try {
6229
6444
  const url = new URL(value);
6230
- 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))) {
6231
6448
  return true;
6449
+ }
6232
6450
  } catch {
6233
6451
  }
6234
6452
  try {
@@ -6270,7 +6488,13 @@ function generatedVariables(values) {
6270
6488
  const structuredNames = Object.keys(group).filter((name) => structures.has(name));
6271
6489
  if (!structuredNames.length)
6272
6490
  continue;
6273
- 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
+ });
6274
6498
  if (resolved.success) {
6275
6499
  const flattened = core.config.toEnv({ prefix: `HAI_${module.toUpperCase()}`, value: resolved.data, shape: shapes[module] });
6276
6500
  const represented = structuredNames.every((name) => Object.entries(flattened).some(([key, value]) => key === name || key.startsWith(`${name}_`) || name.startsWith(`${key}_`) && value.startsWith("[")));
@@ -6308,12 +6532,12 @@ function generatedVariables(values) {
6308
6532
  return generated;
6309
6533
  }
6310
6534
  function updateFile(path, values, privateFile2) {
6311
- 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";
6312
6536
  for (const [name, value] of Object.entries(values)) {
6313
6537
  if (!/^[A-Z][A-Z0-9_]*$/.test(name))
6314
6538
  throw new Error("Invalid environment variable name");
6315
6539
  const line = core.config.stringifyEnv({ [name]: value }).trimEnd();
6316
- 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");
6317
6541
  if (pattern.test(content))
6318
6542
  content = content.replace(pattern, () => line);
6319
6543
  else
@@ -6324,25 +6548,22 @@ ${line}
6324
6548
  for (const [name, value] of Object.entries(core.config.parseEnv(content)).sort(([left], [right]) => left.length - right.length)) {
6325
6549
  if (!name.startsWith("HAI_") || !/^\s*[[{]/.test(value))
6326
6550
  continue;
6327
- const parent = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "m").exec(content);
6328
- 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);
6329
6553
  if (parent && child && parent.index > child.index) {
6330
6554
  const withoutParent = content.slice(0, parent.index) + content.slice(parent.index + parent[0].length);
6331
6555
  content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
6332
6556
  }
6333
6557
  }
6334
- writeFileSync(path, content, { mode: privateFile2 ? 384 : 420 });
6335
- if (privateFile2)
6336
- chmodSync(path, 384);
6558
+ writeLocalFile(path, content, privateFile2);
6337
6559
  }
6338
6560
  function removeOverrides(path, names, privateFile2 = true) {
6339
6561
  if (!existsSync(path) || names.length === 0)
6340
6562
  return;
6341
- let content = readFileSync(path, "utf8");
6563
+ let content = readLocalFile(path).toString("utf8");
6342
6564
  for (const name of names)
6343
- content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
6344
- writeFileSync(path, content, { mode: privateFile2 ? 384 : 420 });
6345
- 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);
6346
6567
  }
6347
6568
  function saveDeploymentEnvironment(environment, values, options = {}) {
6348
6569
  try {
@@ -6352,8 +6573,12 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
6352
6573
  return value;
6353
6574
  if (Array.isArray(value))
6354
6575
  return value.map((item, index) => splitArrayValue2(`${name}_${index}`, item) ?? null);
6355
- if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key)))
6356
- 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
+ }
6357
6582
  privateValues[name] = literal;
6358
6583
  return void 0;
6359
6584
  };
@@ -6407,10 +6632,10 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
6407
6632
  removeOverrides(privatePath, Object.keys(shared).filter((name) => !(name in privateValues)));
6408
6633
  if (Object.keys(privateValues).length) {
6409
6634
  const ignorePath = join(environment.envDir, ".gitignore");
6410
- const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
6635
+ const ignore = existsSync(ignorePath) ? readLocalFile(ignorePath).toString("utf8") : "";
6411
6636
  const missing = [".env.local", ".env.*.local", ".hai/deploy/"].filter((line) => !ignore.split(/\r?\n/).includes(line));
6412
6637
  if (missing.length)
6413
- writeFileSync(ignorePath, `${ignore.trimEnd()}
6638
+ writeLocalFile(ignorePath, `${ignore.trimEnd()}
6414
6639
  ${missing.join("\n")}
6415
6640
  `);
6416
6641
  }
@@ -6420,7 +6645,14 @@ ${missing.join("\n")}
6420
6645
  }
6421
6646
  }
6422
6647
  function normalizeProjectName(name) {
6423
- 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);
6424
6656
  if (!normalized)
6425
6657
  throw new Error(deployM("deploy_projectNameRequired"));
6426
6658
  return normalized;
@@ -6473,7 +6705,9 @@ function normalizeCredentialEntries(entries) {
6473
6705
  }
6474
6706
  function assertAllowedCredentialKey(key) {
6475
6707
  if (!isAllowedCredentialKey(key)) {
6476
- throw new Error(deployM("deploy_credentialInvalidKey", { params: { key } }));
6708
+ throw new Error(deployM("deploy_credentialInvalidKey", {
6709
+ params: { key }
6710
+ }));
6477
6711
  }
6478
6712
  }
6479
6713
  function assertAllowedCredentialEntries(entries) {
@@ -6525,7 +6759,9 @@ function loadCredentials() {
6525
6759
  return err(
6526
6760
  HaiDeployError.CREDENTIAL_ERROR,
6527
6761
  deployM("deploy_credentialError", {
6528
- params: { error: error instanceof Error ? error.message : String(error) }
6762
+ params: {
6763
+ error: error instanceof Error ? error.message : String(error)
6764
+ }
6529
6765
  }),
6530
6766
  error
6531
6767
  );
@@ -6545,7 +6781,9 @@ function saveCredential(key, value) {
6545
6781
  return err(
6546
6782
  HaiDeployError.CREDENTIAL_ERROR,
6547
6783
  deployM("deploy_credentialError", {
6548
- params: { error: error instanceof Error ? error.message : String(error) }
6784
+ params: {
6785
+ error: error instanceof Error ? error.message : String(error)
6786
+ }
6549
6787
  }),
6550
6788
  error
6551
6789
  );
@@ -6567,7 +6805,9 @@ function saveCredentials(entries) {
6567
6805
  return err(
6568
6806
  HaiDeployError.CREDENTIAL_ERROR,
6569
6807
  deployM("deploy_credentialError", {
6570
- params: { error: error instanceof Error ? error.message : String(error) }
6808
+ params: {
6809
+ error: error instanceof Error ? error.message : String(error)
6810
+ }
6571
6811
  }),
6572
6812
  error
6573
6813
  );
@@ -6576,12 +6816,59 @@ function saveCredentials(entries) {
6576
6816
 
6577
6817
  // src/deploy-middleware-config.ts
6578
6818
  var connectionFields = {
6579
- db: ["url", "host", "port", "database", "user", "password", "ssl"],
6580
- cache: ["url", "host", "port", "password", "cluster", "sentinel", "tls"],
6581
- storage: ["endpoint", "bucket", "region", "accessKeyId", "secretAccessKey", "forcePathStyle", "createBucket"],
6582
- vecdb: ["url", "host", "port", "database", "user", "password", "apiKey", "path", "serverCommand", "startupTimeout"],
6583
- ldap: ["url", "bindDn", "bindPassword", "searchBase", "useTls"],
6584
- 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
+ ]
6585
6872
  };
6586
6873
  function mergeManagedMiddlewareConfig(requirement, supplied) {
6587
6874
  if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
@@ -6589,7 +6876,10 @@ function mergeManagedMiddlewareConfig(requirement, supplied) {
6589
6876
  const configured = { ...requirement.config };
6590
6877
  for (const field2 of connectionFields[requirement.service] ?? [])
6591
6878
  delete configured[field2];
6592
- const merged = { ...configured, ...supplied };
6879
+ const merged = {
6880
+ ...configured,
6881
+ ...supplied
6882
+ };
6593
6883
  if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
6594
6884
  const url = new URL(supplied.url);
6595
6885
  if (configured.db !== 0 || url.pathname !== "" && url.pathname !== "/") {
@@ -6604,21 +6894,39 @@ var RecoverySchema = z.object({
6604
6894
  stage: z.string(),
6605
6895
  platformProjectId: z.string().optional(),
6606
6896
  resources: z.array(z.object({
6607
- serviceType: z.enum(["db", "cache", "storage", "email", "sms"]),
6897
+ serviceType: z.enum([
6898
+ "db",
6899
+ "cache",
6900
+ "storage",
6901
+ "email",
6902
+ "sms"
6903
+ ]),
6608
6904
  provisionerName: z.string(),
6609
6905
  resourceInfo: z.string(),
6610
6906
  resourceStatus: z.enum(["created", "reused", "unknown"])
6611
6907
  }))
6612
6908
  });
6613
6909
  var CommandStateSchema = z.object({
6614
- 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
+ ]),
6615
6918
  localTermination: z.enum(["confirmed", "unknown", "not-started"]),
6616
6919
  remoteExecution: z.enum(["unknown", "finished"]).optional(),
6617
6920
  deploymentId: z.string().optional(),
6618
6921
  resultFile: z.string().optional(),
6619
6922
  remoteExitCode: z.number().int().optional(),
6620
6923
  remoteStage: z.string().optional(),
6621
- remoteRecovery: z.enum(["not-needed", "passed", "failed", "stopped"]).optional()
6924
+ remoteRecovery: z.enum([
6925
+ "not-needed",
6926
+ "passed",
6927
+ "failed",
6928
+ "stopped"
6929
+ ]).optional()
6622
6930
  });
6623
6931
  function getDeployCommandState(error) {
6624
6932
  const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
@@ -6634,7 +6942,9 @@ function recoveryFailure(error, recovery) {
6634
6942
  httpStatus: error.httpStatus,
6635
6943
  system: error.system,
6636
6944
  module: error.module,
6637
- message: deployM("deploy_recoveryFailure", { params: { stage: recovery.stage } }),
6945
+ message: deployM("deploy_recoveryFailure", {
6946
+ params: { stage: recovery.stage }
6947
+ }),
6638
6948
  suggestion: deployM("deploy_recoverySuggestion"),
6639
6949
  ext: { recovery }
6640
6950
  });
@@ -6669,12 +6979,15 @@ function runtimeDependencies(appDir) {
6669
6979
  visited.add(id);
6670
6980
  const metadata = importer === void 0 ? record(packages[id.split("(")[0]]) : {};
6671
6981
  const peers = record(metadata.peerDependencies);
6672
- 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
+ })) {
6673
6986
  if (name in peers)
6674
6987
  continue;
6675
6988
  const version = typeof reference === "string" ? reference : record(reference).version;
6676
6989
  if (typeof version !== "string")
6677
- throw new Error("Invalid pnpm dependency reference");
6990
+ throw new TypeError("Invalid pnpm dependency reference");
6678
6991
  if (version.startsWith("link:")) {
6679
6992
  if (importer === void 0)
6680
6993
  throw new Error("Invalid workspace dependency");
@@ -6695,7 +7008,14 @@ function runtimeDependencies(appDir) {
6695
7008
  }
6696
7009
  };
6697
7010
  visit(`workspace:${importerKey}`, record(importers[importerKey]), importerKey);
6698
- 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
+ };
6699
7019
  }
6700
7020
  async function scanApp(inputDir, options) {
6701
7021
  try {
@@ -6717,7 +7037,14 @@ async function scanApp(inputDir, options) {
6717
7037
  const deps = graph.runtime;
6718
7038
  const configs = {};
6719
7039
  const enabledConfigs = /* @__PURE__ */ new Set();
6720
- 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
+ })) {
6721
7048
  configs[name] = {};
6722
7049
  if (!deps.has(`@h-ai/${owner}`))
6723
7050
  continue;
@@ -6731,7 +7058,15 @@ async function scanApp(inputDir, options) {
6731
7058
  }
6732
7059
  const middlewares = [];
6733
7060
  const add = (service, backend, configName = service, configPath = [], config = configs[configName] ?? {}, source = `_${configName}.yml`) => {
6734
- 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
+ });
6735
7070
  };
6736
7071
  const iam = enabledConfigs.has("iam");
6737
7072
  if (enabledConfigs.has("db"))
@@ -6752,8 +7087,12 @@ async function scanApp(inputDir, options) {
6752
7087
  const provider = record(item);
6753
7088
  const type = String(provider.type ?? "");
6754
7089
  const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
6755
- if (service)
6756
- 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
+ }
6757
7096
  }
6758
7097
  }
6759
7098
  if (enabledConfigs.has("reach") && providers === void 0)
@@ -6768,19 +7107,33 @@ async function scanApp(inputDir, options) {
6768
7107
  isSvelteKit,
6769
7108
  adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
6770
7109
  nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
6771
- requiredServices: [...new Set(middlewares.map((item) => item.service))],
7110
+ requiredServices: [
7111
+ ...new Set(middlewares.map((item) => item.service))
7112
+ ],
6772
7113
  serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
6773
7114
  };
6774
- 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
+ });
6775
7121
  return ok(scan);
6776
7122
  } catch {
6777
- 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
+ }));
6778
7126
  }
6779
7127
  }
6780
7128
 
6781
7129
  // src/container/deploy-compose-identity.ts
6782
7130
  function createComposeContainerLookupScript() {
6783
- 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("|");
6784
7137
  return String.raw`compose_container_ids() (
6785
7138
  if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
6786
7139
  lookup_runtime=$1
@@ -7168,11 +7521,28 @@ deployment_forget_metadata() {
7168
7521
 
7169
7522
  // src/container/deploy-managed-secrets.ts
7170
7523
  var groups = [
7171
- { name: "DB_PASSWORD", keys: ["POSTGRES_PASSWORD", "MYSQL_PASSWORD", "MYSQL_ROOT_PASSWORD"], services: ["db", "vecdb"] },
7172
- { name: "STORAGE_PASSWORD", keys: ["RUSTFS_SECRET_KEY", "MINIO_ROOT_PASSWORD"], services: ["storage"] },
7173
- { 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
+ }
7174
7539
  ];
7175
- 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
+ };
7176
7546
  function resolveComposeNamedVolumes(projectName, mounts, declarations = {}) {
7177
7547
  if (!Array.isArray(mounts))
7178
7548
  return [];
@@ -7198,7 +7568,9 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
7198
7568
  const bindings = /* @__PURE__ */ new Set();
7199
7569
  const volumes = /* @__PURE__ */ new Set();
7200
7570
  for (const group of groups) {
7201
- 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
+ ];
7202
7574
  if (!values.length)
7203
7575
  continue;
7204
7576
  if (values.length !== 1 || !/^[\w-]{32}$/.test(values[0]))
@@ -7400,7 +7772,7 @@ function createBackendStateAssets(projectName, compose, selected) {
7400
7772
  const service = compose.services[serviceName];
7401
7773
  const image = service?.image;
7402
7774
  if (typeof image !== "string")
7403
- throw new Error(deployM("deploy_artifactInvalid"));
7775
+ throw new TypeError(deployM("deploy_artifactInvalid"));
7404
7776
  const imageFamily = image.slice(image.lastIndexOf("/") + 1).split(":")[0];
7405
7777
  const family = imageFamily === "postgres" ? "postgresql" : imageFamily;
7406
7778
  const namedVolumes = resolveComposeNamedVolumes(projectName, service.volumes, compose.volumes);
@@ -7753,7 +8125,13 @@ public static class HaiCommandJob {
7753
8125
  }
7754
8126
  `;
7755
8127
  function powershell(script) {
7756
- 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
+ ];
7757
8135
  }
7758
8136
  function windowsCommand(command, args) {
7759
8137
  const name = `Local\\hai-deploy-${randomUUID()}`;
@@ -7767,7 +8145,13 @@ ${nativeJob}
7767
8145
  [Environment]::SetEnvironmentVariable('${key}', $null)
7768
8146
  exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
7769
8147
  } catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
7770
- 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
+ };
7771
8155
  }
7772
8156
  function windowsJobProbe(name) {
7773
8157
  return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
@@ -7837,7 +8221,10 @@ function spawnCommand(command, args, options) {
7837
8221
  const job = process.platform === "win32" ? windowsCommand(command, args) : void 0;
7838
8222
  const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
7839
8223
  ...options,
7840
- env: job ? { ...options.env, ...job.env } : options.env,
8224
+ env: job ? {
8225
+ ...options.env,
8226
+ ...job.env
8227
+ } : options.env,
7841
8228
  windowsHide: true,
7842
8229
  shell: false,
7843
8230
  detached: !job
@@ -7864,7 +8251,12 @@ function spawnCommand(command, args, options) {
7864
8251
  if (child.pid) {
7865
8252
  if (job) {
7866
8253
  if (!exited) {
7867
- 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()));
7868
8260
  child.kill("SIGKILL");
7869
8261
  }
7870
8262
  } else {
@@ -7906,7 +8298,9 @@ function spawnCommand(command, args, options) {
7906
8298
 
7907
8299
  // src/container/deploy-secret-masker.ts
7908
8300
  function createSecretMasker(secrets = []) {
7909
- 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);
7910
8304
  if (values.reduce((total, value) => total + value.length, 0) > 1048576)
7911
8305
  throw new RangeError("Secret redaction input is too large");
7912
8306
  const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
@@ -7978,11 +8372,19 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
7978
8372
  throw new RangeError("Invalid command termination timeout");
7979
8373
  if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
7980
8374
  throw new RangeError("Invalid command timeout");
7981
- const secrets = [...options.secrets ?? []];
8375
+ const secrets = [
8376
+ ...options.secrets ?? []
8377
+ ];
7982
8378
  const safeCommand = maskSecrets(command, secrets);
7983
8379
  const outputMasker = createSecretMasker(secrets);
7984
8380
  const errorMasker = createSecretMasker(secrets);
7985
- 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
+ });
7986
8388
  const child = control.child;
7987
8389
  child.stdout.setEncoding("utf8");
7988
8390
  child.stderr.setEncoding("utf8");
@@ -8021,7 +8423,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8021
8423
  try {
8022
8424
  await options.onOutput?.(text);
8023
8425
  } catch {
8024
- 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
+ }));
8025
8432
  }
8026
8433
  };
8027
8434
  const captureError = (text) => {
@@ -8031,7 +8438,9 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8031
8438
  if (options.timeoutMs && options.timeoutMs > 0) {
8032
8439
  timer = setTimeout(() => {
8033
8440
  logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
8034
- void interrupt("timeout", deployM("deploy_commandTimeout", { params: { command: safeCommand } }));
8441
+ void interrupt("timeout", deployM("deploy_commandTimeout", {
8442
+ params: { command: safeCommand }
8443
+ }));
8035
8444
  }, options.timeoutMs);
8036
8445
  }
8037
8446
  const outputTask = (async () => {
@@ -8046,7 +8455,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8046
8455
  if (!settled && !interrupted)
8047
8456
  await publish(outputMasker.end());
8048
8457
  } catch {
8049
- 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
+ }));
8050
8464
  }
8051
8465
  })();
8052
8466
  const errorTask = (async () => {
@@ -8061,16 +8475,28 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8061
8475
  if (!settled && !interrupted)
8062
8476
  captureError(errorMasker.end());
8063
8477
  } catch {
8064
- 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
+ }));
8065
8484
  }
8066
8485
  })();
8067
8486
  child.stdin.on("error", () => {
8068
- 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
+ }));
8069
8493
  });
8070
8494
  child.on("error", (error) => {
8071
8495
  const safeError = maskSecrets(error.message, secrets);
8072
8496
  logger3.error("Command failed to start", { command: safeCommand, error: safeError });
8073
- 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
+ }));
8074
8500
  });
8075
8501
  child.on("close", async (code) => {
8076
8502
  await Promise.all([outputTask, errorTask]);
@@ -8079,24 +8505,52 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
8079
8505
  if (timer)
8080
8506
  clearTimeout(timer);
8081
8507
  if (!await control.treeExited()) {
8082
- 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
+ }));
8083
8514
  return;
8084
8515
  }
8085
8516
  const exitCode = code ?? -1;
8086
8517
  if (exitCode === 0) {
8087
- finish(ok({ code: exitCode, stdout, stderr, stdoutTruncated, stderrTruncated }));
8518
+ finish(ok({
8519
+ code: exitCode,
8520
+ stdout,
8521
+ stderr,
8522
+ stdoutTruncated,
8523
+ stderrTruncated
8524
+ }));
8088
8525
  return;
8089
8526
  }
8090
- 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 });
8091
- finish(failure(deployM("deploy_commandFailed", { params: { command: safeCommand, error: maskSecrets(`${stdout.slice(-2500)}
8092
- ${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" }));
8093
8542
  });
8094
8543
  if (options.input !== void 0)
8095
8544
  child.stdin.write(options.input);
8096
8545
  child.stdin.end();
8097
8546
  });
8098
8547
  } catch {
8099
- 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" });
8100
8554
  }
8101
8555
  }
8102
8556
 
@@ -8339,7 +8793,13 @@ printf '%s\n' "$HAI_DEPLOY_ASKPASS_PASSWORD"
8339
8793
  `.replaceAll("\\${", "${");
8340
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";
8341
8795
  function helperFailure(key, reason = "configuration") {
8342
- 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
+ });
8343
8803
  }
8344
8804
  async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8345
8805
  let directory;
@@ -8370,7 +8830,12 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8370
8830
  const source = join(directory, "askpass.cs");
8371
8831
  writeFileSync(source, windowsAskpassSource, { mode: 384 });
8372
8832
  const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
8373
- const compiled = await runCommand(compiler, ["-NoProfile", "-NonInteractive", "-Command", compileAskpassScript], {
8833
+ const compiled = await runCommand(compiler, [
8834
+ "-NoProfile",
8835
+ "-NonInteractive",
8836
+ "-Command",
8837
+ compileAskpassScript
8838
+ ], {
8374
8839
  env: { HAI_DEPLOY_ASKPASS_SOURCE: source, HAI_DEPLOY_ASKPASS_OUTPUT: helper2, HAI_DEPLOY_ASKPASS_PASSWORD: "" },
8375
8840
  timeoutMs: Math.max(1, Math.min(3e4, Math.ceil(deadline - performance.now()))),
8376
8841
  terminationTimeoutMs,
@@ -8391,7 +8856,16 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
8391
8856
  const removed = cleanup();
8392
8857
  return removed.success ? helperFailure("deploy_sshAskpassFailed", "timeout") : removed;
8393
8858
  }
8394
- 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
+ });
8395
8869
  } catch {
8396
8870
  const removed = cleanup();
8397
8871
  return removed.success ? helperFailure("deploy_sshAskpassFailed") : removed;
@@ -9352,7 +9826,10 @@ function usesSshPassword(config) {
9352
9826
  }
9353
9827
  function buildConnectOptions(config) {
9354
9828
  const password = usesSshPassword(config);
9355
- const args = ["-o", password ? "BatchMode=no" : "BatchMode=yes"];
9829
+ const args = [
9830
+ "-o",
9831
+ password ? "BatchMode=no" : "BatchMode=yes"
9832
+ ];
9356
9833
  if (password)
9357
9834
  args.push("-o", "PreferredAuthentications=password", "-o", "PasswordAuthentication=yes", "-o", "PubkeyAuthentication=no", "-o", "NumberOfPasswordPrompts=1");
9358
9835
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
@@ -9389,8 +9866,15 @@ function createSshExecutor(config) {
9389
9866
  const runAuthenticated = async (command, args, options) => {
9390
9867
  if (!usesSshPassword(config))
9391
9868
  return runCommand(command, args, options);
9392
- if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
9393
- 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
+ }
9394
9878
  const deadline = options.timeoutMs === void 0 ? Number.POSITIVE_INFINITY : performance.now() + options.timeoutMs;
9395
9879
  const prepared = await prepareSshAskpass(config.password, deadline, options.terminationTimeoutMs);
9396
9880
  if (!prepared.success)
@@ -9399,20 +9883,36 @@ function createSshExecutor(config) {
9399
9883
  let removed = ok(void 0);
9400
9884
  try {
9401
9885
  const remaining = deadline - performance.now();
9402
- 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, {
9403
9893
  ...options,
9404
9894
  env: prepared.data.env,
9405
9895
  timeoutMs: Number.isFinite(deadline) ? Math.ceil(remaining) : void 0,
9406
- secrets: [...options.secrets ?? [], config.password]
9896
+ secrets: [
9897
+ ...options.secrets ?? [],
9898
+ config.password
9899
+ ]
9407
9900
  });
9408
9901
  } catch {
9409
- 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
+ });
9410
9909
  } finally {
9411
9910
  removed = prepared.data.cleanup();
9412
9911
  }
9413
- if (!removed.success)
9912
+ if (!removed.success) {
9414
9913
  return result.success ? removed : err({ ...result.error, message: `${result.error.message}
9415
9914
  ${removed.error.message}` });
9915
+ }
9416
9916
  return result;
9417
9917
  };
9418
9918
  const exec = async (remoteCommand, options) => {
@@ -9426,13 +9926,31 @@ ${removed.error.message}` });
9426
9926
  if (result.success)
9427
9927
  return result;
9428
9928
  const state = getDeployCommandState(result.error);
9429
- 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
+ });
9430
9942
  };
9431
9943
  const upload = async (localPath, remotePath) => {
9432
9944
  const args = buildScpUploadArgs(config, localPath, remotePath);
9433
9945
  const result = await runAuthenticated("scp", args, { timeoutMs: 6e5 });
9434
9946
  if (!result.success) {
9435
- 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
+ });
9436
9954
  }
9437
9955
  return ok(void 0);
9438
9956
  };
@@ -9440,7 +9958,13 @@ ${removed.error.message}` });
9440
9958
  logger4.debug("SSH preflight", { host: config.host, port: config.port });
9441
9959
  const result = await exec("true", { timeoutMs: 2e4 });
9442
9960
  if (!result.success) {
9443
- 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
+ });
9444
9968
  }
9445
9969
  return ok(void 0);
9446
9970
  };
@@ -9473,8 +9997,11 @@ async function createDeploymentBundle(options) {
9473
9997
  const nodeRuntime = artifact.primaryService === "serv" || !!compose.services[artifact.primaryService]?.environment?.NODE_OPTIONS;
9474
9998
  for (const name of selected) {
9475
9999
  const service = compose.services[name];
9476
- if (!service)
9477
- 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
+ }
9478
10005
  if (typeof service.image === "string")
9479
10006
  references.add(service.image);
9480
10007
  if (service.depends_on) {
@@ -9488,12 +10015,18 @@ async function createDeploymentBundle(options) {
9488
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);
9489
10016
  }
9490
10017
  }
9491
- 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
+ }));
9492
10022
  let runtime;
9493
10023
  const imageList = [];
9494
10024
  for (const reference of references) {
9495
- if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
9496
- 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
+ }
9497
10030
  const built = artifact.images.find((image) => image.imageRef === reference);
9498
10031
  const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
9499
10032
  const destination = join(staging, "images", file);
@@ -9514,10 +10047,20 @@ async function createDeploymentBundle(options) {
9514
10047
  if (!runtime)
9515
10048
  throw new Error(deployM("deploy_bundleRuntimeRequired"));
9516
10049
  }
9517
- 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
+ ]);
9518
10057
  if (!inspected.success) {
9519
10058
  const download = resolveImageReference(reference, options.imageSource);
9520
- 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
+ });
9521
10064
  if (!pulled.success)
9522
10065
  throw new Error(pulled.error.message);
9523
10066
  if (download !== reference) {
@@ -9525,7 +10068,13 @@ async function createDeploymentBundle(options) {
9525
10068
  if (!tagged.success)
9526
10069
  throw new Error(tagged.error.message);
9527
10070
  }
9528
- 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
+ ]);
9529
10078
  }
9530
10079
  const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
9531
10080
  const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
@@ -9537,7 +10086,13 @@ async function createDeploymentBundle(options) {
9537
10086
  checksum = copied;
9538
10087
  }
9539
10088
  if (!checksum) {
9540
- 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 });
9541
10096
  if (!saved.success)
9542
10097
  throw new Error(saved.error.message);
9543
10098
  checksum = await artifactChecksum(destination);
@@ -9579,7 +10134,12 @@ async function createDeploymentBundle(options) {
9579
10134
  if (file !== "https")
9580
10135
  put(`.hai/${file}`, content);
9581
10136
  }
9582
- 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") : "";
9583
10143
  const backend = createBackendStateAssets(projectName, compose, selected);
9584
10144
  const metadata = {
9585
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}` : ""}
@@ -9601,31 +10161,103 @@ async function createDeploymentBundle(options) {
9601
10161
  // 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
9602
10162
  "managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
9603
10163
  "managed-volumes": managed["managed-volumes"],
9604
- "managed-app-keys": updatesApp ? `${[...new Set(options.managedAppKeys)].join("\n")}
10164
+ "managed-app-keys": updatesApp ? `${[
10165
+ ...new Set(options.managedAppKeys)
10166
+ ].join("\n")}
9605
10167
  ` : "\n",
9606
- "unmanaged-app-keys": updatesApp ? `${[...new Set(options.unmanagedAppKeys)].join("\n")}
10168
+ "unmanaged-app-keys": updatesApp ? `${[
10169
+ ...new Set(options.unmanagedAppKeys)
10170
+ ].join("\n")}
9607
10171
  ` : "\n",
9608
10172
  "state-backends": backend["state-backends"]
9609
10173
  };
9610
- 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)}
9611
10183
  `);
9612
10184
  const serverDirectory = `${config.remote.baseDir}/${projectName}`;
9613
- 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 }));
9614
- 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
+ };
9615
10201
  const mode = deployM(options.partial ? "deploy_bundlePartial" : "deploy_bundleFull");
9616
- 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
+ });
9617
10208
  const instructions = `${selection}
9618
10209
  ${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
9619
- const runtimeGuide = deployM("deploy_bundleRuntime", { params: { directory: serverDirectory, deploymentId: options.deploymentId } });
9620
- 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
+ });
9621
10216
  const https = config.expose.https;
9622
10217
  const certificates = https?.type === "files" ? `${serverDirectory}/releases/${options.deploymentId}/.hai/tls` : `${serverDirectory}/https/certificates/${https?.domain}`;
9623
- const httpsGuide = https ? deployM("deploy_bundleHttps", { params: { domain: https.domain, port: https.port, mode: https.type, certificates, environment } }) + (https.type === "letsencrypt" ? `
9624
- ${deployM("deploy_bundleAcme", { params: { state: `${serverDirectory}/https/acme`, domain: https.domain } })}` : "") : deployM("deploy_bundleHttpOnly", { params: { port: config.expose.hostPort } });
9625
- const accountGuide = deployM("deploy_bundleAccounts", { params: { sources: options.accountSources || "\u2014" } });
9626
- const guides = [runtimeGuide, variableGuide, httpsGuide, accountGuide, deployM("deploy_bundleDirectories", { params: { directory: serverDirectory, deploymentId: options.deploymentId } })].join("\n\n");
9627
- const bundleMetadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, deploymentId: options.deploymentId, compose: ".hai/compose.json" } });
9628
- 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
+ });
9629
10261
  put("APPLY.txt", `${bundleMetadata}
9630
10262
  ${selection}
9631
10263
 
@@ -9643,7 +10275,13 @@ ${guides}
9643
10275
  checksums.push(`${fileHashes.get(file) ?? await artifactChecksum(join(staging, file))} ${file}`);
9644
10276
  writeFileSync(join(staging, ".hai/SHA256SUMS"), `${checksums.join("\n")}
9645
10277
  `);
9646
- 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
+ ]);
9647
10285
  if (!tar.success)
9648
10286
  throw new Error(tar.error.message);
9649
10287
  chmodSync(temporaryTar, 384);
@@ -9651,7 +10289,10 @@ ${guides}
9651
10289
  const upload = createUploadScripts();
9652
10290
  const scripts = directory;
9653
10291
  writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
9654
- 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 });
9655
10296
  writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
9656
10297
  writeFileSync(join(scripts, "read-target.awk"), upload.reader);
9657
10298
  const target = {
@@ -9683,10 +10324,18 @@ ${guides}
9683
10324
  writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
9684
10325
  `);
9685
10326
  writeFileSync(join(scripts, "README.txt"), readme);
9686
- 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
+ ])
9687
10334
  rmSync(join(directory, file), { force: true });
9688
10335
  return { bundlePath: output, instructions: `${selection}
9689
- ${deployM("deploy_bundleInstructions", { params: { ...instructionParams, file: output } })}
10336
+ ${deployM("deploy_bundleInstructions", {
10337
+ params: { ...instructionParams, file: output }
10338
+ })}
9690
10339
  ${guides}
9691
10340
  ${scripts}`, startName: "apply.sh" };
9692
10341
  } finally {
@@ -9715,9 +10364,7 @@ function createHttpsAssets(config, projectDirectory) {
9715
10364
  try {
9716
10365
  const readPem = (file) => {
9717
10366
  const path = resolve(projectDirectory, file);
9718
- if (!statSync(path).isFile() || statSync(path).size > 1048576)
9719
- throw new Error("Invalid PEM file");
9720
- return readFileSync(path, "utf8");
10367
+ return readLocalFile(path, { maxBytes: 1048576 }).toString("utf8");
9721
10368
  };
9722
10369
  const certificate = readPem(config.certificateFile);
9723
10370
  const key = readPem(config.privateKeyFile);
@@ -9910,7 +10557,21 @@ function planPostgresDb(plan, projectName, password, net) {
9910
10557
  restart: "unless-stopped",
9911
10558
  env_file: ["middleware-runtime.env"],
9912
10559
  volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
9913
- 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
+ },
9914
10575
  networks: net
9915
10576
  };
9916
10577
  plan.volumes[`${projectName}-db-data`] = null;
@@ -9929,7 +10590,12 @@ function planMysqlDb(plan, projectName, password, net) {
9929
10590
  restart: "unless-stopped",
9930
10591
  env_file: ["middleware-runtime.env"],
9931
10592
  volumes: [`${projectName}-db-data:/var/lib/mysql`],
9932
- 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
+ },
9933
10599
  networks: net
9934
10600
  };
9935
10601
  plan.volumes[`${projectName}-db-data`] = null;
@@ -9947,7 +10613,12 @@ function planRedisCache(plan, projectName, net) {
9947
10613
  image: IMAGES.redis,
9948
10614
  container_name: `hai-${projectName}-cache`,
9949
10615
  restart: "unless-stopped",
9950
- 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
+ },
9951
10622
  networks: net
9952
10623
  };
9953
10624
  plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
@@ -9959,15 +10630,40 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
9959
10630
  image: IMAGES[engine],
9960
10631
  container_name: `hai-${projectName}-storage`,
9961
10632
  restart: "unless-stopped",
9962
- ...engine === "minio" ? { command: ["server", "/data"] } : {},
9963
- 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
+ },
9964
10654
  env_file: ["middleware-runtime.env"],
9965
10655
  volumes: [`${projectName}-storage-data:/data`],
9966
10656
  networks: net
9967
10657
  };
9968
10658
  plan.volumes[`${projectName}-storage-data`] = null;
9969
10659
  if (engine === "rustfs") {
9970
- 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
+ });
9971
10667
  } else {
9972
10668
  Object.assign(plan.containerEnv, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
9973
10669
  }
@@ -9977,7 +10673,12 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
9977
10673
  bucket: configured.bucket || "hai",
9978
10674
  region: configured.region || "us-east-1",
9979
10675
  // 自建只替换连接和托管凭据,不能丢失对象前缀、外部签名地址或操作日志策略。
9980
- ...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]])),
9981
10682
  accessKeyId: "hai-storage",
9982
10683
  secretAccessKey: password,
9983
10684
  forcePathStyle: true,
@@ -10102,11 +10803,30 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
10102
10803
  const password = secrets.ldapPassword ?? secrets.dbPassword;
10103
10804
  plan.services.push("ldap");
10104
10805
  plan.dependsOn.push("ldap");
10105
- 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
+ };
10106
10813
  plan.volumes[`${projectName}-ldap-data`] = null;
10107
10814
  plan.volumes[`${projectName}-ldap-config`] = null;
10108
- Object.assign(plan.containerEnv, { LDAP_DOMAIN: "example.org", LDAP_ORGANISATION: "HAI", LDAP_ADMIN_PASSWORD: password, LDAP_TLS: "false" });
10109
- 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
+ });
10110
10830
  plan.secrets.push(password);
10111
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) {
10112
10832
  plan.services.push("email");
@@ -10114,10 +10834,25 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
10114
10834
  plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
10115
10835
  const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
10116
10836
  if (requirements?.length) {
10117
- for (const requirement of requirements)
10118
- 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
+ }
10119
10847
  } else if (!scan.middlewares) {
10120
- 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
+ });
10121
10856
  }
10122
10857
  }
10123
10858
  }
@@ -10132,15 +10867,21 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
10132
10867
  const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
10133
10868
  const environment = options?.environment ?? appEnv.HAI_DEPLOY_ENVIRONMENT ?? "prod";
10134
10869
  const nodeEnv = environment === "dev" || environment === "development" ? "development" : environment === "test" ? "test" : "production";
10135
- const env = mergeEnvironmentLayers([{
10136
- NODE_ENV: nodeEnv
10137
- }, Object.fromEntries(Object.entries(appEnv).filter(([name]) => !CONTAINER_ENV_KEYS.has(name))), sidecar.env, provisionEnv, {
10138
- // 容器监听由本次制品决定,本机回环地址和开发端口不能覆盖部署拓扑。
10139
- HOST: "0.0.0.0",
10140
- PORT: String(containerPort),
10141
- HAI_SERV_SERVER_HOST: "0.0.0.0",
10142
- HAI_SERV_SERVER_PORT: String(containerPort)
10143
- }]);
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
+ ]);
10144
10885
  return { env, sidecar };
10145
10886
  }
10146
10887
  function generateComposeFile(options) {
@@ -10160,7 +10901,12 @@ function generateComposeFile(options) {
10160
10901
  };
10161
10902
  if (primaryService === "serv") {
10162
10903
  app.healthcheck = {
10163
- 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
+ ],
10164
10910
  interval: "30s",
10165
10911
  timeout: "5s",
10166
10912
  start_period: "15s",
@@ -10178,14 +10924,31 @@ function generateComposeFile(options) {
10178
10924
  appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
10179
10925
  if (options.web || options.gateway)
10180
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
+ }
10181
10938
  if (Object.keys(appEnvironment).length)
10182
10939
  app.environment = appEnvironment;
10183
10940
  if (options.mountWebConfig)
10184
10941
  appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
10185
10942
  if (appVolumes.length)
10186
10943
  app.volumes = appVolumes;
10187
- if (sidecar.dependsOn.length > 0)
10188
- 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
+ }
10189
10952
  const services = { [primaryService]: app, ...sidecar.composeServices };
10190
10953
  if (options.web) {
10191
10954
  delete app.ports;
@@ -10197,7 +10960,21 @@ function generateComposeFile(options) {
10197
10960
  pull_policy: "never",
10198
10961
  ports: [`${hostPort}:${options.web.containerPort}`],
10199
10962
  environment: { PUBLIC_API_BASE: "" },
10200
- 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
+ },
10201
10978
  volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
10202
10979
  depends_on: sidecar.services.includes("storage") ? ["serv", "storage"] : ["serv"],
10203
10980
  networks: ["hai-app"]
@@ -10210,9 +10987,26 @@ function generateComposeFile(options) {
10210
10987
  container_name: `hai-${projectName}-gateway`,
10211
10988
  restart: "unless-stopped",
10212
10989
  ports: [`${hostPort}:8080`],
10213
- 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
+ },
10214
11005
  volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
10215
- depends_on: [primaryService, ...sidecar.services.includes("storage") ? ["storage"] : []],
11006
+ depends_on: [
11007
+ primaryService,
11008
+ ...sidecar.services.includes("storage") ? ["storage"] : []
11009
+ ],
10216
11010
  networks: ["hai-app"]
10217
11011
  };
10218
11012
  }
@@ -10255,7 +11049,10 @@ function generateComposeFile(options) {
10255
11049
  };
10256
11050
  if (Object.keys(sidecar.volumes).length > 0)
10257
11051
  compose.volumes = sidecar.volumes;
10258
- for (const role of [primaryService, ...options.web ? ["web"] : []]) {
11052
+ for (const role of [
11053
+ primaryService,
11054
+ ...options.web ? ["web"] : []
11055
+ ]) {
10259
11056
  const hosts = Object.entries(options.compose?.services[role]?.extra_hosts ?? {});
10260
11057
  if (hosts.length)
10261
11058
  services[role].extra_hosts = hosts.map(([host, ip]) => `${host}:${ip}`);
@@ -10269,12 +11066,18 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
10269
11066
  const webPort = options.webPort ?? 8080;
10270
11067
  const webBasePath = options.webBasePath ?? "";
10271
11068
  const apiPath = apiPrefix.replace(/\/+$/, "");
10272
- const backendPaths = [...new Set(options.backendPaths ?? ["/health"])];
11069
+ const backendPaths = [
11070
+ ...new Set(options.backendPaths ?? ["/health"])
11071
+ ];
10273
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}/`))) {
10274
11073
  throw new Error("Invalid web proxy target");
10275
11074
  }
10276
11075
  const webLocation = renderWebLocation(webBasePath);
10277
- 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) : "";
10278
11081
  const backendLocations = backendPaths.map((path) => ` location = ${path} {
10279
11082
  proxy_pass http://hai_serv_backend;
10280
11083
  proxy_http_version 1.1;
@@ -10366,11 +11169,13 @@ function generateApplicationProxyConfig(options) {
10366
11169
  if (!Number.isInteger(port) || port < 1 || port > 65535)
10367
11170
  throw new Error("Invalid application proxy target");
10368
11171
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
11172
+ const trustedEdges = renderTrustedEdgeProxies(options.trustedEdgeProxies);
11173
+ const forwardedProtocol = options.trustedEdgeProxies?.length ? "$hai_request_scheme" : "$scheme";
10369
11174
  return `include /tmp/hai-dns.conf;
10370
- ${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 {
10371
11176
  listen 8080;
10372
11177
  server_name _;
10373
- ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
11178
+ ${trustedEdges} large_client_header_buffers 4 32k;
10374
11179
  ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
10375
11180
  access_log off;
10376
11181
  return 200 'ok';
@@ -10380,8 +11185,11 @@ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath)
10380
11185
  proxy_pass http://hai_application_backend;
10381
11186
  proxy_http_version 1.1;
10382
11187
  proxy_set_header Host $http_host;
10383
- proxy_set_header X-Forwarded-Proto $scheme;
11188
+ proxy_set_header X-Forwarded-Proto ${forwardedProtocol};
10384
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 "";
10385
11193
  proxy_set_header X-Forwarded-For $remote_addr;
10386
11194
  proxy_set_header X-Real-IP $remote_addr;
10387
11195
  proxy_request_buffering off;
@@ -10409,6 +11217,20 @@ function renderTrustedEdgeProxies(entries = []) {
10409
11217
  real_ip_recursive on;
10410
11218
  `;
10411
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
+ }
10412
11234
  function renderWebLocation(webBasePath) {
10413
11235
  if (webBasePath && (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(webBasePath) || webBasePath.split("/").some((segment) => segment === "." || segment === "..") || ["/health", "/_hai/web-health"].includes(webBasePath)))
10414
11236
  throw new Error("Invalid web base path");
@@ -10464,8 +11286,13 @@ function prepareRequestAccess(options) {
10464
11286
  if (!scope || typeof scope !== "object" || !("access" in scope))
10465
11287
  return ok({ settings: options.settings });
10466
11288
  const configured = RequestAccessConfigSchema.safeParse(scope.access);
10467
- if (!configured.success)
10468
- 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
+ }
10469
11296
  try {
10470
11297
  const policy = core.access.loadPolicy(configured.data, options.directory);
10471
11298
  const check = core.access.createRequestCheck(policy);
@@ -10474,10 +11301,20 @@ function prepareRequestAccess(options) {
10474
11301
  if (configured.data === false || !("file" in configured.data))
10475
11302
  return ok({ settings: options.settings });
10476
11303
  const access = { file: "/run/hai/request-access.yml" };
10477
- const settings = options.module === "serv" ? { ...options.settings, http: { ...scope, access } } : { ...options.settings, access };
10478
- 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
+ });
10479
11312
  } catch {
10480
- 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
+ }));
10481
11318
  }
10482
11319
  }
10483
11320
  var HEALTH_MAX_ATTEMPTS = 40;
@@ -10496,10 +11333,25 @@ function endpointSettings(value) {
10496
11333
  }
10497
11334
  var ServerResultSchema = z.object({
10498
11335
  deploymentId: z.string(),
10499
- 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
+ ]),
10500
11347
  exitCode: z.number().int().min(0).max(255),
10501
11348
  durationSeconds: z.number().int().nonnegative(),
10502
- recoveryStatus: z.enum(["not-needed", "passed", "failed", "stopped"]).optional()
11349
+ recoveryStatus: z.enum([
11350
+ "not-needed",
11351
+ "passed",
11352
+ "failed",
11353
+ "stopped"
11354
+ ]).optional()
10503
11355
  });
10504
11356
  function makeDeploymentId() {
10505
11357
  return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
@@ -10526,7 +11378,11 @@ function createDockerSshProvider(config, container) {
10526
11378
  }
10527
11379
  return err(
10528
11380
  HaiDeployError.REMOTE_COMMAND_FAILED,
10529
- deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
11381
+ deployM("deploy_remoteRuntimeMissing", {
11382
+ params: {
11383
+ runtime: candidates.join(" / ")
11384
+ }
11385
+ })
10530
11386
  );
10531
11387
  }
10532
11388
  async function healthCheck(url, certificate) {
@@ -10534,7 +11390,10 @@ function createDockerSshProvider(config, container) {
10534
11390
  try {
10535
11391
  if (certificate) {
10536
11392
  const healthy = await new Promise((resolve9) => {
10537
- 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) => {
10538
11397
  response.resume();
10539
11398
  resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
10540
11399
  });
@@ -10546,7 +11405,9 @@ function createDockerSshProvider(config, container) {
10546
11405
  if (healthy)
10547
11406
  return true;
10548
11407
  } else {
10549
- const response = await fetch(url, { signal: AbortSignal.timeout(5e3) });
11408
+ const response = await fetch(url, {
11409
+ signal: AbortSignal.timeout(5e3)
11410
+ });
10550
11411
  await response.body?.cancel();
10551
11412
  if (response.ok)
10552
11413
  return true;
@@ -10571,10 +11432,16 @@ function createDockerSshProvider(config, container) {
10571
11432
  const primaryService = artifact.primaryService;
10572
11433
  const app = images.find((image) => image.name === primaryService);
10573
11434
  if (options?.selectedServices) {
10574
- const available = [...images.map((image) => image.name), ...scan.requiredServices];
11435
+ const available = [
11436
+ ...images.map((image) => image.name),
11437
+ ...scan.requiredServices
11438
+ ];
10575
11439
  const invalid = options.selectedServices.find((name2) => !available.includes(name2));
10576
- if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
10577
- 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
+ }
10578
11445
  }
10579
11446
  const hasWebProxy = primaryService === "serv" && artifact.hasWeb;
10580
11447
  const partial = artifact.partial || options?.selectedServices !== void 0;
@@ -10589,14 +11456,12 @@ function createDockerSshProvider(config, container) {
10589
11456
  const plannedRuntime = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
10590
11457
  const { sidecar } = plannedRuntime;
10591
11458
  let { env } = plannedRuntime;
10592
- if (https && scan.isSvelteKit && !env.ORIGIN)
10593
- env.ORIGIN = url;
10594
11459
  const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
10595
11460
  let storageBasePath = "";
10596
11461
  if (managedStorage?.publicEndpoint !== void 0) {
10597
11462
  try {
10598
11463
  if (typeof managedStorage.publicEndpoint !== "string")
10599
- throw new Error("Invalid public endpoint type");
11464
+ throw new TypeError("Invalid public endpoint type");
10600
11465
  const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
10601
11466
  const endpoint = new URL(managedStorage.publicEndpoint);
10602
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)) {
@@ -10608,7 +11473,7 @@ function createDockerSshProvider(config, container) {
10608
11473
  }
10609
11474
  }
10610
11475
  const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
10611
- const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
11476
+ const gateway = !hasWebProxy && (scan.isSvelteKit || !!managedStorage || !!https && !standaloneWebConfig);
10612
11477
  if (gateway && primaryService === "web" && !scan.isSvelteKit)
10613
11478
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
10614
11479
  const proxyService = gateway ? "gateway" : "web";
@@ -10670,8 +11535,14 @@ function createDockerSshProvider(config, container) {
10670
11535
  return prepared2;
10671
11536
  Object.assign(server.data, prepared2.data.settings);
10672
11537
  requestAccessSnapshot = prepared2.data.snapshot;
10673
- if (requestAccessSnapshot)
10674
- 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
+ }
10675
11546
  }
10676
11547
  let feedbackTransportBytes;
10677
11548
  if (scan.haiModules?.includes("@h-ai/feedback")) {
@@ -10697,8 +11568,14 @@ function createDockerSshProvider(config, container) {
10697
11568
  return prepared2;
10698
11569
  Object.assign(kit.data, prepared2.data.settings);
10699
11570
  requestAccessSnapshot = prepared2.data.snapshot;
10700
- if (requestAccessSnapshot)
10701
- 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
+ }
10702
11579
  }
10703
11580
  const transport = kit.data.transport;
10704
11581
  if (transport && typeof transport === "object") {
@@ -10714,8 +11591,13 @@ function createDockerSshProvider(config, container) {
10714
11591
  }
10715
11592
  }
10716
11593
  }
10717
- if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success)
10718
- 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
+ }
10719
11601
  let webProxy;
10720
11602
  if (hasWebProxy) {
10721
11603
  const http = server.data.http;
@@ -10730,11 +11612,30 @@ function createDockerSshProvider(config, container) {
10730
11612
  try {
10731
11613
  const configured = transport ? transport.maxPlaintextBytes : void 0;
10732
11614
  const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
10733
- 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
+ });
10734
11624
  } catch (error) {
10735
11625
  return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
10736
11626
  }
10737
- 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
+ ]);
10738
11639
  } else if (gateway) {
10739
11640
  try {
10740
11641
  const http = server.data.http;
@@ -10742,7 +11643,18 @@ function createDockerSshProvider(config, container) {
10742
11643
  const transport = server.data.transport;
10743
11644
  applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
10744
11645
  }
10745
- 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
+ });
10746
11658
  } catch {
10747
11659
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
10748
11660
  }
@@ -10751,11 +11663,23 @@ function createDockerSshProvider(config, container) {
10751
11663
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
10752
11664
  webProxy = generateStaticWebConfig(containerPort, webBasePath);
10753
11665
  }
10754
- if (https && webProxy)
11666
+ if (https && webProxy) {
10755
11667
  webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
11668
+ }
10756
11669
  if (https && primaryService === "serv" && !hasWebProxy) {
10757
11670
  const cors = server.data.cors;
10758
- 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
+ ]);
10759
11683
  }
10760
11684
  const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
10761
11685
  const compressed = requestedOutput.endsWith(".tar.gz");
@@ -10765,20 +11689,43 @@ function createDockerSshProvider(config, container) {
10765
11689
  if (!/^[\w.+-]+$/.test(name))
10766
11690
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleNameInvalid"));
10767
11691
  const artifactDir = resolve(dirname(requestedOutput), name);
10768
- if (existsSync(artifactDir))
10769
- 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
+ }
10770
11697
  mkdirSync(dirname(artifactDir), { recursive: true, mode: 448 });
10771
11698
  mkdirSync(artifactDir, { mode: 448 });
10772
11699
  chmodSync(artifactDir, 448);
10773
11700
  const envPath = join(artifactDir, ".env.runtime");
10774
11701
  const nodeRuntime = primaryService === "serv" || scan.isSvelteKit;
10775
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
+ }
10776
11714
  const runtimeEnvironment = serializeEnvFile(env, startupOverrides, nodeRuntime ? "node" : "compose");
10777
- const envVarsSet = [.../* @__PURE__ */ new Set([
10778
- ...Object.keys(parseEnv(runtimeEnvironment)),
10779
- ...nodeRuntime ? ["NODE_OPTIONS"] : [],
10780
- ...hasWebProxy || gateway ? ["HAI_DEPLOY_PRIVATE_PROXY"] : []
10781
- ])];
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
+ ];
10782
11729
  writeFileSync(envPath, runtimeEnvironment, { mode: 384 });
10783
11730
  if (requestAccessSnapshot)
10784
11731
  writeFileSync(join(artifactDir, "request-access.yml"), requestAccessSnapshot, { mode: 384 });
@@ -10787,7 +11734,12 @@ function createDockerSshProvider(config, container) {
10787
11734
  const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
10788
11735
  if (!environment.success)
10789
11736
  return environment;
10790
- 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
+ });
10791
11743
  if (!saved.success)
10792
11744
  return saved;
10793
11745
  }
@@ -10813,16 +11765,27 @@ function createDockerSshProvider(config, container) {
10813
11765
  writeFileSync(composePath, composeContent);
10814
11766
  if (webProxy)
10815
11767
  writeFileSync(join(artifactDir, "web.conf"), webProxy);
10816
- 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
+ ];
10817
11772
  if (gateway && !partial)
10818
11773
  selected.push("gateway");
10819
11774
  if (https?.type === "letsencrypt" && !partial)
10820
11775
  selected.push("certbot");
10821
- if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
10822
- 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
+ }
10823
11783
  for (const name2 of selected) {
10824
- if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2))
10825
- 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
+ }
10826
11789
  }
10827
11790
  options?.onProgress?.("packaging");
10828
11791
  const bundle = await createDeploymentBundle({
@@ -10844,14 +11807,28 @@ function createDockerSshProvider(config, container) {
10844
11807
  containerPort,
10845
11808
  managedAppKeys: Object.keys(sidecar.env),
10846
11809
  unmanagedAppKeys: scan.middlewares?.filter((item) => !Object.hasOwn(sidecar.env, item.envPrefix)).map((item) => item.envPrefix),
10847
- 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
+ ]),
10848
11816
  healthPaths,
10849
11817
  httpsAssets,
10850
11818
  output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
10851
11819
  cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
10852
11820
  });
10853
- if (options?.bundleOnly)
10854
- 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
+ }
10855
11832
  if (!ssh)
10856
11833
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
10857
11834
  const ping = await ssh.ping();
@@ -10871,19 +11848,23 @@ function createDockerSshProvider(config, container) {
10871
11848
  let pendingOutput = "";
10872
11849
  const started = await ssh.exec(
10873
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; }`,
10874
- { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
10875
- const parts = output.split("\n");
10876
- for (let index = 0; index < parts.length; index++) {
10877
- pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
10878
- if (index === parts.length - 1)
10879
- break;
10880
- const line = pendingOutput;
10881
- pendingOutput = "";
10882
- const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
10883
- if (stage)
10884
- 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
+ }
10885
11866
  }
10886
- } }
11867
+ }
10887
11868
  );
10888
11869
  if (!started.success) {
10889
11870
  const resultFile = `${remoteDir}/result-${deploymentId}.json`;
@@ -10898,16 +11879,34 @@ function createDockerSshProvider(config, container) {
10898
11879
  } catch {
10899
11880
  }
10900
11881
  }
10901
- return err({ ...started.error, message: `${started.error.message}
10902
- ${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: {
10903
- ...state,
10904
- reason: state?.reason ?? "exit",
10905
- localTermination: state?.localTermination ?? "unknown",
10906
- remoteExecution: terminal ? "finished" : "unknown",
10907
- deploymentId,
10908
- resultFile,
10909
- ...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage, remoteRecovery: terminal.recoveryStatus } : {}
10910
- } } });
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
+ });
10911
11910
  }
10912
11911
  logger5.info("Running health check", { url: applicationUrl });
10913
11912
  let healthy = true;
@@ -10919,16 +11918,24 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
10919
11918
  }
10920
11919
  if (!healthy) {
10921
11920
  return err(
10922
- { ...HaiDeployError.HEALTH_CHECK_FAILED, message: deployM("deploy_healthCheckFailed", { params: { url: applicationUrl } }), ext: { commandExecution: {
10923
- reason: "exit",
10924
- localTermination: "confirmed",
10925
- remoteExecution: "finished",
10926
- deploymentId,
10927
- resultFile: `${remoteDir}/result-${deploymentId}.json`,
10928
- remoteExitCode: 0,
10929
- remoteStage: "complete",
10930
- remoteRecovery: "not-needed"
10931
- } } }
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
+ }
10932
11939
  );
10933
11940
  }
10934
11941
  const result = {
@@ -10950,11 +11957,16 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
10950
11957
  const reason = error instanceof Error ? error.message : String(error);
10951
11958
  let safeReason = "Deployment bundle failed";
10952
11959
  try {
10953
- 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));
10954
11964
  } catch {
10955
11965
  }
10956
11966
  logger5.error("Deployment bundle failed", { reason: safeReason });
10957
- 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
+ }));
10958
11970
  }
10959
11971
  };
10960
11972
  return { name: "docker-ssh", deploy: deploySafely };
@@ -10965,7 +11977,12 @@ var POLL_INTERVAL = 3e3;
10965
11977
  var MAX_POLL_ATTEMPTS = 60;
10966
11978
  var VercelApiError = class extends Error {
10967
11979
  constructor(status) {
10968
- 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
+ }));
10969
11986
  this.status = status;
10970
11987
  }
10971
11988
  status;
@@ -10974,7 +11991,9 @@ function toDeployError(codeDef, messageKey, error) {
10974
11991
  return {
10975
11992
  ...codeDef,
10976
11993
  message: deployM(messageKey, {
10977
- params: { error: error instanceof Error ? error.message : String(error) }
11994
+ params: {
11995
+ error: error instanceof Error ? error.message : String(error)
11996
+ }
10978
11997
  }),
10979
11998
  cause: error
10980
11999
  };
@@ -11184,7 +12203,11 @@ async function uploadFile(token, content, sha) {
11184
12203
  body: new Blob([uploadContent])
11185
12204
  });
11186
12205
  if (!response.ok && response.status !== 409) {
11187
- 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
+ }));
11188
12211
  }
11189
12212
  }
11190
12213
  async function pollDeploymentStatus(token, deploymentId) {
@@ -11217,7 +12240,9 @@ function createAliyunProvisioner() {
11217
12240
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
11218
12241
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
11219
12242
  if (!akId || !akSecret || !credentials.signName) {
11220
- 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
+ }));
11221
12246
  }
11222
12247
  accessKey = akId;
11223
12248
  secretKey = akSecret;
@@ -11229,7 +12254,9 @@ function createAliyunProvisioner() {
11229
12254
  return err(
11230
12255
  HaiDeployError.AUTH_FAILED,
11231
12256
  deployM("deploy_authFailed", {
11232
- params: { error: error instanceof Error ? error.message : String(error) }
12257
+ params: {
12258
+ error: error instanceof Error ? error.message : String(error)
12259
+ }
11233
12260
  }),
11234
12261
  error
11235
12262
  );
@@ -11248,7 +12275,15 @@ function createAliyunProvisioner() {
11248
12275
  provisionerName: "aliyun",
11249
12276
  resourceStatus: "reused",
11250
12277
  envVars: {
11251
- 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
+ ])
11252
12287
  },
11253
12288
  resourceInfo: "aliyun:verify-only"
11254
12289
  });
@@ -11270,7 +12305,12 @@ async function neonFetch(token, path, options) {
11270
12305
  }
11271
12306
  });
11272
12307
  if (!response.ok) {
11273
- 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
+ }));
11274
12314
  }
11275
12315
  return response.json();
11276
12316
  }
@@ -11290,10 +12330,14 @@ async function getNeonConnectionUri(token, projectId) {
11290
12330
  const data = await neonFetch(
11291
12331
  token,
11292
12332
  `/projects/${projectId}/connection_uri?${query.toString()}`,
11293
- { headers: { "Content-Type": "application/json" } }
12333
+ {
12334
+ headers: { "Content-Type": "application/json" }
12335
+ }
11294
12336
  );
11295
12337
  if (!data.uri) {
11296
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
12338
+ throw new Error(deployM("deploy_provisionNoResult", {
12339
+ params: { service: "Neon" }
12340
+ }));
11297
12341
  }
11298
12342
  return data.uri;
11299
12343
  }
@@ -11303,7 +12347,11 @@ function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
11303
12347
  provisionerName: "neon",
11304
12348
  resourceStatus,
11305
12349
  envVars: {
11306
- 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
+ })
11307
12355
  },
11308
12356
  resourceInfo: `neon-project:${projectId}`
11309
12357
  };
@@ -11318,7 +12366,9 @@ function createNeonProvisioner() {
11318
12366
  try {
11319
12367
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
11320
12368
  if (!apiToken) {
11321
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "token" } }));
12369
+ throw new Error(deployM("deploy_credentialMissing", {
12370
+ params: { fields: "token" }
12371
+ }));
11322
12372
  }
11323
12373
  await listNeonProjects(apiToken);
11324
12374
  token = apiToken;
@@ -11329,7 +12379,9 @@ function createNeonProvisioner() {
11329
12379
  return err(
11330
12380
  HaiDeployError.AUTH_FAILED,
11331
12381
  deployM("deploy_authFailed", {
11332
- params: { error: error instanceof Error ? error.message : String(error) }
12382
+ params: {
12383
+ error: error instanceof Error ? error.message : String(error)
12384
+ }
11333
12385
  }),
11334
12386
  error
11335
12387
  );
@@ -11370,7 +12422,9 @@ function createNeonProvisioner() {
11370
12422
  resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
11371
12423
  const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
11372
12424
  if (!projectId || !connectionUri) {
11373
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
12425
+ throw new Error(deployM("deploy_provisionNoResult", {
12426
+ params: { service: "Neon" }
12427
+ }));
11374
12428
  }
11375
12429
  logger8.info("Neon database provisioned", { projectId });
11376
12430
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
@@ -11380,7 +12434,9 @@ function createNeonProvisioner() {
11380
12434
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
11381
12435
  projectName: appName,
11382
12436
  stage: "provision:db",
11383
- resources: [{ serviceType: "db", provisionerName: "neon", ...resource }]
12437
+ resources: [
12438
+ { serviceType: "db", provisionerName: "neon", ...resource }
12439
+ ]
11384
12440
  });
11385
12441
  }
11386
12442
  return err(
@@ -11413,13 +12469,20 @@ function createR2Provisioner() {
11413
12469
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
11414
12470
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
11415
12471
  if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
11416
- 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
+ }));
11417
12475
  }
11418
12476
  const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
11419
12477
  headers: { Authorization: `Bearer ${apiTok}` }
11420
12478
  });
11421
12479
  if (!res.ok) {
11422
- 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
+ }));
11423
12486
  }
11424
12487
  accountId = acctId;
11425
12488
  token = apiTok;
@@ -11432,7 +12495,9 @@ function createR2Provisioner() {
11432
12495
  return err(
11433
12496
  HaiDeployError.AUTH_FAILED,
11434
12497
  deployM("deploy_authFailed", {
11435
- params: { error: error instanceof Error ? error.message : String(error) }
12498
+ params: {
12499
+ error: error instanceof Error ? error.message : String(error)
12500
+ }
11436
12501
  }),
11437
12502
  error
11438
12503
  );
@@ -11460,7 +12525,12 @@ function createR2Provisioner() {
11460
12525
  }
11461
12526
  );
11462
12527
  if (!createRes.ok && createRes.status !== 409) {
11463
- 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
+ }));
11464
12534
  }
11465
12535
  logger9.info("R2 bucket provisioned", { bucketName });
11466
12536
  return ok({
@@ -11508,13 +12578,20 @@ function createResendProvisioner() {
11508
12578
  try {
11509
12579
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
11510
12580
  if (!apiToken || !credentials.from) {
11511
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "apiKey, from" } }));
12581
+ throw new Error(deployM("deploy_credentialMissing", {
12582
+ params: { fields: "apiKey, from" }
12583
+ }));
11512
12584
  }
11513
12585
  const res = await fetch(`${RESEND_API}/domains`, {
11514
12586
  headers: { Authorization: `Bearer ${apiToken}` }
11515
12587
  });
11516
12588
  if (!res.ok) {
11517
- 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
+ }));
11518
12595
  }
11519
12596
  token = apiToken;
11520
12597
  from = credentials.from;
@@ -11525,7 +12602,9 @@ function createResendProvisioner() {
11525
12602
  return err(
11526
12603
  HaiDeployError.AUTH_FAILED,
11527
12604
  deployM("deploy_authFailed", {
11528
- params: { error: error instanceof Error ? error.message : String(error) }
12605
+ params: {
12606
+ error: error instanceof Error ? error.message : String(error)
12607
+ }
11529
12608
  }),
11530
12609
  error
11531
12610
  );
@@ -11544,7 +12623,18 @@ function createResendProvisioner() {
11544
12623
  provisionerName: "resend",
11545
12624
  resourceStatus: "reused",
11546
12625
  envVars: {
11547
- 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
+ ])
11548
12638
  },
11549
12639
  resourceInfo: "resend:verify-only"
11550
12640
  });
@@ -11566,7 +12656,12 @@ async function upstashFetch(email, apiKey, path, options) {
11566
12656
  }
11567
12657
  });
11568
12658
  if (!response.ok) {
11569
- 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
+ }));
11570
12665
  }
11571
12666
  return response.json();
11572
12667
  }
@@ -11583,7 +12678,9 @@ async function getUpstashDatabase(email, apiKey, databaseId) {
11583
12678
  function buildUpstashProvisionResult(database, resourceStatus) {
11584
12679
  const databaseId = database.database_id ?? "";
11585
12680
  if (!databaseId || !database.endpoint || !database.password || !database.port) {
11586
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
12681
+ throw new Error(deployM("deploy_provisionNoResult", {
12682
+ params: { service: "Upstash" }
12683
+ }));
11587
12684
  }
11588
12685
  return {
11589
12686
  serviceType: "cache",
@@ -11607,7 +12704,9 @@ function createUpstashProvisioner() {
11607
12704
  const userEmail = credentials.email ?? "";
11608
12705
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
11609
12706
  if (!userEmail || !userKey) {
11610
- 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
+ }));
11611
12710
  }
11612
12711
  await listUpstashDatabases(userEmail, userKey);
11613
12712
  email = userEmail;
@@ -11619,7 +12718,9 @@ function createUpstashProvisioner() {
11619
12718
  return err(
11620
12719
  HaiDeployError.AUTH_FAILED,
11621
12720
  deployM("deploy_authFailed", {
11622
- params: { error: error instanceof Error ? error.message : String(error) }
12721
+ params: {
12722
+ error: error instanceof Error ? error.message : String(error)
12723
+ }
11623
12724
  }),
11624
12725
  error
11625
12726
  );
@@ -11640,12 +12741,17 @@ function createUpstashProvisioner() {
11640
12741
  if (existingDatabase !== null) {
11641
12742
  const databaseId = existingDatabase.database_id ?? "";
11642
12743
  if (!databaseId) {
11643
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
12744
+ throw new Error(deployM("deploy_provisionNoResult", {
12745
+ params: { service: "Upstash" }
12746
+ }));
11644
12747
  }
11645
12748
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
11646
12749
  const detail = await getUpstashDatabase(email, apiKey, databaseId);
11647
12750
  logger11.info("Upstash Redis reused", { databaseId, databaseName });
11648
- return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
12751
+ return ok(buildUpstashProvisionResult({
12752
+ ...existingDatabase,
12753
+ ...detail
12754
+ }, "reused"));
11649
12755
  }
11650
12756
  const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
11651
12757
  method: "POST",
@@ -11665,7 +12771,9 @@ function createUpstashProvisioner() {
11665
12771
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
11666
12772
  projectName: appName,
11667
12773
  stage: "provision:cache",
11668
- resources: [{ serviceType: "cache", provisionerName: "upstash", ...resource }]
12774
+ resources: [
12775
+ { serviceType: "cache", provisionerName: "upstash", ...resource }
12776
+ ]
11669
12777
  });
11670
12778
  }
11671
12779
  return err(
@@ -11699,7 +12807,9 @@ function createProviderByType(type) {
11699
12807
  case "vercel":
11700
12808
  return createVercelProvider();
11701
12809
  default:
11702
- throw new Error(deployM("deploy_unsupportedProviderType", { params: { type } }));
12810
+ throw new Error(deployM("deploy_unsupportedProviderType", {
12811
+ params: { type }
12812
+ }));
11703
12813
  }
11704
12814
  }
11705
12815
  function createProvisionerByName(provisioner) {
@@ -11715,7 +12825,9 @@ function createProvisionerByName(provisioner) {
11715
12825
  case "aliyun":
11716
12826
  return createAliyunProvisioner();
11717
12827
  default:
11718
- throw new Error(deployM("deploy_unsupportedProvisionerType", { params: { type: provisioner } }));
12828
+ throw new Error(deployM("deploy_unsupportedProvisionerType", {
12829
+ params: { type: provisioner }
12830
+ }));
11719
12831
  }
11720
12832
  }
11721
12833
  function extractCredentials(serviceConfig) {
@@ -11748,8 +12860,12 @@ function mergeProvisionEnv(results, scan, options) {
11748
12860
  return item.service === prov.serviceType && (!mode || mode === "default");
11749
12861
  });
11750
12862
  if (targets.length) {
11751
- for (const target of targets)
11752
- 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
+ }
11753
12869
  } else {
11754
12870
  reachProviders.push(provider);
11755
12871
  }
@@ -11771,7 +12887,9 @@ var deploy = {
11771
12887
  logger12.warn("Deploy init already in progress, skipping concurrent call");
11772
12888
  return err(
11773
12889
  HaiDeployError.CONFIG_ERROR,
11774
- deployM("deploy_configError", { params: { error: "init already in progress" } })
12890
+ deployM("deploy_configError", {
12891
+ params: { error: "init already in progress" }
12892
+ })
11775
12893
  );
11776
12894
  }
11777
12895
  initInProgress = true;
@@ -11786,7 +12904,9 @@ var deploy = {
11786
12904
  logger12.error("Deploy config validation failed", { error: parseResult.error.message });
11787
12905
  return err(
11788
12906
  HaiDeployError.CONFIG_ERROR,
11789
- deployM("deploy_configError", { params: { error: parseResult.error.message } }),
12907
+ deployM("deploy_configError", {
12908
+ params: { error: parseResult.error.message }
12909
+ }),
11790
12910
  parseResult.error
11791
12911
  );
11792
12912
  }
@@ -11834,7 +12954,9 @@ var deploy = {
11834
12954
  currentConfig = parsed;
11835
12955
  logger12.info("Deploy module initialized", {
11836
12956
  provider: parsed.provider.type,
11837
- provisioners: [...currentProvisioners.keys()]
12957
+ provisioners: [
12958
+ ...currentProvisioners.keys()
12959
+ ]
11838
12960
  });
11839
12961
  return ok(void 0);
11840
12962
  } catch (error) {
@@ -11846,7 +12968,9 @@ var deploy = {
11846
12968
  return err(
11847
12969
  HaiDeployError.CONFIG_ERROR,
11848
12970
  deployM("deploy_configError", {
11849
- params: { error: error instanceof Error ? error.message : String(error) }
12971
+ params: {
12972
+ error: error instanceof Error ? error.message : String(error)
12973
+ }
11850
12974
  }),
11851
12975
  error
11852
12976
  );
@@ -11900,7 +13024,12 @@ var deploy = {
11900
13024
  resources: [
11901
13025
  ...results.map(({ envVars: _envVars, ...resource }) => resource),
11902
13026
  ...getDeployRecovery(provResult.error)?.resources ?? [
11903
- { serviceType, provisionerName: provisioner.name, resourceInfo: projectName, resourceStatus: "unknown" }
13027
+ {
13028
+ serviceType,
13029
+ provisionerName: provisioner.name,
13030
+ resourceInfo: projectName,
13031
+ resourceStatus: "unknown"
13032
+ }
11904
13033
  ]
11905
13034
  ]
11906
13035
  });
@@ -11938,7 +13067,9 @@ var deploy = {
11938
13067
  connectionVariables.add(match[1]);
11939
13068
  }
11940
13069
  } catch {
11941
- 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
+ }));
11942
13073
  }
11943
13074
  }
11944
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")));
@@ -11946,7 +13077,9 @@ var deploy = {
11946
13077
  options = {
11947
13078
  ...options,
11948
13079
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
11949
- ...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
+ } : {}
11950
13083
  };
11951
13084
  if (options.selectedServices) {
11952
13085
  options.serviceModes = {
@@ -11982,7 +13115,10 @@ var deploy = {
11982
13115
  return err(provResults.error);
11983
13116
  }
11984
13117
  recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
11985
- allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
13118
+ allEnvVars = {
13119
+ ...allEnvVars,
13120
+ ...mergeProvisionEnv(provResults.data, scan, options)
13121
+ };
11986
13122
  if (options?.environment) {
11987
13123
  const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
11988
13124
  if (!saved.success)
@@ -12058,7 +13194,16 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
12058
13194
  }
12059
13195
  provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
12060
13196
  }
12061
- 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
+ });
12062
13207
  }
12063
13208
  z.enum(["sqlite", "postgresql", "mysql"]);
12064
13209
  var ReldbOperationLogConfigSchema = z.object({
@@ -12091,7 +13236,12 @@ var MysqlPoolConfigSchema = z.object({
12091
13236
  }).strict();
12092
13237
  var SslConfigSchema = z.union([
12093
13238
  z.boolean(),
12094
- z.enum(["require", "prefer", "allow", "disable"]),
13239
+ z.enum([
13240
+ "require",
13241
+ "prefer",
13242
+ "allow",
13243
+ "disable"
13244
+ ]),
12095
13245
  z.record(z.string(), z.unknown())
12096
13246
  ]);
12097
13247
  var MysqlSslConfigSchema = z.union([
@@ -12107,7 +13257,9 @@ var MysqlSslConfigSchema = z.union([
12107
13257
  cert: z.string().min(1).optional(),
12108
13258
  /** MySQL 客户端私钥,PEM 内容;必须同时提供 cert */
12109
13259
  key: z.string().min(1).optional()
12110
- }).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
+ })
12111
13263
  ]);
12112
13264
  var SqliteOptionsSchema = z.object({
12113
13265
  /** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
@@ -12169,7 +13321,10 @@ var PostgresqlConfigSchema = NetworkConfigSchema.extend({
12169
13321
  var MysqlConfigSchema = NetworkConfigSchema.extend({
12170
13322
  type: z.literal("mysql"),
12171
13323
  /** 网络数据库URL;MySQL空串表示使用分字段,非空仅mysql://且拒绝query/fragment,优先于host/port/database/user/password */
12172
- 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(),
12173
13328
  /** 数据库:SQLite文件路径或:memory:;PostgreSQL必填名称;MySQL无URL时必填,URL模式从URL读取 */
12174
13329
  database: z.string().min(1).optional(),
12175
13330
  /** TLS:PostgreSQL使用其模式;MySQL仅false或严格对象,空对象默认校验证书链和DNS身份 */
@@ -12179,7 +13334,9 @@ var MysqlConfigSchema = NetworkConfigSchema.extend({
12179
13334
  /** MySQL 特定选项 */
12180
13335
  mysql: MysqlOptionsSchema.optional(),
12181
13336
  ...OperationLogConfigFieldSchema
12182
- }).refine((config) => Boolean(config.url) || Boolean(config.database), { path: ["database"] });
13337
+ }).refine((config) => Boolean(config.url) || Boolean(config.database), {
13338
+ path: ["database"]
13339
+ });
12183
13340
  var ReldbConfigSchema = z.discriminatedUnion("type", [
12184
13341
  SqliteConfigSchema,
12185
13342
  PostgresqlConfigSchema,
@@ -12189,8 +13346,12 @@ function canKeepMiddleware(requirement, backend, environment) {
12189
13346
  const privatePath = join(environment.envDir, `.env.${environment.environment}.local`);
12190
13347
  let targetValues = environment.targetValues;
12191
13348
  try {
12192
- if (existsSync(privatePath))
12193
- 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
+ }
12194
13355
  } catch {
12195
13356
  return false;
12196
13357
  }
@@ -12200,19 +13361,31 @@ function canKeepMiddleware(requirement, backend, environment) {
12200
13361
  function empty(value2) {
12201
13362
  if (Array.isArray(value2))
12202
13363
  return value2.map(empty);
12203
- if (value2 && typeof value2 === "object")
12204
- 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
+ }
12205
13370
  return void 0;
12206
13371
  }
12207
13372
  let seed = empty(requirement.config);
12208
- for (const part of [...requirement.configPath].reverse())
13373
+ for (const part of [...requirement.configPath].reverse()) {
12209
13374
  seed = /^\d+$/.test(part) ? Array.from({ length: Number(part) + 1 }, (_, index) => index === Number(part) ? seed : null) : { [part]: seed };
12210
- 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
+ });
12211
13383
  if (!resolved.success)
12212
13384
  return false;
12213
13385
  let value = resolved.data;
12214
- for (const part of requirement.configPath)
13386
+ for (const part of requirement.configPath) {
12215
13387
  value = value && typeof value === "object" ? value[part] : void 0;
13388
+ }
12216
13389
  if (!value || typeof value !== "object" || Array.isArray(value))
12217
13390
  return false;
12218
13391
  const target = value;
@@ -12229,30 +13402,39 @@ function validateMiddlewareField(options) {
12229
13402
  }
12230
13403
  function middlewareFieldProblem(options) {
12231
13404
  const { value, field: field2, backend } = options;
12232
- if (value === "")
13405
+ if (value === "") {
12233
13406
  return field2.required ? deployM("deploy_middlewareRequired") : void 0;
12234
- if (field2.kind === "port")
12235
- 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
+ }
12236
13413
  if (field2.kind === "number") {
12237
13414
  const min = field2.minimum ?? 0;
12238
13415
  const max = field2.maximum ?? Number.MAX_SAFE_INTEGER;
12239
- 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
+ });
12240
13419
  }
12241
- if (field2.kind === "boolean")
13420
+ if (field2.kind === "boolean") {
12242
13421
  return ["true", "false"].includes(value) ? void 0 : deployM("deploy_middlewareBoolean");
13422
+ }
12243
13423
  if (backend === "s3" && field2.name === "endpoint") {
12244
13424
  const endpoint = URL.parse(value);
12245
13425
  return endpoint?.hostname && ["http:", "https:"].includes(endpoint.protocol) ? void 0 : deployM("deploy_middlewareS3Endpoint");
12246
13426
  }
12247
- if (field2.kind === "url")
13427
+ if (field2.kind === "url") {
12248
13428
  return URL.canParse(value) ? void 0 : deployM("deploy_middlewareUrl");
13429
+ }
12249
13430
  if (field2.name === "ssl") {
12250
13431
  try {
12251
13432
  const document = parseDocument(value);
12252
13433
  if (document.errors.length)
12253
13434
  return deployM("deploy_middlewareTlsSyntax");
12254
- if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success)
13435
+ if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success) {
12255
13436
  return deployM(backend === "mysql" ? "deploy_middlewareMysqlTls" : "deploy_middlewarePostgresTls");
13437
+ }
12256
13438
  } catch {
12257
13439
  return deployM("deploy_middlewareTlsSyntax");
12258
13440
  }
@@ -12274,8 +13456,11 @@ function parseMiddlewareField(options) {
12274
13456
  if (options.managed && (options.field.secret || options.field.managed) && options.value === "")
12275
13457
  return ok(void 0);
12276
13458
  const problem = middlewareFieldProblem(options);
12277
- if (problem)
12278
- 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
+ }
12279
13464
  const { value, field: field2 } = options;
12280
13465
  if (value === "")
12281
13466
  return ok(void 0);
@@ -12293,8 +13478,13 @@ function saveMiddlewareEnvironment(environment, options) {
12293
13478
  const configuration = requirement.service === "ldap" ? options.configuration : { ...options.configuration, type: backend };
12294
13479
  if (requirement.service === "db") {
12295
13480
  const validated = ReldbConfigSchema.safeParse(configuration);
12296
- if (!validated.success)
12297
- 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
+ }
12298
13488
  }
12299
13489
  if (requirement.service === "ldap") {
12300
13490
  updates.HAI_IAM_LOGIN_LDAP = backend !== "disabled";
@@ -12308,17 +13498,26 @@ function saveMiddlewareEnvironment(environment, options) {
12308
13498
  mkdirSync(directory, { recursive: true });
12309
13499
  const path = join(directory, `_${requirement.configName}.yml`);
12310
13500
  const lineCounter = new LineCounter();
12311
- const document = parseDocument(existsSync(path) ? readFileSync(path, "utf8") : "{}\n", { lineCounter, prettyErrors: false });
12312
- if (document.errors.length)
12313
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
12314
- 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)) {
12315
13511
  document.setIn(requirement.configPath, document.createNode(requirement.config.name ? { name: requirement.config.name, type: backend } : {}));
13512
+ }
12316
13513
  if (requirement.service === "ldap" && !document.hasIn(["login", "ldap"]))
12317
13514
  document.setIn(["login", "ldap"], false);
12318
13515
  const content = document.toString();
12319
- if (!existsSync(path) || readFileSync(path, "utf8") !== content)
12320
- writeFileSync(path, content);
12321
- 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
+ });
12322
13521
  } catch {
12323
13522
  return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
12324
13523
  }
@@ -12326,46 +13525,253 @@ function saveMiddlewareEnvironment(environment, options) {
12326
13525
 
12327
13526
  // src/deploy-middleware.ts
12328
13527
  var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
12329
- 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
+ }
12330
13534
  var DEPLOY_MIDDLEWARE = {
12331
13535
  db: [
12332
- { 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
+ },
12333
13544
  ...["postgresql", "mysql"].map((backend) => ({
12334
13545
  backend,
12335
13546
  label: backend === "mysql" ? "MySQL" : "PostgreSQL",
12336
13547
  purpose: "deploy_purposeDatabase",
12337
13548
  provisioner: backend === "postgresql" ? "neon" : void 0,
12338
13549
  cloudFields: backend === "postgresql" ? provisionerFields("neon") : void 0,
12339
- 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
+ ]
12340
13559
  }))
12341
13560
  ],
12342
13561
  cache: [
12343
- { backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
12344
- { 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
+ }
12345
13589
  ],
12346
13590
  storage: [
12347
- { backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required("root", "./data/storage")] },
12348
- { 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
+ }
12349
13632
  ],
12350
13633
  vecdb: [
12351
- { backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required("path", "./data/vectors")] },
12352
- { backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
12353
- { 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)] },
12354
- { 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
+ }
12355
13672
  ],
12356
13673
  ldap: [
12357
- { backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
12358
- { 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
+ }
12359
13703
  ],
12360
13704
  email: [
12361
- { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
12362
- { 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")] },
12363
- { 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
+ }
12364
13738
  ],
12365
13739
  sms: [
12366
- { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
12367
- { 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")] },
12368
- { 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
+ }
12369
13775
  ]
12370
13776
  };
12371
13777
  function describeMiddlewareBackend(backend) {
@@ -12375,8 +13781,15 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12375
13781
  if ("projectName" in options && options.projectName !== void 0)
12376
13782
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
12377
13783
  const parsed = DeployConfigSchema.safeParse(input);
12378
- if (!parsed.success)
12379
- 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
+ }
12380
13793
  const config = parsed.data;
12381
13794
  const scan = await scanApp(appDir, { environment: options.environment ?? config.environment, envDir: options.envDir });
12382
13795
  if (!scan.success)
@@ -12403,21 +13816,51 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12403
13816
  const compose = validateComposeServices(config.container, artifact);
12404
13817
  if (!compose.success)
12405
13818
  return compose;
12406
- images = [.../* @__PURE__ */ new Set([artifact.primaryService, ...artifact.hasWeb ? ["web"] : []])];
13819
+ images = [
13820
+ .../* @__PURE__ */ new Set([
13821
+ artifact.primaryService,
13822
+ ...artifact.hasWeb ? ["web"] : []
13823
+ ])
13824
+ ];
12407
13825
  } else {
12408
13826
  const file = join(root, "hai.json");
12409
- 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;
12410
- images = project?.build?.images?.map((image) => image.name) ?? [app.isSvelteKit ? "web" : "serv"];
12411
- }
12412
- const middlewares = (app.middlewares ?? []).filter((item) => !["sqlite", "local", "memory", "disabled", "console", "lancedb"].includes(item.backend));
12413
- const available = [.../* @__PURE__ */ new Set([...images, ...app.requiredServices])];
12414
- 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
+ ];
12415
13853
  if (!selected.length || new Set(selected).size !== selected.length || selected.some((name) => !available.includes(name)))
12416
13854
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
12417
13855
  if (artifact && selected.some((name) => images.includes(name) && !artifact.images.some((image) => image.name === name)))
12418
13856
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
12419
13857
  const remoteDirectory = provider.type === "docker-ssh" ? `${provider.remote.baseDir}/${projectName}` : void 0;
12420
- 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
+ };
12421
13864
  if (options.inspectRemote) {
12422
13865
  if (provider.type !== "docker-ssh" || !provider.ssh || !remoteDirectory)
12423
13866
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
@@ -12433,13 +13876,27 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12433
13876
  baseline.release = release || void 0;
12434
13877
  }
12435
13878
  }
12436
- 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);
12437
13885
  const configDirectory = join(directory, "config");
12438
13886
  if (existsSync(configDirectory))
12439
13887
  configFiles.push(...readdirSync(configDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)).map((entry) => join(configDirectory, entry.name)).sort());
12440
- 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
+ };
12441
13892
  const effective = resolveSidecarOptions(app, { ...options, selectedServices: selected, serviceModes });
12442
- 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
+ };
12443
13900
  const provisionEnv = {};
12444
13901
  const provisioners = /* @__PURE__ */ new Map();
12445
13902
  for (const service of Object.keys(provisionKeys)) {
@@ -12451,7 +13908,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12451
13908
  }
12452
13909
  const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
12453
13910
  const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
12454
- 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");
12455
13912
  if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
12456
13913
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
12457
13914
  if (gateway) {
@@ -12470,17 +13927,54 @@ async function createDeploymentPlan(appDir, input, options = {}) {
12470
13927
  root,
12471
13928
  serviceDir: directory,
12472
13929
  environment: environment.data.environment,
12473
- 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 },
12474
- 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,
12475
13946
  selected,
12476
13947
  retained: available.filter((name) => !selected.includes(name)),
12477
- 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] : [])] : [],
12478
- 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
+ ],
12479
13956
  logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
12480
- 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) })),
12481
- 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
+ },
12482
13971
  baseline,
12483
- 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
+ }
12484
13978
  });
12485
13979
  } catch {
12486
13980
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
@@ -12491,7 +13985,11 @@ var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(v
12491
13985
  var importSchema = z.strictObject({
12492
13986
  version: z.literal(1),
12493
13987
  projectName: nameSchema,
12494
- 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
+ }),
12495
13993
  services: z.array(z.strictObject({
12496
13994
  service: z.enum(["db", "vecdb", "storage", "ldap"]),
12497
13995
  container: nameSchema,
@@ -12505,7 +14003,12 @@ var importSchema = z.strictObject({
12505
14003
  bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
12506
14004
  })).min(1).max(4)
12507
14005
  });
12508
- 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
+ };
12509
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;
12510
14013
  try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
12511
14014
  if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
@@ -12537,11 +14040,24 @@ async function importDeploymentSecrets(options) {
12537
14040
  throw new Error("input");
12538
14041
  const input = privateFile(resolve(options.inputFile));
12539
14042
  const parsed = importSchema.safeParse(JSON.parse(input));
12540
- if (!parsed.success)
12541
- 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
+ }
12542
14056
  const config = parsed.data;
12543
14057
  const project = `hai-${config.projectName}`;
12544
- 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();
12545
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])) {
12546
14062
  throw new Error("input");
12547
14063
  }
@@ -12581,23 +14097,89 @@ async function importDeploymentSecrets(options) {
12581
14097
  const application = await inspectContainer(options.runtime, helper2, secrets);
12582
14098
  if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
12583
14099
  throw new Error("identity");
12584
- 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
+ });
12585
14119
  verified = result.success && result.data.stdout.trim() === "VERIFIED";
12586
14120
  } else {
12587
- 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];
12588
- const result = await runCommand(options.runtime, args, { input: service.kind === "ldap" ? password : `${password}
12589
- `, 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
+ });
12590
14152
  verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
12591
14153
  if (verified && service.kind === "mysql") {
12592
- const rootCheck = await runCommand(options.runtime, [...args.slice(0, -3), "root", service.database, service.container], { input: `${password}
12593
- `, 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
+ });
12594
14166
  verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
12595
14167
  const invalidPassword = `hai-import-invalid-${randomUUID()}`;
12596
14168
  for (const username of [service.username, "root"]) {
12597
14169
  if (!verified)
12598
14170
  break;
12599
- const negative = await runCommand(options.runtime, [...args.slice(0, -3), username, service.database, service.container], { input: `${invalidPassword}
12600
- `, 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
+ });
12601
14183
  verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
12602
14184
  }
12603
14185
  }
@@ -12607,8 +14189,15 @@ async function importDeploymentSecrets(options) {
12607
14189
  phase = "identity";
12608
14190
  }
12609
14191
  phase = "state";
12610
- 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}`));
12611
- 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
+ ];
12612
14201
  let changed = false;
12613
14202
  if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
12614
14203
  const existing = readImportedState(privateFile(stateFile));
@@ -12621,9 +14210,24 @@ async function importDeploymentSecrets(options) {
12621
14210
  throw new Error("lock");
12622
14211
  changed = true;
12623
14212
  }
12624
- 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
+ });
12625
14220
  } catch {
12626
- 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
+ });
12627
14231
  } finally {
12628
14232
  await lock?.release();
12629
14233
  }
@@ -12659,19 +14263,26 @@ function isSymbolicLink(path) {
12659
14263
  }
12660
14264
  }
12661
14265
  function privateFile(path) {
12662
- const stat = lstatSync(path);
12663
- if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || process.platform !== "win32" && (stat.mode & 63 || process.getuid?.() !== 0 && stat.uid !== process.getuid?.()))
12664
- throw new Error("private-file");
12665
- return readFileSync(path, "utf8");
14266
+ return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
12666
14267
  }
12667
14268
  async function inspectContainer(runtime, container, secrets) {
12668
- 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 });
12669
14275
  if (!result.success || result.data.stdoutTruncated)
12670
14276
  throw new Error("identity");
12671
14277
  const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
12672
14278
  const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
12673
- const mounts = z.array(z.object({ Type: z.string(), Name: z.string().optional() })).parse(JSON.parse(mountsText));
12674
- 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) ?? {});
12675
14286
  const dockerProject = labels["com.docker.compose.project"] || void 0;
12676
14287
  const podmanProject = labels["io.podman.compose.project"] || void 0;
12677
14288
  const dockerService = labels["com.docker.compose.service"] || void 0;
@@ -12679,11 +14290,27 @@ async function inspectContainer(runtime, container, secrets) {
12679
14290
  if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
12680
14291
  throw new Error("identity");
12681
14292
  }
12682
- 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
+ };
12683
14300
  }
12684
14301
  async function acquireImportLock(path, root) {
12685
14302
  return new Promise((resolveLock, reject) => {
12686
- 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
+ });
12687
14314
  let acquired = false;
12688
14315
  let exited = false;
12689
14316
  let output = "";