@h-ai/cli 1.0.0-beta.2 → 1.0.0-beta.4

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 (58) 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-MUHRBJSW.js → chunk-K6RWTTHD.js} +546 -97
  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-EF7WOFWZ.js → chunk-YVNNMC4H.js} +364 -136
  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-HHFHNXBN.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 +117 -208
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -1
  17. package/dist/{src-TEBHR5I5.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/hai-ai/guide.md +5 -3
  45. package/templates/skills/hai-app-create/development.md +5 -3
  46. package/templates/skills/hai-app-create/reference.md +1 -1
  47. package/templates/skills/hai-build/SKILL.md +5 -5
  48. package/templates/skills/hai-build/paths.md +1 -1
  49. package/templates/skills/hai-build/reference.md +1 -1
  50. package/templates/skills/hai-core/request-access.md +1 -1
  51. package/templates/skills/hai-deploy/SKILL.md +15 -3
  52. package/templates/skills/hai-deploy/middleware.md +4 -2
  53. package/templates/skills/hai-serv/reference.md +5 -8
  54. package/dist/chunk-AKV2CQZV.js.map +0 -1
  55. package/dist/chunk-EF7WOFWZ.js.map +0 -1
  56. package/dist/chunk-MUHRBJSW.js.map +0 -1
  57. package/dist/cli-config-templates-HHFHNXBN.js +0 -5
  58. package/dist/src-TEBHR5I5.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
  }
@@ -22614,7 +22619,7 @@ var en_US_default = {
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
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
@@ -22722,7 +22729,7 @@ var ja_JP_default = {
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
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
@@ -22830,7 +22839,7 @@ var zh_CN_default2 = {
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
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,11 +27552,21 @@ 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, ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], options.storageBasePath) : "";
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("");
27199
27570
  return `${options.storageBucket ? renderStorageUpstream(options.storageBucket, options.storageBasePath) : ""}server {
27200
27571
  listen ${webPort};
27201
27572
  server_name _;
@@ -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;
@@ -27282,11 +27647,11 @@ function generateApplicationProxyConfig(options) {
27282
27647
  if (!Number.isInteger(port) || port < 1 || port > 65535)
27283
27648
  throw new Error("Invalid application proxy target");
27284
27649
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
27285
- return `${renderStorageUpstream(bucket, options.storageBasePath)}server {
27650
+ return `${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
27286
27651
  listen 8080;
27287
27652
  server_name _;
27288
27653
  ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
27289
- ${renderStorageLocation(bucket, reservedPaths, options.storageBasePath)} location = /_hai/gateway-health {
27654
+ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
27290
27655
  access_log off;
27291
27656
  return 200 'ok';
27292
27657
  }
@@ -27394,9 +27759,22 @@ function prepareRequestAccess(options) {
27394
27759
  }
27395
27760
  }
27396
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
+ }
27397
27775
  var ServerResultSchema = external_exports.object({
27398
27776
  deploymentId: external_exports.string(),
27399
- 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"]),
27400
27778
  exitCode: external_exports.number().int().min(0).max(255),
27401
27779
  durationSeconds: external_exports.number().int().nonnegative(),
27402
27780
  recoveryStatus: external_exports.enum(["not-needed", "passed", "failed", "stopped"]).optional()
@@ -27408,7 +27786,7 @@ function makeDeploymentId() {
27408
27786
  return `${stamp}-${randomBytes(3).toString("hex")}`;
27409
27787
  }
27410
27788
  function sleep(ms) {
27411
- return new Promise((resolve8) => setTimeout(resolve8, ms));
27789
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
27412
27790
  }
27413
27791
  function createDockerSshProvider(config2, container) {
27414
27792
  const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
@@ -27432,13 +27810,28 @@ function createDockerSshProvider(config2, container) {
27432
27810
  deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
27433
27811
  );
27434
27812
  }
27435
- async function healthCheck(url2) {
27813
+ async function healthCheck(url2, certificate) {
27436
27814
  for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
27437
27815
  try {
27438
- const response = await fetch(url2, { signal: AbortSignal.timeout(5e3) });
27439
- await response.body?.cancel();
27440
- if (response.ok)
27441
- 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
+ }
27442
27835
  } catch {
27443
27836
  }
27444
27837
  await sleep(HEALTH_POLL_INTERVAL);
@@ -27449,6 +27842,8 @@ function createDockerSshProvider(config2, container) {
27449
27842
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
27450
27843
  const deploymentId = makeDeploymentId();
27451
27844
  const hostPort = config2.expose.hostPort;
27845
+ const https = config2.expose.https;
27846
+ const httpsAssets = https ? createHttpsAssets(https, resolveProjectDirectory(appDir)) : void 0;
27452
27847
  const deploymentRoot = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
27453
27848
  const artifact = context.artifact;
27454
27849
  const resolvedBuildDir = context.artifactDir;
@@ -27467,12 +27862,14 @@ function createDockerSshProvider(config2, container) {
27467
27862
  const containerPort = app?.containerPort ?? (primaryService === "serv" ? artifact.servContainerPort : artifact.webContainerPort) ?? config2.expose.containerPort;
27468
27863
  const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
27469
27864
  const host = config2.ssh?.host;
27470
- 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}`;
27471
27866
  const webBasePath = artifact.webBasePath ?? "";
27472
27867
  const applicationUrl = `${url2}${webBasePath ? `${webBasePath}/` : ""}`;
27473
- const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && !!webBasePath;
27474
- 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"];
27475
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;
27476
27873
  const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
27477
27874
  let storageBasePath = "";
27478
27875
  if (managedStorage?.publicEndpoint !== void 0) {
@@ -27490,7 +27887,7 @@ function createDockerSshProvider(config2, container) {
27490
27887
  }
27491
27888
  }
27492
27889
  const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
27493
- const gateway = !!managedStorage && !hasWebProxy;
27890
+ const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
27494
27891
  if (gateway && primaryService === "web" && !scan.isSvelteKit)
27495
27892
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
27496
27893
  const proxyService = gateway ? "gateway" : "web";
@@ -27501,6 +27898,35 @@ function createDockerSshProvider(config2, container) {
27501
27898
  const server = existsSync(serverPath) || hasWebProxy ? loadDeploymentConfig("serv", serverPath, env) : ok({});
27502
27899
  if (!server.success)
27503
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
+ }
27504
27930
  let requestAccessSnapshot;
27505
27931
  if (updatesApp && primaryService === "serv") {
27506
27932
  const prepared2 = prepareRequestAccess({ settings: server.data, module: "serv", directory: appDir });
@@ -27562,25 +27988,25 @@ function createDockerSshProvider(config2, container) {
27562
27988
  if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix)) {
27563
27989
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
27564
27990
  }
27565
- 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(/\/+$/, "")}/`)))
27566
27992
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
27567
27993
  const transport = server.data.transport;
27568
27994
  try {
27569
27995
  const configured = transport ? transport.maxPlaintextBytes : void 0;
27570
27996
  const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
27571
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config2.trustedEdgeProxies });
27997
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, backendPaths, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config2.trustedEdgeProxies });
27572
27998
  } catch (error62) {
27573
27999
  return err(HaiDeployError.CONFIG_ERROR, error62 instanceof RangeError ? error62.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
27574
28000
  }
27575
- env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
27576
- } 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) {
27577
28003
  try {
27578
28004
  const http = server.data.http;
27579
28005
  if (primaryService === "serv") {
27580
28006
  const transport = server.data.transport;
27581
28007
  applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
27582
28008
  }
27583
- 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 });
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 });
27584
28010
  } catch {
27585
28011
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
27586
28012
  }
@@ -27589,8 +28015,15 @@ function createDockerSshProvider(config2, container) {
27589
28015
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
27590
28016
  webProxy = generateStaticWebConfig(containerPort, webBasePath);
27591
28017
  }
27592
- const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`);
27593
- 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, "");
27594
28027
  const suffix = `-${artifact.appVersion}`;
