@h-ai/cli 1.0.0-beta.1 → 1.0.0-beta.3

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.
Files changed (74) hide show
  1. package/README.en.md +20 -8
  2. package/README.ja.md +20 -8
  3. package/README.md +20 -8
  4. package/dist/{chunk-M7HN2OE3.js → chunk-K6RWTTHD.js} +573 -106
  5. package/dist/chunk-K6RWTTHD.js.map +1 -0
  6. package/dist/{chunk-AKV2CQZV.js → chunk-SWEU7KUE.js} +71 -59
  7. package/dist/chunk-SWEU7KUE.js.map +1 -0
  8. package/dist/{chunk-3JCZO34U.js → chunk-YVNNMC4H.js} +365 -137
  9. package/dist/chunk-YVNNMC4H.js.map +1 -0
  10. package/dist/cli-config-templates-LUJGDQNW.js +5 -0
  11. package/dist/{cli-config-templates-RCWQMHZN.js.map → cli-config-templates-LUJGDQNW.js.map} +1 -1
  12. package/dist/{cli-skill-templates-PN4FLGBC.js → cli-skill-templates-VV2XWYON.js} +3 -3
  13. package/dist/{cli-skill-templates-PN4FLGBC.js.map → cli-skill-templates-VV2XWYON.js.map} +1 -1
  14. package/dist/index.js +118 -209
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -1
  17. package/dist/{src-JRKSWJ2Q.js → src-IR5AU43K.js} +3 -3
  18. package/dist/src-IR5AU43K.js.map +1 -0
  19. package/package.json +11 -11
  20. package/templates/apps/api/.dockerignore.hbs +10 -0
  21. package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +20 -1
  22. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +6 -1
  23. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +1 -1
  24. package/templates/apps/api/images/serv.Dockerfile.hbs +3 -2
  25. package/templates/apps/fullstack/.dockerignore.hbs +10 -0
  26. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +1 -1
  27. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +16 -2
  28. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +6 -2
  29. package/templates/apps/fullstack/images/web.Dockerfile.hbs +6 -1
  30. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +1 -1
  31. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +10 -7
  32. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +14 -7
  33. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  34. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +1 -1
  35. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +7 -1
  36. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +1 -1
  37. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +1 -0
  38. package/templates/apps/website/scripts/static-website.mjs.hbs +3 -3
  39. package/templates/base/.dockerignore.hbs +10 -0
  40. package/templates/base/images/web.Dockerfile.hbs +4 -2
  41. package/templates/base/package.json.hbs +1 -1
  42. package/templates/base/src/hooks.server.ts.hbs +9 -7
  43. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +3 -1
  44. package/templates/skills/AGENTS.md +2 -0
  45. package/templates/skills/bridges/admin/AGENTS.md +2 -0
  46. package/templates/skills/bridges/api/AGENTS.md +2 -0
  47. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +2 -0
  48. package/templates/skills/bridges/h5/AGENTS.md +2 -0
  49. package/templates/skills/bridges/mobile-app/AGENTS.md +2 -0
  50. package/templates/skills/bridges/website/AGENTS.md +2 -0
  51. package/templates/skills/hai-ai/guide.md +5 -3
  52. package/templates/skills/hai-app-create/SKILL.md +1 -1
  53. package/templates/skills/hai-app-create/collaboration.md +6 -1
  54. package/templates/skills/hai-app-create/development.md +10 -4
  55. package/templates/skills/hai-app-create/reference.md +4 -2
  56. package/templates/skills/hai-app-review/SKILL.md +2 -0
  57. package/templates/skills/hai-app-tests/SKILL.md +2 -0
  58. package/templates/skills/hai-build/SKILL.md +5 -5
  59. package/templates/skills/hai-build/paths.md +1 -1
  60. package/templates/skills/hai-build/reference.md +1 -1
  61. package/templates/skills/hai-change-plan/SKILL.md +46 -0
  62. package/templates/skills/hai-deploy/SKILL.md +16 -4
  63. package/templates/skills/hai-deploy/middleware.md +4 -2
  64. package/templates/skills/hai-feedback/SKILL.md +1 -1
  65. package/templates/skills/hai-pr-review/SKILL.md +2 -0
  66. package/templates/skills/hai-serv/reference.md +5 -8
  67. package/templates/skills/hai-spec-implement/SKILL.md +2 -0
  68. package/templates/skills/hai-storage/SKILL.md +2 -2
  69. package/templates/skills/hai-storage/reference.md +1 -1
  70. package/dist/chunk-3JCZO34U.js.map +0 -1
  71. package/dist/chunk-AKV2CQZV.js.map +0 -1
  72. package/dist/chunk-M7HN2OE3.js.map +0 -1
  73. package/dist/cli-config-templates-RCWQMHZN.js +0 -5
  74. package/dist/src-JRKSWJ2Q.js.map +0 -1
@@ -1,17 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { __commonJS, __export, __toESM } from './chunk-ZSDEVXZ7.js';
3
- import { createHash, randomUUID, randomBytes } from 'crypto';
4
- import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, readdirSync, realpathSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync } from 'fs';
3
+ import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
4
+ import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, readdirSync, realpathSync, chmodSync, mkdirSync, statSync, mkdtempSync, copyFileSync, renameSync, rmSync, createWriteStream } from 'fs';
5
5
  import { resolve, extname, join, dirname, relative, basename, sep } from 'path';
6
- import { core, err, ok, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
6
+ import { core, err, ok, CoreConfigSchema, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
7
7
  import process6 from 'process';
8
8
  import { parseEnv } from 'util';
9
9
  import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
10
10
  import { homedir, tmpdir } from 'os';
11
+ import { request } from 'https';
12
+ import { rootCertificates } from 'tls';
13
+ import { Transform } from 'stream';
14
+ import { pipeline } from 'stream/promises';
11
15
  import { spawn } from 'child_process';
12
16
  import { readdir, readFile } from 'fs/promises';
13
17
  import { setTimeout as setTimeout$1 } from 'timers/promises';
14
18
  import { Buffer } from 'buffer';
19
+ import { isIP } from 'net';
15
20
 
16
21
  // ../../node_modules/jsbn/index.js
17
22
  var require_jsbn = __commonJS({
@@ -7878,7 +7883,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
7878
7883
  var NONE = 0;
7879
7884
  var ASSUMED = 1;
7880
7885
  var PROVEN = 2;
7881
- function isRecursive(inst, stack, resolve8) {
7886
+ function isRecursive(inst, stack, resolve9) {
7882
7887
  const cached2 = recursive.get(inst);
7883
7888
  if (cached2 !== void 0)
7884
7889
  return cached2 ? PROVEN : NONE;
@@ -7888,7 +7893,7 @@ function isRecursive(inst, stack, resolve8) {
7888
7893
  let result = NONE;
7889
7894
  const check2 = (child) => {
7890
7895
  if (result !== PROVEN && child?._zod) {
7891
- const answer = isRecursive(child, stack, resolve8);
7896
+ const answer = isRecursive(child, stack, resolve9);
7892
7897
  if (answer > result)
7893
7898
  result = answer;
7894
7899
  }
@@ -7899,7 +7904,7 @@ function isRecursive(inst, stack, resolve8) {
7899
7904
  const desc = Object.getOwnPropertyDescriptor(sh, key);
7900
7905
  if (!desc.enumerable)
7901
7906
  continue;
7902
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve8) : NONE;
7907
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve9) : NONE;
7903
7908
  if (child > answer)
7904
7909
  answer = child;
7905
7910
  }
@@ -7963,7 +7968,7 @@ function isRecursive(inst, stack, resolve8) {
7963
7968
  break;
7964
7969
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
7965
7970
  case "lazy": {
7966
- const inner = def._cachedInner ?? (resolve8 ? inst._zod.innerType : void 0);
7971
+ const inner = def._cachedInner ?? (resolve9 ? inst._zod.innerType : void 0);
7967
7972
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
7968
7973
  break;
7969
7974
  }
@@ -22612,9 +22617,9 @@ var en_US_default = {
22612
22617
  deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
22613
22618
  deploy_webBaseConflict: "The website mount path conflicts with an API or health endpoint. Choose a different PUBLIC_BASE_PATH.",
22614
22619
  deploy_storageProxyInvalid: "Cannot generate the storage proxy. Use a valid DNS bucket name whose path does not overlap the website, API, or health endpoints.",
22615
- deploy_storagePublicEndpointInvalid: "Invalid storage.publicEndpoint. Use a browser-accessible HTTP(S) origin without a bucket, path, credentials, query, or fragment.",
22620
+ deploy_storagePublicEndpointInvalid: "Invalid storage.publicEndpoint. Use a browser-accessible HTTP(S) address with an optional valid proxy path, without a bucket, credentials, query or fragment.",
22616
22621
  deploy_storageBackendRequired: "A static browser app cannot deploy server-side storage credentials. Configure Storage in Serv or SvelteKit and obtain presigned links through its API.",
22617
- deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
22622
+ deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target image, select valid --artifact-dir, or use pnpm run deploy --build.",
22618
22623
  deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
22619
22624
  deploy_sshPasswordInvalid: "SSH passwords must be nonempty, contain no CR/LF/NUL, and be at most 1023 UTF-8 bytes.",
22620
22625
  deploy_sshAskpassFailed: "Could not prepare the managed SSH password helper. Check OpenSSH and Windows PowerShell 5.",
@@ -22664,7 +22669,9 @@ var en_US_default = {
22664
22669
  deploy_secretImportFailed: "Legacy credential import was rejected during {phase}; check private input permissions, project/volume identity, live credentials and the deployment lock. No services were updated; inspect the protected state file if commit is unconfirmed",
22665
22670
  deploy_projectNameRequired: "The root package.json.name is missing or cannot be normalized into an image and deployment resource identity.",
22666
22671
  deploy_projectNameOverrideRemoved: "Deployment identity comes from the root package.json.name; remove the obsolete projectName override.",
22667
- deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle."
22672
+ deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle.",
22673
+ deploy_httpsPortsInvalid: "HTTP and HTTPS ports must differ; Let\u2019s Encrypt HTTP-01 requires host port 80.",
22674
+ deploy_httpsCertificateInvalid: "Invalid HTTPS certificate. Check readable PEM files, matching unencrypted private key, validity dates and domain, then repackage."
22668
22675
  };
22669
22676
 
22670
22677
  // ../deploy/messages/ja-JP.json
@@ -22720,9 +22727,9 @@ var ja_JP_default = {
22720
22727
  deploy_webProxyInvalid: "API\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3001\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30B9\u30E9\u30C3\u30B7\u30E5\u306E\u307F\u3092\u542B\u3080\u7D76\u5BFE\u30D1\u30B9\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22721
22728
  deploy_webBaseConflict: "\u30B5\u30A4\u30C8\u306E\u30DE\u30A6\u30F3\u30C8\u30D1\u30B9\u304CAPI\u307E\u305F\u306F\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u3068\u7AF6\u5408\u3057\u3066\u3044\u307E\u3059\u3002\u5225\u306EPUBLIC_BASE_PATH\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22722
22729
  deploy_storageProxyInvalid: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30D7\u30ED\u30AD\u30B7\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002\u6709\u52B9\u306A DNS \u30D0\u30B1\u30C3\u30C8\u540D\u3092\u6307\u5B9A\u3057\u3001\u30B5\u30A4\u30C8\u3001API\u3001\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u306E\u30D1\u30B9\u3068\u306E\u91CD\u8907\u3092\u907F\u3051\u3066\u304F\u3060\u3055\u3044\u3002",
22723
- deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u304C\u7121\u52B9\u3067\u3059\u3002\u30D0\u30B1\u30C3\u30C8\u3001\u30D1\u30B9\u3001\u8A8D\u8A3C\u60C5\u5831\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u3092\u542B\u307E\u306A\u3044\u3001\u30D6\u30E9\u30A6\u30B6\u30FC\u304B\u3089\u5230\u9054\u53EF\u80FD\u306A HTTP(S) \u30AA\u30EA\u30B8\u30F3\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22730
+ deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u304C\u7121\u52B9\u3067\u3059\u3002\u30D6\u30E9\u30A6\u30B6\u30FC\u304B\u3089\u5230\u9054\u53EF\u80FD\u306A HTTP(S) \u30A2\u30C9\u30EC\u30B9\u306B\u6709\u52B9\u306A\u30D7\u30ED\u30AD\u30B7\u30D1\u30B9\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002\u30D0\u30B1\u30C3\u30C8\u3001\u8A8D\u8A3C\u60C5\u5831\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u306F\u542B\u3081\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002",
22724
22731
  deploy_storageBackendRequired: "\u9759\u7684\u30D6\u30E9\u30A6\u30B6\u30A2\u30D7\u30EA\u306B\u306F\u30B5\u30FC\u30D0\u30FC\u7528\u30B9\u30C8\u30EC\u30FC\u30B8\u8A8D\u8A3C\u60C5\u5831\u3092\u914D\u7F6E\u3067\u304D\u307E\u305B\u3093\u3002Serv \u307E\u305F\u306F SvelteKit \u3067 Storage \u3092\u8A2D\u5B9A\u3057\u3001API \u7D4C\u7531\u3067\u7F72\u540D\u4ED8\u304D\u30EA\u30F3\u30AF\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22725
- deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target docker \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001--artifact-dir \u3067\u6709\u52B9\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001pnpm run deploy --build \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22732
+ deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target image \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001--artifact-dir \u3067\u6709\u52B9\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001pnpm run deploy --build \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22726
22733
  deploy_sshRequired: "\u81EA\u52D5\u30C7\u30D7\u30ED\u30A4\u306B\u306FSSH\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u306A\u3044\u5834\u5408\u306F--bundle-only\u3067\u624B\u52D5\u8EE2\u9001\u7528\u306E\u30D0\u30F3\u30C9\u30EB\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22727
22734
  deploy_sshPasswordInvalid: "SSH\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u7A7A\u306B\u3067\u304D\u305A\u3001CR/LF/NUL\u3092\u542B\u3081\u3089\u308C\u307E\u305B\u3093\u3002UTF-8\u30671023\u30D0\u30A4\u30C8\u4EE5\u4E0B\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22728
22735
  deploy_sshAskpassFailed: "\u7BA1\u7406\u5BFE\u8C61\u306ESSH\u30D1\u30B9\u30EF\u30FC\u30C9helper\u3092\u6E96\u5099\u3067\u304D\u307E\u305B\u3093\u3002OpenSSH\u3068Windows PowerShell 5\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
@@ -22772,7 +22779,9 @@ var ja_JP_default = {
22772
22779
  deploy_secretImportFailed: "\u65E7\u8A8D\u8A3C\u60C5\u5831\u306E\u30A4\u30F3\u30DD\u30FC\u30C8\u306F {phase} \u6BB5\u968E\u3067\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002\u5165\u529B\u6A29\u9650\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3068\u30DC\u30EA\u30E5\u30FC\u30E0\u3001\u5B9F\u969B\u306E\u8A8D\u8A3C\u60C5\u5831\u3001\u30C7\u30D7\u30ED\u30A4\u30ED\u30C3\u30AF\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D3\u30B9\u306F\u66F4\u65B0\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u78BA\u5B9A\u304C\u4E0D\u660E\u306A\u5834\u5408\u306F\u4FDD\u8B77\u3055\u308C\u305F\u72B6\u614B\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
22773
22780
  deploy_projectNameRequired: "\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304C\u306A\u3044\u304B\u6B63\u898F\u5316\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30A4\u30E1\u30FC\u30B8\u3068\u30C7\u30D7\u30ED\u30A4\u30EA\u30BD\u30FC\u30B9\u306E\u8B58\u5225\u540D\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002",
22774
22781
  deploy_projectNameOverrideRemoved: "\u30C7\u30D7\u30ED\u30A4\u8B58\u5225\u540D\u306F\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304B\u3089\u53D6\u5F97\u3057\u307E\u3059\u3002\u65E7 projectName \u4E0A\u66F8\u304D\u5F15\u6570\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22775
- deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
22782
+ deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
22783
+ deploy_httpsPortsInvalid: "HTTP \u3068 HTTPS \u306E\u30DD\u30FC\u30C8\u306F\u7570\u306A\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002Let\u2019s Encrypt HTTP-01 \u306B\u306F\u30DB\u30B9\u30C8\u306E\u30DD\u30FC\u30C8 80 \u304C\u5FC5\u8981\u3067\u3059\u3002",
22784
+ deploy_httpsCertificateInvalid: "HTTPS \u8A3C\u660E\u66F8\u304C\u7121\u52B9\u3067\u3059\u3002PEM \u30D5\u30A1\u30A4\u30EB\u3001\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u79D8\u5BC6\u9375\u3068\u306E\u4E00\u81F4\u3001\u6709\u52B9\u671F\u9593\u3001\u30C9\u30E1\u30A4\u30F3\u3092\u78BA\u8A8D\u3057\u3001\u518D\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
22776
22785
  };
22777
22786
 
22778
22787
  // ../deploy/messages/zh-CN.json
@@ -22828,9 +22837,9 @@ var zh_CN_default2 = {
22828
22837
  deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
22829
22838
  deploy_webBaseConflict: "\u7F51\u7AD9\u6302\u8F7D\u76EE\u5F55\u4E0E API \u6216\u5065\u5EB7\u68C0\u67E5\u5165\u53E3\u51B2\u7A81\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C\u7684 PUBLIC_BASE_PATH\u3002",
22830
22839
  deploy_storageProxyInvalid: "\u65E0\u6CD5\u751F\u6210\u5BF9\u8C61\u5B58\u50A8\u4EE3\u7406\uFF1A\u8BF7\u4F7F\u7528\u6709\u6548\u7684 DNS \u6876\u540D\uFF0C\u5E76\u786E\u4FDD\u6876\u8DEF\u5F84\u4E0D\u4E0E\u7F51\u7AD9\u76EE\u5F55\u3001API \u6216\u5065\u5EB7\u68C0\u67E5\u5165\u53E3\u91CD\u53E0\u3002",
22831
- deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u65E0\u6548\uFF1A\u8BF7\u586B\u5199\u6D4F\u89C8\u5668\u53EF\u8BBF\u95EE\u7684 HTTP(S) \u6E90\u5730\u5740\uFF0C\u4E0D\u80FD\u5305\u542B\u6876\u540D\u3001\u8DEF\u5F84\u3001\u51ED\u636E\u3001\u67E5\u8BE2\u6216\u7247\u6BB5\u3002",
22840
+ deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u65E0\u6548\uFF1A\u8BF7\u586B\u5199\u6D4F\u89C8\u5668\u53EF\u8BBF\u95EE\u7684 HTTP(S) \u5730\u5740\uFF0C\u53EF\u5305\u542B\u89C4\u8303\u7684\u4EE3\u7406\u76EE\u5F55\uFF0C\u4E0D\u80FD\u5305\u542B\u6876\u540D\u3001\u51ED\u636E\u3001\u67E5\u8BE2\u6216\u7247\u6BB5\u3002",
22832
22841
  deploy_storageBackendRequired: "\u9759\u6001\u6D4F\u89C8\u5668\u5E94\u7528\u4E0D\u80FD\u90E8\u7F72\u5B58\u50A8\u670D\u52A1\u7AEF\u914D\u7F6E\u3002\u8BF7\u5728 Serv \u6216 SvelteKit \u670D\u52A1\u4E2D\u914D\u7F6E Storage\uFF0C\u518D\u901A\u8FC7 API \u83B7\u53D6\u9884\u7B7E\u540D\u94FE\u63A5\u3002",
22833
- deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target docker\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
22842
+ deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target image\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
22834
22843
  deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
22835
22844
  deploy_sshPasswordInvalid: "SSH \u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A\u6216\u5305\u542B CR/LF/NUL\uFF0CUTF-8 \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 1023 \u5B57\u8282\u3002",
22836
22845
  deploy_sshAskpassFailed: "\u65E0\u6CD5\u51C6\u5907\u53D7\u63A7 SSH \u5BC6\u7801 helper\uFF1B\u8BF7\u68C0\u67E5\u7CFB\u7EDF OpenSSH \u548C Windows PowerShell 5\u3002",
@@ -22880,7 +22889,9 @@ var zh_CN_default2 = {
22880
22889
  deploy_secretImportFailed: "\u65E7\u51ED\u636E\u5BFC\u5165\u5728 {phase} \u9636\u6BB5\u88AB\u62D2\u7EDD\uFF1B\u8BF7\u6838\u5BF9\u79C1\u6709\u8F93\u5165\u6743\u9650\u3001\u9879\u76EE/\u5377\u8EAB\u4EFD\u3001\u5B9E\u9645\u670D\u52A1\u51ED\u636E\u53CA\u90E8\u7F72\u9501\uFF0C\u672A\u6267\u884C\u670D\u52A1\u66F4\u65B0\uFF1B\u63D0\u4EA4\u7ED3\u679C\u4E0D\u660E\u65F6\u8BF7\u6838\u5BF9\u53D7\u4FDD\u62A4\u7684\u72B6\u6001\u6587\u4EF6",
22881
22890
  deploy_projectNameRequired: "\u5DE5\u7A0B\u6839 package.json.name \u7F3A\u5931\u6216\u65E0\u6CD5\u5F52\u4E00\u5316\uFF0C\u4E0D\u80FD\u751F\u6210\u955C\u50CF\u4E0E\u90E8\u7F72\u8D44\u6E90\u8EAB\u4EFD\u3002",
22882
22891
  deploy_projectNameOverrideRemoved: "\u90E8\u7F72\u9879\u76EE\u540D\u7EDF\u4E00\u6765\u81EA\u5DE5\u7A0B\u6839 package.json.name\uFF1B\u8BF7\u79FB\u9664\u65E7 projectName \u8986\u76D6\u53C2\u6570\u3002",
22883
- deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002"
22892
+ deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002",
22893
+ deploy_httpsPortsInvalid: "HTTP \u4E0E HTTPS \u7AEF\u53E3\u5FC5\u987B\u4E0D\u540C\uFF1BLet\u2019s Encrypt \u7684 HTTP-01 \u5165\u53E3\u5FC5\u987B\u4F7F\u7528\u5BBF\u4E3B\u673A 80 \u7AEF\u53E3\u3002",
22894
+ deploy_httpsCertificateInvalid: "HTTPS \u8BC1\u4E66\u65E0\u6548\uFF1A\u8BF7\u6838\u5BF9 PEM \u6587\u4EF6\u53EF\u8BFB\u3001\u8BC1\u4E66\u4E0E\u672A\u52A0\u5BC6\u79C1\u94A5\u5339\u914D\u3001\u8BC1\u4E66\u6709\u6548\u671F\u53CA\u57DF\u540D\uFF0C\u7136\u540E\u91CD\u65B0\u6253\u5305\u3002"
22884
22895
  };
22885
22896
 
22886
22897
  // ../deploy/src/deploy-i18n.ts
@@ -22920,12 +22931,43 @@ var SshConfigSchema = external_exports.object({
22920
22931
  if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
22921
22932
  context.addIssue({ code: "custom", path: ["password"], message: deployM("deploy_sshPasswordInvalid") });
22922
22933
  });
22934
+ var HttpsDomainSchema = external_exports.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
22935
+ var httpsOptions = {
22936
+ /** 公开访问域名;同时用于证书身份校验和 Nginx 入口。 */
22937
+ domain: HttpsDomainSchema.describe("HTTPS \u516C\u5F00\u57DF\u540D\uFF1B\u8BC1\u4E66\u5FC5\u987B\u5339\u914D\u8BE5\u57DF\u540D"),
22938
+ /** HTTPS 宿主端口,与 HTTP 端口不同。 */
22939
+ port: external_exports.number().int().min(1).max(65535).default(443).describe("HTTPS \u5BBF\u4E3B\u7AEF\u53E3\uFF0C\u9ED8\u8BA4 443\uFF0C\u987B\u4E0E HTTP \u7AEF\u53E3\u4E0D\u540C"),
22940
+ /** 是否将普通 HTTP 请求跳转到 HTTPS。 */
22941
+ redirectHttp: external_exports.boolean().default(true).describe("\u666E\u901A HTTP \u8BF7\u6C42\u662F\u5426\u4EE5 308 \u8DF3\u8F6C\u81F3 HTTPS\uFF0C\u5065\u5EB7\u68C0\u67E5\u548C ACME \u6311\u6218\u9664\u5916")
22942
+ };
22943
+ var HttpsConfigSchema = external_exports.discriminatedUnion("type", [
22944
+ external_exports.object({
22945
+ type: external_exports.literal("files"),
22946
+ ...httpsOptions,
22947
+ /** 相对于工程根或绝对路径的 PEM 完整证书链及未加密私钥。 */
22948
+ certificateFile: external_exports.string().min(1).describe("PEM \u5B8C\u6574\u8BC1\u4E66\u94FE\u8DEF\u5F84\uFF0C\u76F8\u5BF9\u5DE5\u7A0B\u6839\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u6253\u5305\u65F6\u590D\u5236\u5230\u79C1\u6709\u53D1\u5E03\u76EE\u5F55"),
22949
+ privateKeyFile: external_exports.string().min(1).describe("\u672A\u52A0\u5BC6 PEM \u79C1\u94A5\u8DEF\u5F84\uFF0C\u76F8\u5BF9\u5DE5\u7A0B\u6839\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u5FC5\u987B\u4E0E\u8BC1\u4E66\u5339\u914D")
22950
+ }),
22951
+ external_exports.object({
22952
+ type: external_exports.literal("letsencrypt"),
22953
+ ...httpsOptions,
22954
+ domain: HttpsDomainSchema.refine((value) => value.includes(".") && !/^\d+(?:\.\d+){3}$/.test(value)).describe("Let\u2019s Encrypt \u5355\u4E2A\u516C\u7F51\u57DF\u540D\uFF0C\u4F7F\u7528 HTTP-01\uFF0C\u4E0D\u652F\u6301 IP \u6216\u901A\u914D\u7B26"),
22955
+ email: external_exports.email().describe("Let\u2019s Encrypt \u8D26\u53F7\u8054\u7CFB\u90AE\u7BB1"),
22956
+ /** 使用者须明确同意 Let's Encrypt 的服务条款。 */
22957
+ acceptTerms: external_exports.literal(true).describe("\u660E\u786E\u540C\u610F Let\u2019s Encrypt \u670D\u52A1\u6761\u6B3E\u540E\u624D\u53EF\u8BBE\u7F6E\u4E3A true")
22958
+ })
22959
+ ]);
22923
22960
  var PortExposeSchema = external_exports.object({
22924
22961
  type: external_exports.literal("port").default("port"),
22925
22962
  /** 容器内监听端口(adapter-node 默认 3000) */
22926
22963
  containerPort: external_exports.number().int().min(1).max(65535).default(3e3),
22927
22964
  /** 宿主机映射端口 */
22928
- hostPort: external_exports.number().int().min(1).max(65535)
22965
+ hostPort: external_exports.number().int().min(1).max(65535),
22966
+ /** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
22967
+ 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")
22968
+ }).superRefine((expose, context) => {
22969
+ if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
22970
+ context.addIssue({ code: "custom", path: ["https"], message: deployM("deploy_httpsPortsInvalid") });
22929
22971
  });
22930
22972
  var ExposeSchema = PortExposeSchema;
22931
22973
  var DockerSshProviderConfigSchema = external_exports.object({
@@ -23424,6 +23466,32 @@ function saveCredentials(entries) {
23424
23466
  );
23425
23467
  }
23426
23468
  }
23469
+
23470
+ // ../deploy/src/deploy-middleware-config.ts
23471
+ var connectionFields = {
23472
+ db: ["url", "host", "port", "database", "user", "password", "ssl"],
23473
+ cache: ["url", "host", "port", "password", "cluster", "sentinel", "tls"],
23474
+ storage: ["endpoint", "bucket", "region", "accessKeyId", "secretAccessKey", "forcePathStyle", "createBucket"],
23475
+ vecdb: ["url", "host", "port", "database", "user", "password", "apiKey", "path", "serverCommand", "startupTimeout"],
23476
+ ldap: ["url", "bindDn", "bindPassword", "searchBase", "useTls"],
23477
+ email: ["host", "port", "secure", "user", "pass"]
23478
+ };
23479
+ function mergeManagedMiddlewareConfig(requirement, supplied) {
23480
+ if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
23481
+ return supplied;
23482
+ const configured = { ...requirement.config };
23483
+ for (const field2 of connectionFields[requirement.service] ?? [])
23484
+ delete configured[field2];
23485
+ const merged = { ...configured, ...supplied };
23486
+ if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
23487
+ const url2 = new URL(supplied.url);
23488
+ if (configured.db !== 0 || url2.pathname !== "" && url2.pathname !== "/") {
23489
+ url2.pathname = String(configured.db);
23490
+ merged.url = url2.toString();
23491
+ }
23492
+ }
23493
+ return merged;
23494
+ }
23427
23495
  var RecoverySchema = external_exports.object({
23428
23496
  projectName: external_exports.string(),
23429
23497
  stage: external_exports.string(),
@@ -25200,7 +25268,7 @@ async function groupExited(pid) {
25200
25268
  async function helper(command, args, timeoutMs) {
25201
25269
  if (timeoutMs <= 0)
25202
25270
  return false;
25203
- return new Promise((resolve8) => {
25271
+ return new Promise((resolve9) => {
25204
25272
  const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
25205
25273
  let settled = false;
25206
25274
  let timer;
@@ -25210,7 +25278,7 @@ async function helper(command, args, timeoutMs) {
25210
25278
  settled = true;
25211
25279
  if (timer)
25212
25280
  clearTimeout(timer);
25213
- resolve8(success2);
25281
+ resolve9(success2);
25214
25282
  };
25215
25283
  timer = globalThis.setTimeout(() => {
25216
25284
  child.kill("SIGKILL");
@@ -25353,7 +25421,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
25353
25421
  ext: { commandExecution: state }
25354
25422
  });
25355
25423
  try {
25356
- return await new Promise((resolve8) => {
25424
+ return await new Promise((resolve9) => {
25357
25425
  if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
25358
25426
  throw new TypeError("Invalid command arguments");
25359
25427
  if (options.input !== void 0 && typeof options.input !== "string")
@@ -25388,7 +25456,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
25388
25456
  settled = true;
25389
25457
  if (timer)
25390
25458
  clearTimeout(timer);
25391
- resolve8(result);
25459
+ resolve9(result);
25392
25460
  };
25393
25461
  const interrupt = (reason, message) => {
25394
25462
  interrupted = true;
@@ -25837,7 +25905,7 @@ start_services() {
25837
25905
  application_services=''
25838
25906
  application_orphan_option=$orphan_option
25839
25907
  for service in "$@"; do
25840
- case "$service" in serv|web|gateway) ;; *) middleware_services="$middleware_services $service";; esac
25908
+ case "$service" in serv|web|gateway|certbot) ;; *) middleware_services="$middleware_services $service";; esac
25841
25909
  done
25842
25910
  if [ -n "$middleware_services" ]; then
25843
25911
  compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $middleware_services || return 1
@@ -25853,6 +25921,9 @@ start_services() {
25853
25921
  fi
25854
25922
  case " $* " in *' gateway '*) compose up -d --no-deps --no-build --pull never "$recreate_option" gateway || return 1;; esac
25855
25923
  }
25924
+ start_renewal() {
25925
+ case " $* " in *' certbot '*) compose up -d --no-deps --no-build --pull never --no-recreate certbot || return 1;; esac
25926
+ }
25856
25927
  read_proxy_service() {
25857
25928
  proxy_directory=$1
25858
25929
  if [ -e "$proxy_directory/proxy-service" ] || [ -L "$proxy_directory/proxy-service" ]; then
@@ -25876,13 +25947,22 @@ reload_retained_proxy() {
25876
25947
  esac
25877
25948
  }
25878
25949
  wait_http() {
25879
- http_port=$1
25880
- shift
25950
+ http_release=$1
25951
+ http_port=$2
25952
+ shift 2
25953
+ https_domain=''
25954
+ if [ -f "$http_release/https" ]; then
25955
+ read -r https_domain https_port https_type < "$http_release/https"
25956
+ https_certificates="$http_release/tls"
25957
+ if [ "$https_type" = letsencrypt ]; then https_certificates="$root/https/certificates/$https_domain"; fi
25958
+ fi
25881
25959
  for http_path in "$@"; do
25882
25960
  http_ready=false
25883
25961
  http_attempt=0
25884
25962
  while [ "$http_attempt" -lt 40 ]; do
25885
- if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:$http_port$http_path" >/dev/null 2>&1; then http_ready=true; break; fi
25963
+ if [ -n "$https_domain" ]; then
25964
+ if curl --noproxy '*' -fsS --max-time 5 --cacert "$https_certificates/fullchain.pem" --resolve "$https_domain:$https_port:127.0.0.1" "https://$https_domain:$https_port$http_path" >/dev/null 2>&1; then http_ready=true; break; fi
25965
+ elif curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:$http_port$http_path" >/dev/null 2>&1; then http_ready=true; break; fi
25886
25966
  http_attempt=$((http_attempt + 1))
25887
25967
  sleep 3
25888
25968
  done
@@ -25895,7 +25975,6 @@ verify_recovery() {
25895
25975
  recovery_port=$(awk -F: 'NR == 1 { print $1 }' "$recovery_release/layout") || return 1
25896
25976
  recovery_paths=''
25897
25977
  if [ -f "$recovery_release/health-paths" ]; then
25898
- recovery_port=$(cat "$recovery_release/health-port") || return 1
25899
25978
  recovery_paths=$(cat "$recovery_release/health-paths") || return 1
25900
25979
  else
25901
25980
  # 旧格式发布没有独立健康文件;按已存拓扑恢复最小 HTTP 检查。
@@ -25905,10 +25984,10 @@ verify_recovery() {
25905
25984
  elif [ "$recovery_primary" = web ]; then recovery_paths='/'; else recovery_paths='/health'; fi
25906
25985
  fi
25907
25986
  case "$recovery_port" in ''|*[!0-9]*) return 1;; esac
25908
- test -n "$recovery_paths" || return 1
25909
25987
  wait_services $recover_selected || return 1
25988
+ start_renewal $recover_selected || return 1
25910
25989
  reload_retained_proxy "$recovery_release" || return 1
25911
- wait_http "$recovery_port" $recovery_paths
25990
+ wait_http "$recovery_release" "$recovery_port" $recovery_paths
25912
25991
  }
25913
25992
  finish() {
25914
25993
  code=$?
@@ -26036,7 +26115,7 @@ if [ "$partial" = true ]; then
26036
26115
  test "$current_storage_routing" = "$previous_storage_routing" || { printf 'Managed storage configuration changed. Create a full deployment to switch its public endpoint, bucket, or access policy consistently.\n' >&2; exit 1; }
26037
26116
  if ! cmp -s layout "$root/releases/$previous/layout"; then
26038
26117
  # 容量调整允许复用已部署 Web 镜像;端口、路由及其余代理策略仍要求全量部署。
26039
- test "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4}' layout)" = "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4}' "$root/releases/$previous/layout")" || { printf 'Ports or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
26118
+ test "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4 ":" $6}' layout)" = "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4 ":" $6}' "$root/releases/$previous/layout")" || { printf 'Ports, HTTPS or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
26040
26119
  test -f web.conf && test -f proxy-entry.json || { printf 'Proxy configuration missing. Create a full deployment bundle.\n' >&2; exit 1; }
26041
26120
  previous_proxy=
26042
26121
  while IFS= read -r compose_file; do
@@ -26054,9 +26133,17 @@ if [ "$partial" = true ]; then
26054
26133
  fi
26055
26134
  fi
26056
26135
  mkdir -p "$release"
26057
- cp -R "$source_dir/." "$release/"
26136
+ # 镜像直接从本次解包目录导入;发布记录只保存运行配置与恢复证据。
26137
+ for entry in "$source_dir"/* "$source_dir"/.[!.]*; do
26138
+ test -e "$entry" || continue
26139
+ test "$entry" != "$source_dir/images" || continue
26140
+ cp -R "$entry" "$release/"
26141
+ done
26058
26142
  # Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
26059
26143
  if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
26144
+ for https_script in https-start.sh renew-https.sh; do
26145
+ if [ -f "$release/$https_script" ]; then chmod 444 "$release/$https_script"; fi
26146
+ done
26060
26147
  # 策略不是凭据,发布目录保持700;只读挂载允许镜像中的非root应用读取。
26061
26148
  if [ -f "$release/request-access.yml" ]; then chmod 444 "$release/request-access.yml"; fi
26062
26149
  : > "$release/compose-files"
@@ -26070,6 +26157,12 @@ if [ "$partial" = true ]; then
26070
26157
  fi
26071
26158
  step credentials 'Resolving server-managed middleware credentials'
26072
26159
  sh "$release/resolve-managed-secrets.sh" "$root" "$release" "$runtime"
26160
+ if [ -f "$release/runtime-env.mjs" ]; then
26161
+ # 上级目录保护宿主秘密;只读文件挂载允许非 root 应用读取,宿主所有者仍可修改配置。
26162
+ chmod 700 "$root" "$release"
26163
+ chmod 644 "$release/.env.runtime" "$release/middleware.env"
26164
+ chmod 444 "$release/runtime-env.mjs"
26165
+ fi
26073
26166
  # 在任何可能启动服务(包括增量恢复旧拓扑)之前定稿,失败回滚不删除卷的引擎身份。
26074
26167
  sh "$release/resolve-backend-state.sh" "$root" "$release" "$runtime" "$previous" commit
26075
26168
  printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
@@ -26107,12 +26200,15 @@ step images 'Loading packaged images (no registry access)'
26107
26200
  while IFS=' ' read -r archive reference; do
26108
26201
  test -n "$archive" || continue
26109
26202
  images_changed=true
26110
- "$runtime" load -i "$release/images/$archive"
26203
+ "$runtime" load -i "$source_dir/images/$archive"
26111
26204
  "$runtime" image inspect "$reference" >/dev/null
26112
26205
  architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
26113
26206
  host_arch=$(uname -m)
26114
26207
  case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
26115
26208
  done < "$release/image-list"
26209
+ if [ -f "$release/prepare-https.sh" ]; then
26210
+ sh "$release/prepare-https.sh" "$root" "$release" "$runtime" prepare "$partial"
26211
+ fi
26116
26212
  step dependencies 'Checking dependencies of selected services'
26117
26213
  if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
26118
26214
  while IFS= read -r dependency; do
@@ -26124,9 +26220,18 @@ step start 'Updating selected services'
26124
26220
  changed=true
26125
26221
  remove_selected $selected
26126
26222
  start_services $selected
26223
+ if [ -f "$release/prepare-https.sh" ] && [ "$partial" = false ]; then
26224
+ step https 'Issuing or installing HTTPS certificate'
26225
+ tls_proxy=$(read_proxy_service "$release")
26226
+ wait_services "$tls_proxy"
26227
+ sh "$release/prepare-https.sh" "$root" "$release" "$runtime" issue "$partial"
26228
+ compose exec -T "$tls_proxy" nginx -t
26229
+ compose exec -T "$tls_proxy" nginx -s reload
26230
+ fi
26231
+ start_renewal $selected
26127
26232
  reload_retained_proxy "$release"
26128
26233
  step health 'Checking HTTP access'
26129
- wait_http @@PORT@@ @@HEALTH@@
26234
+ wait_http "$release" @@PORT@@ @@HEALTH@@
26130
26235
  step complete 'Deployment passed all checks'
26131
26236
  printf '%s\n' "$id" > "$root/current.next"
26132
26237
  mv "$root/current.next" "$root/current"
@@ -26180,7 +26285,7 @@ case "$timeout" in ''|*[!0-9]*|???????????*) invalid_target;; esac
26180
26285
  test "$timeout" -ge 1 && test "$timeout" -le 2147483647 || invalid_target
26181
26286
  case "$remote" in /*) ;; *) invalid_target;; esac
26182
26287
  case "$remote" in *[!a-zA-Z0-9/_.\ -]*|*/../*|*/..|/..) invalid_target;; esac
26183
- case "$bundle" in *.tar) ;; *) invalid_target;; esac
26288
+ case "$bundle" in *.tar|*.tar.gz) ;; *) invalid_target;; esac
26184
26289
  case "$bundle" in ''|*[!a-zA-Z0-9_.+-]*) invalid_target;; esac
26185
26290
  case "$strict" in true) strict=accept-new;; false) strict=no;; *) invalid_target;; esac
26186
26291
  test -f "./$bundle" || { printf 'Bundle missing: %s\n' "$bundle" >&2; exit 1; }
@@ -26304,7 +26409,7 @@ monitor=''
26304
26409
  if [ "$upload_code" -ne 0 ]; then printf '[FAIL] Upload failed (exit %s). Check SSH output above.\n' "$upload_code" >&2; exit "$upload_code"; fi
26305
26410
  printf '[UPLOAD] 100%% (%s/%s bytes)\n' "$total" "$total"
26306
26411
  printf '[3/3] Applying package and checking HTTP access\n'
26307
- if run_native ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"; then
26412
+ if run_native ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote' && { rm -rf -- '$bundle.contents' && rm -f -- '$bundle' || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }"; then
26308
26413
  :
26309
26414
  else
26310
26415
  code=$?
@@ -26339,7 +26444,7 @@ if ($windowsRuntime) {
26339
26444
  Set-Location -LiteralPath $PSScriptRoot
26340
26445
  $target = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'deploy-target.json') -Raw -Encoding UTF8 | ConvertFrom-Json
26341
26446
  if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
26342
- if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
26447
+ if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar(\.gz)?$') { throw 'Invalid deployment target.' }
26343
26448
  $port = if ($null -eq $target.port) { 22 } else { $target.port }
26344
26449
  $timeout = if ($null -eq $target.connectTimeoutSeconds) { 10 } else { $target.connectTimeoutSeconds }
26345
26450
  if ($port -is [string] -or [string]$port -notmatch '^[0-9]+$' -or $port -lt 1 -or $port -gt 65535 -or $timeout -is [string] -or [string]$timeout -notmatch '^[0-9]+$' -or $timeout -lt 1 -or $timeout -gt 2147483647) { throw 'Invalid deployment port or timeout.' }
@@ -26498,7 +26603,7 @@ try {
26498
26603
  Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$port, "./$bundle", "$($scpDestination):$remote/incoming/$bundle"))
26499
26604
  Write-Host "[UPLOAD] 100% ($uploadBytes/$uploadBytes bytes)"
26500
26605
  $applying = $true
26501
- Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
26606
+ Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote' && { rm -rf -- '$bundle.contents' && rm -f -- '$bundle' || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }"))
26502
26607
  } catch {
26503
26608
  if ($applying) {
26504
26609
  if ($LASTEXITCODE -gt 0 -and $LASTEXITCODE -lt 128) { [Console]::Error.WriteLine("[FAIL] Remote deployment failed (exit $LASTEXITCODE). Inspect $remote/result-<deploymentId>.json (ID in README.txt) and startup diagnostics.") }
@@ -26667,13 +26772,14 @@ ${removed.error.message}` });
26667
26772
  async function createDeploymentBundle(options) {
26668
26773
  const { directory, artifactDir, artifact, projectName, environment, config: config2, selected } = options;
26669
26774
  const output2 = resolve(options.output);
26670
- if (!/^[\w.+-]+\.tar$/.test(basename(output2)))
26775
+ if (!/^[\w.+-]+\.tar(?:\.gz)?$/.test(basename(output2)))
26671
26776
  throw new Error(deployM("deploy_bundleNameInvalid"));
26672
26777
  const staging = mkdtempSync(join(directory, ".bundle-"));
26673
26778
  const temporaryTar = `${output2}.partial`;
26674
26779
  mkdirSync(dirname(output2), { recursive: true, mode: 448 });
26675
26780
  chmodSync(staging, 448);
26676
26781
  const files = [];
26782
+ const fileHashes = /* @__PURE__ */ new Map();
26677
26783
  const put = (file2, content) => {
26678
26784
  mkdirSync(dirname(join(staging, file2)), { recursive: true });
26679
26785
  writeFileSync(join(staging, file2), content, { mode: 384 });
@@ -26698,7 +26804,7 @@ async function createDeploymentBundle(options) {
26698
26804
  service.env_file = name === artifact.primaryService ? [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`] : [`../${options.deploymentId}/middleware-runtime.env`];
26699
26805
  }
26700
26806
  if (Array.isArray(service.volumes))
26701
- service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`).replace("./request-access.yml:", `../${options.deploymentId}/request-access.yml:`) : mount);
26807
+ service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace(/\.\/(web\.conf|request-access\.yml|tls|https-start\.sh|renew-https\.sh|\.env\.runtime|middleware\.env|runtime-env\.mjs):/g, `../${options.deploymentId}/$1:`) : mount);
26702
26808
  }
26703
26809
  put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
26704
26810
  put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
@@ -26709,8 +26815,12 @@ async function createDeploymentBundle(options) {
26709
26815
  throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
26710
26816
  const built = artifact.images.find((image) => image.imageRef === reference);
26711
26817
  const file2 = built?.file ?? `middleware-${imageList.length + 1}.tar`;
26818
+ const destination = join(staging, "images", file2);
26712
26819
  if (built) {
26713
- copyFileSync(join(artifactDir, file2), join(staging, "images", file2));
26820
+ const checksum = await copyArchive(join(artifactDir, file2), destination);
26821
+ if (checksum !== built.sha256)
26822
+ throw new Error(deployM("deploy_artifactInvalid"));
26823
+ fileHashes.set(`images/${file2}`, checksum);
26714
26824
  } else {
26715
26825
  if (!runtime) {
26716
26826
  const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
@@ -26723,14 +26833,41 @@ async function createDeploymentBundle(options) {
26723
26833
  if (!runtime)
26724
26834
  throw new Error(deployM("deploy_bundleRuntimeRequired"));
26725
26835
  }
26726
- if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
26836
+ let inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
26837
+ if (!inspected.success) {
26727
26838
  const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
26728
26839
  if (!pulled.success)
26729
26840
  throw new Error(pulled.error.message);
26841
+ inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
26842
+ }
26843
+ const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
26844
+ const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
26845
+ const cache = options.cacheDirectory && /^[a-f0-9]{64}$/.test(identity) ? join(options.cacheDirectory, `${runtime}-${cacheIdentity}.tar`) : void 0;
26846
+ let checksum;
26847
+ if (cache && existsSync(cache) && existsSync(`${cache}.sha256`) && lstatSync(cache).isFile() && !lstatSync(cache).isSymbolicLink()) {
26848
+ const copied = await copyArchive(cache, destination);
26849
+ if (copied === readFileSync(`${cache}.sha256`, "utf8").trim())
26850
+ checksum = copied;
26851
+ }
26852
+ if (!checksum) {
26853
+ const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", destination, reference], { timeoutMs: 6e5 });
26854
+ if (!saved.success)
26855
+ throw new Error(saved.error.message);
26856
+ checksum = await artifactChecksum(destination);
26857
+ if (cache) {
26858
+ mkdirSync(dirname(cache), { recursive: true, mode: 448 });
26859
+ const temporary = `${cache}.${basename(staging)}`;
26860
+ try {
26861
+ copyFileSync(destination, temporary);
26862
+ renameSync(temporary, cache);
26863
+ writeFileSync(`${cache}.sha256`, `${checksum}
26864
+ `, { mode: 384 });
26865
+ } finally {
26866
+ rmSync(temporary, { force: true });
26867
+ }
26868
+ }
26730
26869
  }
26731
- const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file2), reference], { timeoutMs: 6e5 });
26732
- if (!saved.success)
26733
- throw new Error(saved.error.message);
26870
+ fileHashes.set(`images/${file2}`, checksum);
26734
26871
  }
26735
26872
  files.push(`images/${file2}`);
26736
26873
  imageList.push(`${file2} ${reference}`);
@@ -26739,6 +26876,8 @@ async function createDeploymentBundle(options) {
26739
26876
  if (selected.includes(artifact.primaryService) && existsSync(join(directory, "request-access.yml")))
26740
26877
  put("request-access.yml", readFileSync(join(directory, "request-access.yml"), "utf8"));
26741
26878
  put("middleware.env", options.middlewareEnv);
26879
+ if (selected.includes(artifact.primaryService) && !compose.services[artifact.primaryService]?.env_file)
26880
+ put("runtime-env.mjs", readFileSync(new URL("./deploy-runtime-env.mjs", import.meta.url), "utf8"));
26742
26881
  const middlewareRuntime = join(directory, "middleware-runtime.env");
26743
26882
  const nativeEnvironment = existsSync(middlewareRuntime) ? readFileSync(middlewareRuntime, "utf8") : "";
26744
26883
  put("middleware-runtime.env", nativeEnvironment);
@@ -26746,18 +26885,17 @@ async function createDeploymentBundle(options) {
26746
26885
  put(file2, content);
26747
26886
  if (options.hasWebConfig)
26748
26887
  put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
26888
+ for (const [file2, content] of Object.entries(options.httpsAssets ?? {}))
26889
+ put(file2, content);
26749
26890
  put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
26750
26891
  `);