27595
28028
  const name = stem.endsWith(suffix) ? stem : `${stem}${suffix}`;
27596
28029
  if (!/^[\w.+-]+$/.test(name))
@@ -27623,10 +28056,13 @@ function createDockerSshProvider(config2, container) {
27623
28056
  containerPort,
27624
28057
  primaryService,
27625
28058
  requestAccessPolicy: requestAccessSnapshot !== void 0,
28059
+ nodeRuntime: primaryService === "serv" || scan.isSvelteKit,
28060
+ nodeOptions: env.NODE_OPTIONS,
27626
28061
  sidecar,
27627
28062
  web: hasWebProxy ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0,
27628
28063
  mountWebConfig: standaloneWebConfig,
27629
- gateway
28064
+ gateway,
28065
+ https
27630
28066
  });
27631
28067
  const composePath = join(artifactDir, composeName);
27632
28068
  writeFileSync(composePath, composeContent);
@@ -27635,6 +28071,8 @@ function createDockerSshProvider(config2, container) {
27635
28071
  const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
27636
28072
  if (gateway && !partial2)
27637
28073
  selected.push("gateway");
28074
+ if (https?.type === "letsencrypt" && !partial2)
28075
+ selected.push("certbot");
27638
28076
  if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
27639
28077
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
27640
28078
  for (const name2 of selected) {
@@ -27659,7 +28097,9 @@ function createDockerSshProvider(config2, container) {
27659
28097
  containerPort,
27660
28098
  middlewareEnv: serializeEnvFile(sidecar.env),
27661
28099
  healthPaths,
27662
- output: join(artifactDir, `${name}.tar`)
28100
+ httpsAssets,
28101
+ output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
28102
+ cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
27663
28103
  });
27664
28104
  if (options?.bundleOnly)
27665
28105
  return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
@@ -27681,7 +28121,7 @@ function createDockerSshProvider(config2, container) {
27681
28121
  return uploaded;
27682
28122
  let pendingOutput = "";
27683
28123
  const started = await ssh.exec(
27684
- `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; }`,
27685
28125
  { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output2) => {
27686
28126
  const parts = output2.split("\n");
27687
28127
  for (let index = 0; index < parts.length; index++) {
@@ -27690,7 +28130,7 @@ function createDockerSshProvider(config2, container) {
27690
28130
  break;
27691
28131
  const line = pendingOutput;
27692
28132
  pendingOutput = "";
27693
- const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
28133
+ const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
27694
28134
  if (stage)
27695
28135
  options?.onProgress?.(stage);
27696
28136
  }
@@ -27723,7 +28163,7 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
27723
28163
  logger6.info("Running health check", { url: applicationUrl });
27724
28164
  let healthy = true;
27725
28165
  for (const healthPath of healthPaths) {
27726
- if (!await healthCheck(`${url2}${healthPath}`)) {
28166
+ if (!await healthCheck(`${url2}${healthPath}`, httpsAssets?.["tls/fullchain.pem"])) {
27727
28167
  healthy = false;
27728
28168
  break;
27729
28169
  }
@@ -28010,7 +28450,7 @@ async function pollDeploymentStatus(token, deploymentId) {
28010
28450
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
28011
28451
  return "error";
28012
28452
  }
28013
- await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL));
28453
+ await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL));
28014
28454
  }
28015
28455
  return "building";
28016
28456
  }
@@ -28545,7 +28985,7 @@ var deployCredentials = {
28545
28985
  saveAll: (entries) => saveCredentials(entries)
28546
28986
  };
28547
28987
  function mergeProvisionEnv(results, scan, options) {
28548
- let env = {};
28988
+ const env = {};
28549
28989
  const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
28550
28990
  const reachProviders = reachRequirements.map((item) => item.config);
28551
28991
  let hasReachOutput = false;
@@ -28560,13 +29000,16 @@ function mergeProvisionEnv(results, scan, options) {
28560
29000
  });
28561
29001
  if (targets.length) {
28562
29002
  for (const target of targets)
28563
- 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 };
28564
29004
  } else {
28565
29005
  reachProviders.push(provider);
28566
29006
  }