26751
26892
  if (options.hasWebConfig) {
26752
26893
  put("proxy-entry.json", JSON.stringify({ services: { [options.proxyService ?? "web"]: { volumes: [`../${options.deploymentId}/web.conf:/etc/nginx/conf.d/default.conf:ro`] } } }));
26753
26894
  }
26754
- put("proxy-service", `${options.hasWebConfig ? options.proxyService ?? "web" : ""}
26755
- `);
26756
26895
  put("storage-routing", `${options.storageRouting ?? ""}
26757
26896
  `);
26758
- put("layout", `${config2.expose.hostPort}:${options.containerPort}:${options.hasWebConfig ? options.proxyService === "gateway" ? "gateway" : "true" : "false"}:${artifact.primaryService}:${options.hasWebConfig ? await artifactChecksum(join(directory, "web.conf")) : ""}
26759
- `);
26760
- put("health-port", `${config2.expose.hostPort}
26897
+ const httpsFingerprint = config2.expose.https ? createHash("sha256").update(JSON.stringify({ ...config2.expose.https, certificateFile: void 0, privateKeyFile: void 0, certificate: options.httpsAssets?.["tls/fullchain.pem"] })).digest("hex") : "";
26898
+ put("layout", `${config2.expose.hostPort}:${options.containerPort}:${options.hasWebConfig ? options.proxyService === "gateway" ? "gateway" : "true" : "false"}:${artifact.primaryService}:${options.hasWebConfig ? await artifactChecksum(join(directory, "web.conf")) : ""}${httpsFingerprint ? `:${httpsFingerprint}` : ""}
26761
26899
  `);
26762
26900
  put("web-base-path", `${artifact.webBasePath ?? ""}
26763
26901
  `);
@@ -26775,15 +26913,15 @@ async function createDeploymentBundle(options) {
26775
26913
  const selection = deployM("deploy_bundleSelection", { params: { mode, services: selected.join(", ") } });
26776
26914
  const instructions = `${selection}
26777
26915
  ${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
26778
- put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, deploymentId: options.deploymentId, compose: "compose.json" } })}
26916
+ const readme = `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, deploymentId: options.deploymentId, compose: "compose.json" } })}
26779
26917
  ${instructions}
26780
- `);
26918
+ `;
26781
26919
  const checksums = [];
26782
26920
  for (const file2 of files)
26783
- checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
26921
+ checksums.push(`${fileHashes.get(file2) ?? await artifactChecksum(join(staging, file2))} ${file2}`);
26784
26922
  writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
26785
26923
  `);
26786
- const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
26924
+ const tar = await runCommand("tar", [output2.endsWith(".gz") ? "-czf" : "-cf", temporaryTar, "-C", staging, "."]);
26787
26925
  if (!tar.success)
26788
26926
  throw new Error(tar.error.message);
26789
26927
  chmodSync(temporaryTar, 384);
@@ -26822,7 +26960,7 @@ ${instructions}
26822
26960
  };
26823
26961
  writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
26824
26962
  `);
26825
- writeFileSync(join(scripts, "README.txt"), readFileSync(join(staging, "README.txt"), "utf8"));
26963
+ writeFileSync(join(scripts, "README.txt"), readme);
26826
26964
  for (const file2 of [`docker-compose-${artifact.appVersion}.yml`, ".env.runtime", "middleware-runtime.env", "request-access.yml", ...options.hasWebConfig ? ["web.conf"] : []])
26827
26965
  rmSync(join(directory, file2), { force: true });
26828
26966
  return { bundlePath: output2, instructions: `${selection}
@@ -26833,6 +26971,178 @@ ${scripts}`, startName: "apply.sh" };
26833
26971
  rmSync(temporaryTar, { force: true });
26834
26972
  }
26835
26973
  }
26974
+ async function copyArchive(source, destination) {
26975
+ const hash2 = createHash("sha256");
26976
+ const digest = new Transform({
26977
+ transform(chunk, _encoding, callback) {
26978
+ hash2.update(chunk);
26979
+ callback(null, chunk);
26980
+ }
26981
+ });
26982
+ await pipeline(createReadStream(source), digest, createWriteStream(destination, { mode: 384 }));
26983
+ return hash2.digest("hex");
26984
+ }
26985
+ var CERTBOT_IMAGE = "docker.io/certbot/certbot:v5.9.0";
26986
+ function createHttpsAssets(config2, projectDirectory) {
26987
+ const assets = {
26988
+ https: `${config2.domain} ${config2.port} ${config2.type}
26989
+ `
26990
+ };
26991
+ if (config2.type === "files") {
26992
+ try {
26993
+ const readPem = (file2) => {
26994
+ const path = resolve(projectDirectory, file2);
26995
+ if (!statSync(path).isFile() || statSync(path).size > 1048576)
26996
+ throw new Error("Invalid PEM file");
26997
+ return readFileSync(path, "utf8");
26998
+ };
26999
+ const certificate = readPem(config2.certificateFile);
27000
+ const key = readPem(config2.privateKeyFile);
27001
+ const parsed = new X509Certificate(certificate);
27002
+ const publicKey = createPublicKey(createPrivateKey(key)).export({ type: "spki", format: "der" });
27003
+ const now = Date.now();
27004
+ if (!publicKey.equals(parsed.publicKey.export({ type: "spki", format: "der" })) || now < Date.parse(parsed.validFrom) || now >= Date.parse(parsed.validTo) || !(isIP(config2.domain) ? parsed.checkIP(config2.domain) : parsed.checkHost(config2.domain))) {
27005
+ throw new Error("Certificate validation failed");
27006
+ }
27007
+ assets["tls/fullchain.pem"] = certificate;
27008
+ assets["tls/privkey.pem"] = key;
27009
+ } catch {
27010
+ throw new Error(deployM("deploy_httpsCertificateInvalid"));
27011
+ }
27012
+ } else {
27013
+ assets["renew-https.sh"] = createRenewScript(config2);
27014
+ }
27015
+ assets["prepare-https.sh"] = createPrepareScript(config2);
27016
+ assets["https-start.sh"] = String.raw`#!/bin/sh
27017
+ set -eu
27018
+ # 目录挂载允许原子替换证书;只有新证书通过 nginx -t 才重载。
27019
+ watch_certificate() {
27020
+ previous=$(sha256sum /etc/nginx/tls/fullchain.pem)
27021
+ while sleep 60; do
27022
+ current=$(sha256sum /etc/nginx/tls/fullchain.pem) || continue
27023
+ if [ "$previous" != "$current" ]; then
27024
+ if nginx -t && nginx -s reload; then previous=$current; else printf '[ERROR] HTTPS certificate reload failed\n' >&2; fi
27025
+ fi
27026
+ done
27027
+ }
27028
+ watch_certificate &
27029
+ exec /docker-entrypoint.sh nginx -g 'daemon off;'
27030
+ `;
27031
+ return assets;
27032
+ }
27033
+ function renderHttpsProxy(source, config2, httpPort) {
27034
+ const publicPort = config2.port === 443 ? "" : `:${config2.port}`;
27035
+ const challenge = " location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }\n";
27036
+ const tls = source.replace(`listen ${httpPort};`, `listen ${httpPort === 8443 ? 8444 : 8443} ssl;
27037
+ ssl_certificate /etc/nginx/tls/fullchain.pem;
27038
+ ssl_certificate_key /etc/nginx/tls/privkey.pem;`).replace("server_name _;", `server_name ${config2.domain};`).replaceAll("proxy_set_header X-Forwarded-Proto $scheme;", "proxy_set_header X-Forwarded-Proto https;");
27039
+ const http = config2.redirectHttp ? `server {
27040
+ listen ${httpPort};
27041
+ server_name ${config2.domain};
27042
+ ${challenge} location ~ ^/_hai/(web-health|gateway-health)$ { access_log off; return 200 'ok'; }
27043
+ location / { return 308 https://${config2.domain}${publicPort}$request_uri; }
27044
+ }
27045
+ ` : source.slice(source.indexOf("server {")).replace("server_name _;", `server_name ${config2.domain};
27046
+ ${challenge}`);
27047
+ return `${tls}
27048
+ ${http}`;
27049
+ }
27050
+ var publishCertificate = String.raw`publish_certificate() {
27051
+ certificate="/etc/letsencrypt/live/$domain"
27052
+ test -s "$certificate/fullchain.pem" && test -s "$certificate/privkey.pem"
27053
+ cp "$certificate/fullchain.pem" /output/fullchain.pem.next
27054
+ cp "$certificate/privkey.pem" /output/privkey.pem.next
27055
+ chmod 444 /output/fullchain.pem.next /output/privkey.pem.next
27056
+ mv /output/privkey.pem.next /output/privkey.pem
27057
+ mv /output/fullchain.pem.next /output/fullchain.pem
27058
+ }
27059
+ `;
27060
+ function createRenewScript(config2) {
27061
+ return `#!/bin/sh
27062
+ set -eu
27063
+ domain=${shellArgument(config2.domain)}
27064
+ ${publishCertificate}${String.raw`
27065
+ trap 'exit 0' TERM INT
27066
+ # Certbot 自己判断到期时间;同一官方客户端同时负责申请与续期,不实现 ACME 协议。
27067
+ while :; do
27068
+ if certbot renew --cert-name "$domain" --non-interactive; then
27069
+ if [ -s "/etc/letsencrypt/live/$domain/fullchain.pem" ]; then
27070
+ current=$(sha256sum "/etc/letsencrypt/live/$domain/fullchain.pem" | awk '{print $1}')
27071
+ installed=$(sha256sum /output/fullchain.pem | awk '{print $1}')
27072
+ if [ "$current" != "$installed" ]; then publish_certificate; fi
27073
+ fi
27074
+ else
27075
+ printf '[ERROR] Certbot renewal failed; the existing certificate remains installed\n' >&2
27076
+ fi
27077
+ sleep 43200 & wait $! || exit 0
27078
+ done
27079
+ `}`;
27080
+ }
27081
+ function createPrepareScript(config2) {
27082
+ const header = `#!/bin/sh
27083
+ set -eu
27084
+ umask 077
27085
+ root=$1
27086
+ release=$2
27087
+ runtime=$3
27088
+ mode=$4
27089
+ domain=${shellArgument(config2.domain)}
27090
+ `;
27091
+ if (config2.type === "files") {
27092
+ return header + String.raw`
27093
+ test "$mode" != prepare || {
27094
+ # 宿主 root/release 保持私密;容器仅可读取独立挂载的证书目录。
27095
+ chmod 700 "$root" "$release"
27096
+ chmod 755 "$release/tls"
27097
+ chmod 444 "$release/tls/fullchain.pem" "$release/tls/privkey.pem"
27098
+ }
27099
+ `;
27100
+ }
27101
+ const python = [
27102
+ "from cryptography import x509",
27103
+ "from cryptography.x509.oid import NameOID",
27104
+ "from cryptography.hazmat.primitives import hashes, serialization",
27105
+ "from cryptography.hazmat.primitives.asymmetric import ec",
27106
+ "from datetime import datetime, timedelta, timezone",
27107
+ "from pathlib import Path",
27108
+ "import sys",
27109
+ "name=sys.argv[1]",
27110
+ "key=ec.generate_private_key(ec.SECP256R1())",
27111
+ "subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,name)])",
27112
+ "now=datetime.now(timezone.utc)",
27113
+ "cert=x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(days=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName(name)]),False).sign(key,hashes.SHA256())",
27114
+ 'Path("/output/privkey.pem").write_bytes(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()))',
27115
+ 'Path("/output/fullchain.pem").write_bytes(cert.public_bytes(serialization.Encoding.PEM))'
27116
+ ].join("\n");
27117
+ const publish = `set -eu
27118
+ domain=${shellArgument(config2.domain)}
27119
+ ${publishCertificate}
27120
+ publish_certificate
27121
+ `;
27122
+ return `${header}image=${shellArgument(CERTBOT_IMAGE)}
27123
+ email=${shellArgument(config2.email)}
27124
+ bootstrap=${shellArgument(python)}
27125
+ ${String.raw`
27126
+ state="$root/https"
27127
+ certificates="$state/certificates/$domain"
27128
+ mkdir -p "$state/acme" "$state/webroot" "$certificates"
27129
+ chmod 700 "$root" "$state" "$state/acme"
27130
+ chmod 755 "$state/webroot" "$certificates"
27131
+ if [ "$mode" = prepare ]; then
27132
+ if [ ! -s "$certificates/fullchain.pem" ] || [ ! -s "$certificates/privkey.pem" ]; then
27133
+ if [ "$5" = true ]; then printf 'HTTPS certificate state missing; create a full deployment.\n' >&2; exit 1; fi
27134
+ # 临时证书只用于启动挑战入口;签发失败不得提交 current。
27135
+ "$runtime" run --rm --pull never --entrypoint python -v "$certificates:/output" "$image" -c "$bootstrap" "$domain"
27136
+ chmod 444 "$certificates/fullchain.pem" "$certificates/privkey.pem"
27137
+ fi
27138
+ elif [ "$mode" = issue ]; then
27139
+ "$runtime" run --rm --pull never -v "$state/acme:/etc/letsencrypt" -v "$state/webroot:/var/www/certbot" "$image" certonly --webroot --webroot-path /var/www/certbot --cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive --keep-until-expiring
27140
+ "$runtime" run --rm --pull never --entrypoint sh -v "$state/acme:/etc/letsencrypt:ro" -v "$certificates:/output" "$image" -c `}${shellArgument(publish)}
27141
+ fi
27142
+ `;
27143
+ }
27144
+
27145
+ // ../deploy/src/container/deploy-container-compose.ts
26836
27146
  var IMAGES = {
26837
27147
  postgres: "docker.io/library/postgres:16.15-alpine",
26838
27148
  mysql: "docker.io/library/mysql:8.4.11",
@@ -27097,12 +27407,17 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
27097
27407
  const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
27098
27408
  if (requirements?.length) {
27099
27409
  for (const requirement of requirements)
27100
- plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
27410
+ 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" });
27101
27411
  } else if (!scan.middlewares) {
27102
27412
  plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
27103
27413
  }
27104
27414
  }
27105
27415
  }
27416
+ for (const requirement of scan.middlewares ?? []) {
27417
+ const supplied = plan.env[requirement.envPrefix];
27418
+ if (supplied)
27419
+ plan.env[requirement.envPrefix] = JSON.stringify(mergeManagedMiddlewareConfig(requirement, JSON.parse(supplied)));
27420
+ }
27106
27421
  return plan;
27107
27422
  }
27108
27423
  function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
@@ -27121,6 +27436,7 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
27121
27436
  function generateComposeFile(options) {
27122
27437
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
27123
27438
  const primaryService = options.primaryService ?? "serv";
27439
+ const nodeRuntime = options.nodeRuntime ?? primaryService === "serv";
27124
27440
  const app = {
27125
27441
  image: imageRef,
27126
27442
  labels: { "org.opencontainers.image.version": options.appVersion },
@@ -27132,11 +27448,28 @@ function generateComposeFile(options) {
27132
27448
  ports: [`${hostPort}:${containerPort}`],
27133
27449
  networks: ["hai-app"]
27134
27450
  };
27451
+ if (primaryService === "serv") {
27452
+ app.healthcheck = {
27453
+ test: ["CMD", "node", "-e", "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config' }); 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); } })()"],
27454
+ interval: "30s",
27455
+ timeout: "5s",
27456
+ start_period: "15s",
27457
+ retries: 3
27458
+ };
27459
+ }
27135
27460
  const appVolumes = [...sidecar.appVolumes];
27461
+ const appEnvironment = {};
27462
+ if (nodeRuntime) {
27463
+ delete app.env_file;
27464
+ appVolumes.push("./.env.runtime:/run/hai/.env.runtime:ro", "./middleware.env:/run/hai/middleware.env:ro", "./runtime-env.mjs:/run/hai/runtime-env.mjs:ro");
27465
+ appEnvironment.NODE_OPTIONS = `--import /run/hai/runtime-env.mjs${options.nodeOptions ? ` ${options.nodeOptions}` : ""}`.replaceAll("$", () => "$$");
27466
+ }
27136
27467
  if (options.requestAccessPolicy)
27137
27468
  appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
27138
27469
  if (options.web || options.gateway)
27139
- app.environment = { HAI_DEPLOY_PRIVATE_PROXY: "1" };
27470
+ appEnvironment.HAI_DEPLOY_PRIVATE_PROXY = "1";
27471
+ if (Object.keys(appEnvironment).length)
27472
+ app.environment = appEnvironment;
27140
27473
  if (options.mountWebConfig)
27141
27474
  appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
27142
27475
  if (appVolumes.length)
@@ -27169,10 +27502,38 @@ function generateComposeFile(options) {
27169
27502
  ports: [`${hostPort}:8080`],
27170
27503
  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 },
27171
27504
  volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
27172
- depends_on: [primaryService, "storage"],
27505
+ depends_on: [primaryService, ...sidecar.services.includes("storage") ? ["storage"] : []],
27173
27506
  networks: ["hai-app"]
27174
27507
  };
27175
27508
  }
27509
+ if (options.https) {
27510
+ const proxy = services[options.gateway ? "gateway" : options.web ? "web" : primaryService];
27511
+ const https = options.https;
27512
+ const httpPort = options.gateway ? 8080 : options.web?.containerPort ?? containerPort;
27513
+ proxy.ports = [...proxy.ports, `${https.port}:${httpPort === 8443 ? 8444 : 8443}`];
27514
+ const certificates = https.type === "files" ? "./tls" : `../../https/certificates/${https.domain}`;
27515
+ proxy.volumes = [
27516
+ ...proxy.volumes,
27517
+ `${certificates}:/etc/nginx/tls:ro`,
27518
+ "./https-start.sh:/run/hai/https-start.sh:ro",
27519
+ ...https.type === "letsencrypt" ? ["../../https/webroot:/var/www/certbot:ro"] : []
27520
+ ];
27521
+ proxy.entrypoint = ["sh", "/run/hai/https-start.sh"];
27522
+ if (https.type === "letsencrypt") {
27523
+ services.certbot = {
27524
+ image: CERTBOT_IMAGE,
27525
+ restart: "unless-stopped",
27526
+ entrypoint: ["sh", "/run/hai/renew-https.sh"],
27527
+ volumes: [
27528
+ "../../https/acme:/etc/letsencrypt",
27529
+ "../../https/webroot:/var/www/certbot",
27530
+ `../../https/certificates/${https.domain}:/output`,
27531
+ "./renew-https.sh:/run/hai/renew-https.sh:ro"
27532
+ ],
27533
+ networks: ["hai-app"]
27534
+ };
27535
+ }
27536
+ }
27176
27537
  const compose = {
27177
27538
  "x-hai-version": options.appVersion,
27178
27539
  services,
@@ -27191,12 +27552,22 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
27191
27552
  const webPort = options.webPort ?? 8080;
27192
27553
  const webBasePath = options.webBasePath ?? "";
27193
27554
  const apiPath = apiPrefix.replace(/\/+$/, "");
27194
- if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535) || webBasePath && (webBasePath === apiPath || webBasePath.startsWith(`${apiPath}/`))) {
27555
+ const backendPaths = [...new Set(options.backendPaths ?? ["/health"])];
27556
+ if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535) || backendPaths.some((path) => !/^\/[\w./-]*$/.test(path) || path.startsWith("//") || path.split("/").some((part) => part === "." || part === "..") || path === `${webBasePath}/runtime-config.js` || path === "/_hai/web-health") || webBasePath && (backendPaths.includes(webBasePath) || webBasePath === apiPath || webBasePath.startsWith(`${apiPath}/`))) {
27195
27557
  throw new Error("Invalid web proxy target");
27196
27558
  }
27197
27559
  const webLocation = renderWebLocation(webBasePath);
27198
- const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, webBasePath]) : "";
27199
- return `server {
27560
+ const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, ...backendPaths, ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], options.storageBasePath) : "";
27561
+ const backendLocations = backendPaths.map((path) => ` location = ${path} {
27562
+ proxy_pass http://serv:${port};
27563
+ proxy_http_version 1.1;
27564
+ proxy_set_header Host $http_host;
27565
+ proxy_set_header X-Forwarded-Proto $scheme;
27566
+ proxy_set_header X-Forwarded-For $remote_addr;
27567
+ proxy_set_header X-Real-IP $remote_addr;
27568
+ }
27569
+ `).join("");
27570
+ return `${options.storageBucket ? renderStorageUpstream(options.storageBucket, options.storageBasePath) : ""}server {
27200
27571
  listen ${webPort};
27201
27572
  server_name _;
27202
27573
  ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} root /usr/share/nginx/html;
@@ -27210,13 +27581,7 @@ ${storageLocation}${webLocation} location = ${webBasePath}/runtime-config.js {
27210
27581
  access_log off;
27211
27582
  return 200 'ok';
27212
27583
  }
27213
- location = /health {
27214
- proxy_pass http://serv:${port};
27215
- proxy_set_header Host $http_host;
27216
- proxy_set_header X-Forwarded-For $remote_addr;
27217
- proxy_set_header X-Real-IP $remote_addr;
27218
- }
27219
- location ${apiPrefix.replace(/\/$/, "")}/ {
27584
+ ${backendLocations} location ${apiPrefix.replace(/\/$/, "")}/ {
27220
27585
  client_max_body_size ${bodyLimits.encryptedPayloadBytes};
27221
27586
  proxy_pass http://serv:${port};
27222
27587
  proxy_http_version 1.1;
@@ -27232,9 +27597,11 @@ ${storageLocation}${webLocation} location = ${webBasePath}/runtime-config.js {
27232
27597
  }
27233
27598
  `;