28567
29007
  }
28568
29008
  }
28569
- 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
+ }
28570
29013
  }
28571
29014
  if (hasReachOutput)
28572
29015
  env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
@@ -28987,7 +29430,8 @@ async function createDeploymentPlan(appDir, input2, options = {}) {
28987
29430
  }
28988
29431
  }
28989
29432
  const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config2.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
28990
- 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));
28991
29435
  if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
28992
29436
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
28993
29437
  if (gateway) {
@@ -28995,18 +29439,23 @@ async function createDeploymentPlan(appDir, input2, options = {}) {
28995
29439
  if (!baseline.required)
28996
29440
  selected.push("gateway");
28997
29441
  }
29442
+ if (https?.type === "letsencrypt") {
29443
+ available.push("certbot");
29444
+ if (!baseline.required)
29445
+ selected.push("certbot");
29446
+ }
28998
29447
  const services = Object.entries(sidecar?.composeServices ?? {}).filter(([name]) => selected.includes(name));
28999
29448
  return ok({
29000
29449
  projectName,
29001
29450
  root,
29002
- appDir: directory,
29451
+ serviceDir: directory,
29003
29452
  environment: environment.data.environment,
29004
- 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 },
29005
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,
29006
29455
  selected,
29007
29456
  retained: available.filter((name) => !selected.includes(name)),
29008
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] : [])] : [],
29009
- 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"] : []],
29010
29459
  logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
29011
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) })),
29012
29461
  configuration: { files: [join(root, "hai.json"), ...configFiles].filter(existsSync), environmentKeys: Object.keys(environment.data.values).sort() },
@@ -29268,5 +29717,5 @@ async function acquireImportLock(path, root) {
29268
29717
  }
29269
29718
 
29270
29719
  export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, createDeploymentPlan, deploy, describeMiddlewareBackend, external_exports, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, readDeploymentArtifact, saveDeploymentEnvironment };
29271
- //# sourceMappingURL=chunk-MUHRBJSW.js.map
29272
- //# sourceMappingURL=chunk-MUHRBJSW.js.map
29720
+ //# sourceMappingURL=chunk-K6RWTTHD.js.map
29721
+ //# sourceMappingURL=chunk-K6RWTTHD.js.map