27234
27599
  }
27235
- function renderStorageLocation(bucket, reservedPaths) {
27236
- const path = `/${bucket}`;
27237
- if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || ["health", "_hai"].includes(bucket) || reservedPaths.some((reserved) => reserved && (reserved === path || reserved.startsWith(`${path}/`) || path.startsWith(`${reserved}/`)))) {
27600
+ function renderStorageLocation(bucket, reservedPaths, storageBasePath = "") {
27601
+ renderWebLocation(storageBasePath);
27602
+ const path = `${storageBasePath}/${bucket}`;
27603
+ const reserved = ["/health", "/_hai", ...reservedPaths].filter(Boolean);
27604
+ if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || ["health", "_hai"].includes(bucket) || reserved.some((item) => item === path || item.startsWith(`${path}/`) || path.startsWith(`${item}/`) || storageBasePath && (item === storageBasePath || item.startsWith(`${storageBasePath}/`) || storageBasePath.startsWith(`${item}/`)))) {
27238
27605
  throw new Error("Storage bucket conflicts with application routes or is not a valid DNS bucket name");
27239
27606
  }
27240
27607
  return ` location ^~ ${path}/ {
@@ -27251,7 +27618,8 @@ function renderStorageLocation(bucket, reservedPaths) {
27251
27618
  add_header Cache-Control "private, no-store" always;
27252
27619
  limit_except GET PUT OPTIONS { deny all; }
27253
27620
  client_max_body_size 0;
27254
- proxy_pass http://storage:9000;
27621
+ ${storageBasePath ? ` # map \u4FDD\u7559\u539F\u59CB URI \u4E0E\u67E5\u8BE2\uFF1B\u4E0D\u5728 location \u5185\u521B\u5EFA\u6539\u53D8\u4EE3\u7406\u7EE7\u627F\u7684 if \u914D\u7F6E\u3002
27622
+ proxy_pass http://hai_storage_proxy$hai_storage_uri;` : " proxy_pass http://storage:9000;"}
27255
27623
  proxy_http_version 1.1;
27256
27624
  proxy_set_header Host $http_host;
27257
27625
  proxy_set_header Connection "";
@@ -27263,16 +27631,27 @@ function renderStorageLocation(bucket, reservedPaths) {
27263
27631
  }
27264
27632
  `;
27265
27633
  }
27634
+ function renderStorageUpstream(bucket, storageBasePath = "") {
27635
+ if (!storageBasePath)
27636
+ return "";
27637
+ const path = `${storageBasePath}/${bucket}`.replaceAll(".", "\\\\.");
27638
+ return `upstream hai_storage_proxy { server storage:9000; }
27639
+ map $request_uri $hai_storage_uri {
27640
+ default $request_uri;
27641
+ "~^${path}(/.*)$" /${bucket}$1;
27642
+ }
27643
+ `;
27644
+ }
27266
27645
  function generateApplicationProxyConfig(options) {
27267
27646
  const { primaryService, port, bucket, reservedPaths, maxPlaintextBytes } = options;
27268
27647
  if (!Number.isInteger(port) || port < 1 || port > 65535)
27269
27648
  throw new Error("Invalid application proxy target");
27270
27649
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
27271
- return `server {
27650
+ return `${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
27272
27651
  listen 8080;
27273
27652
  server_name _;
27274
27653
  ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
27275
- ${renderStorageLocation(bucket, reservedPaths)} location = /_hai/gateway-health {
27654
+ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
27276
27655
  access_log off;
27277
27656
  return 200 'ok';
27278
27657
  }
@@ -27380,9 +27759,22 @@ function prepareRequestAccess(options) {
27380
27759
  }
27381
27760
  }
27382
27761
  var HEALTH_MAX_ATTEMPTS = 40;
27762
+ function servicePath(value, fallback) {
27763
+ const path = value ?? fallback;
27764
+ if (typeof path !== "string" || !/^\/[\w./-]*$/.test(path) || path.startsWith("//") || path.split("/").some((part) => part === "." || part === ".."))
27765
+ throw new Error("Invalid service endpoint path");
27766
+ return path;
27767
+ }
27768
+ function endpointSettings(value) {
27769
+ if (value === void 0)
27770
+ return {};
27771
+ if (!value || typeof value !== "object" || Array.isArray(value))
27772
+ throw new Error("Invalid service endpoint configuration");
27773
+ return value;
27774
+ }
27383
27775
  var ServerResultSchema = external_exports.object({
27384
27776
  deploymentId: external_exports.string(),
27385
- stage: external_exports.enum(["preflight", "credentials", "recover", "images", "dependencies", "start", "health", "complete"]),
27777
+ stage: external_exports.enum(["preflight", "credentials", "recover", "images", "dependencies", "start", "https", "health", "complete"]),
27386
27778
  exitCode: external_exports.number().int().min(0).max(255),
27387
27779
  durationSeconds: external_exports.number().int().nonnegative(),
27388
27780
  recoveryStatus: external_exports.enum(["not-needed", "passed", "failed", "stopped"]).optional()
@@ -27394,7 +27786,7 @@ function makeDeploymentId() {
27394
27786
  return `${stamp}-${randomBytes(3).toString("hex")}`;
27395
27787
  }
27396
27788
  function sleep(ms) {
27397
- return new Promise((resolve8) => setTimeout(resolve8, ms));
27789
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
27398
27790
  }
27399
27791
  function createDockerSshProvider(config2, container) {
27400
27792
  const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
@@ -27418,13 +27810,28 @@ function createDockerSshProvider(config2, container) {
27418
27810
  deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
27419
27811
  );
27420
27812
  }
27421
- async function healthCheck(url2) {
27813
+ async function healthCheck(url2, certificate) {
27422
27814
  for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
27423
27815
  try {
27424
- const response = await fetch(url2, { signal: AbortSignal.timeout(5e3) });
27425
- await response.body?.cancel();
27426
- if (response.ok)
27427
- return true;
27816
+ if (certificate) {
27817
+ const healthy = await new Promise((resolve9) => {
27818
+ const request$1 = request(url2, { ca: [...rootCertificates, certificate], timeout: 5e3 }, (response) => {
27819
+ response.resume();
27820
+ resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
27821
+ });
27822
+ request$1.on("error", () => resolve9(false));
27823
+ request$1.on("close", () => resolve9(false));
27824
+ request$1.on("timeout", () => request$1.destroy());
27825
+ request$1.end();
27826
+ });
27827
+ if (healthy)
27828
+ return true;
27829
+ } else {
27830
+ const response = await fetch(url2, { signal: AbortSignal.timeout(5e3) });
27831
+ await response.body?.cancel();
27832
+ if (response.ok)
27833
+ return true;
27834
+ }
27428
27835
  } catch {
27429
27836
  }
27430
27837
  await sleep(HEALTH_POLL_INTERVAL);
@@ -27435,6 +27842,8 @@ function createDockerSshProvider(config2, container) {
27435
27842
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
27436
27843
  const deploymentId = makeDeploymentId();
27437
27844
  const hostPort = config2.expose.hostPort;
27845
+ const https = config2.expose.https;
27846
+ const httpsAssets = https ? createHttpsAssets(https, resolveProjectDirectory(appDir)) : void 0;
27438
27847
  const deploymentRoot = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
27439
27848
  const artifact = context.artifact;
27440
27849
  const resolvedBuildDir = context.artifactDir;
@@ -27453,26 +27862,32 @@ function createDockerSshProvider(config2, container) {
27453
27862
  const containerPort = app?.containerPort ?? (primaryService === "serv" ? artifact.servContainerPort : artifact.webContainerPort) ?? config2.expose.containerPort;
27454
27863
  const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
27455
27864
  const host = config2.ssh?.host;
27456
- const url2 = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
27865
+ const url2 = https ? `https://${https.domain}${https.port === 443 ? "" : `:${https.port}`}` : host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
27457
27866
  const webBasePath = artifact.webBasePath ?? "";
27458
27867
  const applicationUrl = `${url2}${webBasePath ? `${webBasePath}/` : ""}`;
27459
- const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && !!webBasePath;
27460
- const healthPaths = hasWebProxy ? ["/health", "/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
27868
+ const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && (!!webBasePath || !!https);
27869
+ const healthPaths = hasWebProxy ? ["/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : primaryService === "serv" ? [] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
27461
27870
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
27871
+ if (https && scan.isSvelteKit && !env.ORIGIN)
27872
+ env.ORIGIN = url2;
27462
27873
  const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
27874
+ let storageBasePath = "";
27463
27875
  if (managedStorage?.publicEndpoint !== void 0) {
27464
27876
  try {
27465
27877
  if (typeof managedStorage.publicEndpoint !== "string")
27466
27878
  throw new Error("Invalid public endpoint type");
27879
+ const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
27467
27880
  const endpoint = new URL(managedStorage.publicEndpoint);
27468
- if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash)
27881
+ 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)) {
27469
27882
  throw new Error("Invalid public endpoint origin");
27883
+ }
27884
+ storageBasePath = endpoint.pathname.replace(/\/+$/, "");
27470
27885
  } catch {
27471
27886
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storagePublicEndpointInvalid"));
27472
27887
  }
27473
27888
  }
27474
27889
  const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
27475
- const gateway = !!managedStorage && !hasWebProxy;
27890
+ const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
27476
27891
  if (gateway && primaryService === "web" && !scan.isSvelteKit)
27477
27892
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
27478
27893
  const proxyService = gateway ? "gateway" : "web";
@@ -27483,6 +27898,35 @@ function createDockerSshProvider(config2, container) {
27483
27898
  const server = existsSync(serverPath) || hasWebProxy ? loadDeploymentConfig("serv", serverPath, env) : ok({});
27484
27899
  if (!server.success)
27485
27900
  return server;
27901
+ const backendPaths = [];
27902
+ try {
27903
+ const http = endpointSettings(server.data.http);
27904
+ if (primaryService === "serv" && http.health !== false) {
27905
+ const health = endpointSettings(http.health);
27906
+ const path = servicePath(health.path, "/health");
27907
+ healthPaths.unshift(path);
27908
+ backendPaths.push(path, servicePath(health.readyPath, "/ready"));
27909
+ }
27910
+ if (hasWebProxy) {
27911
+ const corePath = join(appDir, "config", "_core.yml");
27912
+ const settings = existsSync(corePath) ? loadDeploymentConfig("core", corePath, env) : ok(env.HAI_CORE ? JSON.parse(env.HAI_CORE) : { env: env.HAI_CORE_ENV });
27913
+ if (!settings.success)
27914
+ return settings;
27915
+ const application = CoreConfigSchema.safeParse(settings.data);
27916
+ if (!application.success)
27917
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
27918
+ if (application.data.env !== "production") {
27919
+ if (http.openapi !== void 0 && http.openapi !== false)
27920
+ backendPaths.push(servicePath(endpointSettings(http.openapi).path, "/openapi.json"));
27921
+ if (http.docs !== void 0 && http.docs !== false) {
27922
+ const docsPath = servicePath(endpointSettings(http.docs).path, "/docs");
27923
+ backendPaths.push(docsPath, `${docsPath.slice(0, docsPath.lastIndexOf("/") + 1)}_hai/scalar.js`);
27924
+ }
27925
+ }
27926
+ }
27927
+ } catch {
27928
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
27929
+ }
27486
27930
  let requestAccessSnapshot;
27487
27931
  if (updatesApp && primaryService === "serv") {
27488
27932
  const prepared2 = prepareRequestAccess({ settings: server.data, module: "serv", directory: appDir });
@@ -27544,25 +27988,25 @@ function createDockerSshProvider(config2, container) {
27544
27988
  if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix)) {
27545
27989
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
27546
27990
  }
27547
- if (webBasePath && (["/health", "/_hai/web-health"].includes(webBasePath) || webBasePath === apiPrefix.replace(/\/+$/, "") || webBasePath.startsWith(`${apiPrefix.replace(/\/+$/, "")}/`)))
27991
+ if (webBasePath && ([...backendPaths, "/_hai/web-health"].includes(webBasePath) || webBasePath === apiPrefix.replace(/\/+$/, "") || webBasePath.startsWith(`${apiPrefix.replace(/\/+$/, "")}/`)))
27548
27992
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
27549
27993
  const transport = server.data.transport;
27550
27994
  try {
27551
27995
  const configured = transport ? transport.maxPlaintextBytes : void 0;
27552
27996
  const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
27553
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, storageBucket: managedStorage?.bucket, trustedEdgeProxies: config2.trustedEdgeProxies });
27997
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, backendPaths, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config2.trustedEdgeProxies });
27554
27998
  } catch (error62) {
27555
27999
  return err(HaiDeployError.CONFIG_ERROR, error62 instanceof RangeError ? error62.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
27556
28000
  }
27557
- env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
27558
- } else if (gateway && managedStorage) {
28001
+ env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && (config2.ssh || https) ? url2 : cors?.origin ?? "*" } });
28002
+ } else if (gateway) {
27559
28003
  try {
27560
28004
  const http = server.data.http;
27561
28005
  if (primaryService === "serv") {
27562
28006
  const transport = server.data.transport;
27563
28007
  applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
27564
28008
  }
27565
- webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage.bucket, reservedPaths: [webBasePath, http?.apiPrefix ?? "/api/v1"], maxPlaintextBytes: applicationPlaintextBytes, trustedEdgeProxies: config2.trustedEdgeProxies });
28009
+ 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: config2.trustedEdgeProxies });
27566
28010
  } catch {
27567
28011
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
27568
28012
  }
@@ -27571,8 +28015,15 @@ function createDockerSshProvider(config2, container) {
27571
28015
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
27572
28016
  webProxy = generateStaticWebConfig(containerPort, webBasePath);
27573
28017
  }
27574
- const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`);
27575
- const stem = basename(requestedOutput).replace(/\.tar$/i, "");
28018
+ if (https && webProxy)
28019
+ webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
28020
+ if (https && primaryService === "serv" && !hasWebProxy) {
28021
+ const cors = server.data.cors;
28022
+ env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: !cors?.origin || cors.origin === "*" ? url2 : cors.origin } });
28023
+ }
28024
+ const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
28025
+ const compressed = requestedOutput.endsWith(".tar.gz");
28026
+ const stem = basename(requestedOutput).replace(/\.tar(?:\.gz)?$/i, "");
27576
28027
  const suffix = `-${artifact.appVersion}`;
27577
28028
  const name = stem.endsWith(suffix) ? stem : `${stem}${suffix}`;
27578
28029
  if (!/^[\w.+-]+$/.test(name))
@@ -27605,10 +28056,13 @@ function createDockerSshProvider(config2, container) {
27605
28056
  containerPort,
27606
28057
  primaryService,
27607
28058
  requestAccessPolicy: requestAccessSnapshot !== void 0,
28059
+ nodeRuntime: primaryService === "serv" || scan.isSvelteKit,
28060
+ nodeOptions: env.NODE_OPTIONS,
27608
28061
  sidecar,
27609
28062
  web: hasWebProxy ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0,
27610
28063
  mountWebConfig: standaloneWebConfig,
27611
- gateway
28064
+ gateway,
28065
+ https
27612
28066
  });
27613
28067
  const composePath = join(artifactDir, composeName);
27614
28068
  writeFileSync(composePath, composeContent);
@@ -27617,6 +28071,8 @@ function createDockerSshProvider(config2, container) {
27617
28071
  const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
27618
28072
  if (gateway && !partial2)
27619
28073
  selected.push("gateway");
28074
+ if (https?.type === "letsencrypt" && !partial2)
28075
+ selected.push("certbot");
27620
28076
  if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
27621
28077
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
27622
28078
  for (const name2 of selected) {
@@ -27641,7 +28097,9 @@ function createDockerSshProvider(config2, container) {
27641
28097
  containerPort,
27642
28098
  middlewareEnv: serializeEnvFile(sidecar.env),
27643
28099
  healthPaths,
27644
- output: join(artifactDir, `${name}.tar`)
28100
+ httpsAssets,
28101
+ output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
28102
+ cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
27645
28103
  });
27646
28104
  if (options?.bundleOnly)
27647
28105
  return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
@@ -27663,7 +28121,7 @@ function createDockerSshProvider(config2, container) {
27663
28121
  return uploaded;
27664
28122
  let pendingOutput = "";
27665
28123
  const started = await ssh.exec(
27666
- `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)}`,
28124
+ `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; }`,
27667
28125
  { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output2) => {
27668
28126
  const parts = output2.split("\n");
27669
28127
  for (let index = 0; index < parts.length; index++) {
@@ -27672,7 +28130,7 @@ function createDockerSshProvider(config2, container) {
27672
28130
  break;
27673
28131
  const line = pendingOutput;
27674
28132
  pendingOutput = "";
27675
- const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
28133
+ const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
27676
28134
  if (stage)
27677
28135
  options?.onProgress?.(stage);
27678
28136
  }
@@ -27705,7 +28163,7 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
27705
28163
  logger6.info("Running health check", { url: applicationUrl });
27706
28164
  let healthy = true;
27707
28165
  for (const healthPath of healthPaths) {
27708
- if (!await healthCheck(`${url2}${healthPath}`)) {
28166
+ if (!await healthCheck(`${url2}${healthPath}`, httpsAssets?.["tls/fullchain.pem"])) {
27709
28167
  healthy = false;
27710
28168
  break;
27711
28169
  }
@@ -27992,7 +28450,7 @@ async function pollDeploymentStatus(token, deploymentId) {
27992
28450
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
27993
28451
  return "error";
27994
28452
  }
27995
- await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL));
28453
+ await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL));
27996
28454
  }
27997
28455
  return "building";
27998
28456
  }
@@ -28527,7 +28985,7 @@ var deployCredentials = {
28527
28985
  saveAll: (entries) => saveCredentials(entries)
28528
28986
  };
28529
28987
  function mergeProvisionEnv(results, scan, options) {
28530
- let env = {};
28988
+ const env = {};
28531
28989
  const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
28532
28990
  const reachProviders = reachRequirements.map((item) => item.config);
28533
28991
  let hasReachOutput = false;
@@ -28542,13 +29000,16 @@ function mergeProvisionEnv(results, scan, options) {
28542
29000
  });
28543
29001
  if (targets.length) {
28544
29002
  for (const target of targets)
28545
- reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
29003
+ reachProviders[reachRequirements.indexOf(target)] = { ...mergeManagedMiddlewareConfig(target, provider), name: target.config.name ?? provider.name };
28546
29004
  } else {
28547
29005
  reachProviders.push(provider);
28548
29006
  }
28549
29007
  }
28550
29008
  }
28551
- env = { ...env, ...prov.envVars };
29009
+ for (const [name, value] of Object.entries(prov.envVars)) {
29010
+ const requirement = scan?.middlewares?.find((item) => item.envPrefix === name);
29011
+ env[name] = requirement ? JSON.stringify(mergeManagedMiddlewareConfig(requirement, JSON.parse(value))) : value;
29012
+ }
28552
29013
  }
28553
29014
  if (hasReachOutput)
28554
29015
  env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
@@ -28969,7 +29430,8 @@ async function createDeploymentPlan(appDir, input2, options = {}) {
28969
29430
  }
28970
29431
  }
28971
29432
  const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config2.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
28972
- const gateway = !!artifact && sidecar?.services.includes("storage") && !(artifact.primaryService === "serv" && artifact.hasWeb);
29433
+ const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
29434
+ const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (sidecar?.services.includes("storage") || !!https && (artifact.primaryService === "serv" || app.isSvelteKit));
28973
29435
  if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
28974
29436
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
28975
29437
  if (gateway) {
@@ -28977,18 +29439,23 @@ async function createDeploymentPlan(appDir, input2, options = {}) {
28977
29439
  if (!baseline.required)
28978
29440
  selected.push("gateway");
28979
29441
  }
29442
+ if (https?.type === "letsencrypt") {
29443
+ available.push("certbot");
29444
+ if (!baseline.required)
29445
+ selected.push("certbot");
29446
+ }
28980
29447
  const services = Object.entries(sidecar?.composeServices ?? {}).filter(([name]) => selected.includes(name));
28981
29448
  return ok({
28982
29449
  projectName,
28983
29450
  root,
28984
- appDir: directory,
29451
+ serviceDir: directory,
28985
29452
  environment: environment.data.environment,
28986
- 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 } : { provider: provider.type },
29453
+ 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 },
28987
29454
  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,
28988
29455
  selected,
28989
29456
  retained: available.filter((name) => !selected.includes(name)),
28990
29457
  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] : [])] : [],
28991
- composeManagedServices: services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name),
29458
+ composeManagedServices: [...services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name), ...selected.includes("certbot") ? ["certbot"] : []],
28992
29459
  logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
28993
29460
  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) })),
28994
29461
  configuration: { files: [join(root, "hai.json"), ...configFiles].filter(existsSync), environmentKeys: Object.keys(environment.data.values).sort() },
@@ -29250,5 +29717,5 @@ async function acquireImportLock(path, root) {
29250
29717
  }
29251
29718
 
29252
29719
  export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, createDeploymentPlan, deploy, describeMiddlewareBackend, external_exports, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, readDeploymentArtifact, saveDeploymentEnvironment };
29253
- //# sourceMappingURL=chunk-M7HN2OE3.js.map
29254
- //# sourceMappingURL=chunk-M7HN2OE3.js.map
29720
+ //# sourceMappingURL=chunk-K6RWTTHD.js.map
29721
+ //# sourceMappingURL=chunk-K6RWTTHD.js.map