@h-ai/deploy 1.0.0-beta.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -118,7 +118,7 @@ Runtime lockfile dependencies and configuration determine middleware. Choose def
118
118
 
119
119
  #### Direct private object storage access
120
120
 
121
- Self-hosted S3 uses an internal endpoint; configure `_storage.yml` publicEndpoint with the browser-accessible origin, such as `https://app.example.com`, without a bucket or website path. Fullstack Web Nginx and the generated SSR/API gateway forward `/<bucket>/` directly to storage:9000, preserve signed Host/URI/query and disable object buffering/signature logs. Storage ports and consoles remain private; S3 validates authorization. Configure cross-origin CORS at S3 and preserve the bucket route at external ingress, including sites hosted below a prefix.
121
+ Self-hosted S3 uses an internal endpoint. Set `_storage.yml` publicEndpoint to an external address such as `https://app.example.com/delivery/files`. Nginx exposes `/delivery/files/<bucket>/...`, removes the proxy directory and forwards the original bucket URI, signature query and Host directly to storage:9000. Files bypass Serv; omitting the directory retains root bucket routes. A route may sit under the website directory; API, asset and health conflicts fail packaging. Storage ports/consoles remain private, buffering/signature logs are disabled, and S3 handles authorization and cross-origin CORS. External ingress must retain the full entry path and Host so only the SDK gateway removes the prefix.
122
122
 
123
123
  Managed deployments retain bucket/region/prefix/publicEndpoint/publicUrl/operationLog while replacing internal connections and managed credentials. Adding the gateway or changing access settings requires a full baseline; partial preflight rejects mismatching configuration fingerprints. App/storage partial updates refresh retained proxy DNS, and recovery restores the previous topology. External/kept storage gets no local sidecar route; Local does not expose private directories. See the [storage handbook](../../docs/usage/configuration/data.md).
124
124
 
@@ -261,6 +261,8 @@ Serv `http.access` and SvelteKit `access` can reference separate yml/yaml files.
261
261
 
262
262
  Full deployments and backend increments carry their own snapshot; frontend-only increments retain the backend mount. Failed deployment recovery uses the previous successful release's Compose and policy file. Non-root containers can read the policy inside a private release directory. Image and policy rollback does not undo database migrations or completed business effects.
263
263
 
264
+ For operator-managed rules that reload on restart, manage a read-only file outside the bundle and use a Compose override to replace the same `/run/hai/request-access.yml` mount. SDK rollback does not restore this external file. Recreate the container once to apply the mount; later file changes require a restart. Changed `env_file` variables still require container recreation, usually without rebuilding the image. See [mounted policy setup and verification](../../docs/usage/configuration/request-access.md#44-运维独立管理文件修改后重启生效).
265
+
264
266
  Managed Compose sets `HAI_DEPLOY_PRIVATE_PROXY=1` only when a Web/gateway proxy fronts a backend without public ports. Serv/Kit default `trustedProxies: auto` uses container networks there and ignores forwarded headers on other hosts. Generated Nginx overwrites client-supplied forwarding headers with the actual peer. For an upstream load balancer, explicitly set Docker SSH Provider `trustedEdgeProxies` to its IPs/CIDRs; omitting it trusts no upstream, and `[]` explicitly selects the same behavior. Source addresses lost before Nginx cannot be reconstructed, including some rootless forwarding environments. See the [request access handbook](../../docs/usage/configuration/request-access.md) for examples and verification.
265
267
 
266
268
  ## Development and testing
package/README.ja.md CHANGED
@@ -116,7 +116,7 @@ SSH 設定は選択環境の `.env.<環境>.local` に保存します。`--bundl
116
116
 
117
117
  #### 非公開オブジェクトストレージへの直接アクセス
118
118
 
119
- 自前 S3 の endpoint は内部接続先です。`_storage.yml` の publicEndpoint には `https://app.example.com` などブラウザーから到達可能なオリジンを指定し、バケットやサイトパスを含めません。全栈 Web Nginx と SSR/API 用 gateway は `/<bucket>/` を storage:9000 へ直接転送し、署名対象 Host/URI/query を保持してバッファリングと署名ログを無効化します。ストレージポートとコンソールは非公開で、権限は S3 が検証します。クロスオリジン CORS は S3 に設定し、外部入口でもサイトのディレクトリとは別にバケットルートを保持します。
119
+ 自前 S3 の endpoint は内部接続先です。publicEndpoint に `https://app.example.com/delivery/files` などの外部アドレスを設定できます。Nginx は `/delivery/files/<bucket>/...` を受け、入口パスだけを除去し、本来のバケット URI、署名クエリ、Host を保持して storage:9000 に転送します。Serv を経由せず、パス省略時は従来のルートバケット形式です。サイト配下に置けますが API、資源、ヘルスパスの競合は拒否します。ポートと管理コンソールは非公開、バッファリングと署名ログは無効です。認可と CORS は S3 に設定し、外部プロキシは入口パスと Host を保持してください。
120
120
 
121
121
  自前構成は bucket/region/prefix/publicEndpoint/publicUrl/operationLog を保持し、内部接続と受管認証情報だけを置き換えます。gateway 追加や公開設定変更には完全デプロイが必要で、部分更新の事前検証は設定摘要の不一致を拒否します。アプリ/ストレージ部分更新では保持プロキシの DNS を更新し、失敗時は以前の構成に復元します。外部/保持接続にローカルルートは生成せず、Local の非公開ディレクトリも公開しません。[ストレージ手引き](../../docs/usage/configuration/data.md)を参照してください。
122
122
 
@@ -259,6 +259,8 @@ Serv の `http.access` と SvelteKit の `access` は独立した yml/yaml を
259
259
 
260
260
  全量とバックエンドを含む増分は独自のスナップショットを持ち、フロントエンドだけの増分は旧バックエンドのマウントを維持します。失敗時は前回成功した Compose とポリシーへ復旧します。非 root コンテナでも私有リリースディレクトリ内のファイルを読み取れます。イメージと設定の復旧は DB 移行や実施済み業務を取り消しません。
261
261
 
262
+ 運用側でルールを変更して再起動で反映する場合は、配布物の外で読み取り専用ファイルを管理し、Compose override で同じ `/run/hai/request-access.yml` のマウントを置き換えます。この外部ファイルは SDK の自動復旧対象外です。初回マウント適用はコンテナ再作成、以後のファイル変更は再起動で反映できます。`env_file` の環境変数変更は引き続きコンテナ再作成が必要で、通常イメージ再構築は不要です。[設定と実測の境界](../../docs/usage/configuration/request-access.md#44-运维独立管理文件修改后重启生效)を参照してください。
263
+
262
264
  Web/gateway 入口がありバックエンドのポートが非公開の場合だけ、管理 Compose は `HAI_DEPLOY_PRIVATE_PROXY=1` を設定します。この構成の `trustedProxies: auto` はコンテナネットワークを採用し、他のホストでは転送ヘッダーを無視します。生成 Nginx は実接続元で転送ヘッダーを上書きします。外側の負荷分散器は Docker SSH Provider の `trustedEdgeProxies` に実 IP/CIDR を明記してください。省略時は上流を信頼せず、`[]` も同じ動作です。Nginx 到達前に失われた源 IP は復元できず、一部の rootless 構成も実測が必要です。[リクエスト制御手引き](../../docs/usage/configuration/request-access.md)に例と確認手順を記載しています。
263
265
 
264
266
  ## 開発とテスト
package/README.md CHANGED
@@ -129,7 +129,7 @@ SSH 参数保存到选中环境的 `.env.<环境>.local`。`--bundle-only` 在
129
129
 
130
130
  #### 私有对象存储直连
131
131
 
132
- 自建 S3 的 `endpoint` 使用 Compose 内部地址,浏览器下载须在 `_storage.yml` 配置 `publicEndpoint`,例如 `https://app.example.com`;只填源地址,不含桶名或网站目录。全栈 Web Nginx 及 SSR/API 的新增 gateway 将 `/<bucket>/` 原样代理到 storage:9000,保留签名 Host/URI/query,关闭对象缓冲与签名日志。存储端口和控制台不直接公开,实际权限由私有桶签名校验;跨源 CORS 在 S3 配置。外层入口也须原样转发桶路径,不能只转发网站子目录。
132
+ 自建 S3 的 `endpoint` 使用 Compose 内部地址,浏览器地址用 `_storage.yml` 的 `publicEndpoint` 配置,例如 `https://app.example.com/delivery/files`。Nginx 对外开放 `/delivery/files/<bucket>/...`,去掉 `/delivery/files` 后将原始桶 URI、签名查询与 Host 直接转发 storage:9000,文件不经过 Serv;省略目录时继续使用根桶路径。入口可位于网站目录内,API/资源/健康路径冲突会在打包前失败。存储端口与控制台不公开,对象缓冲和签名日志关闭;S3 负责私有桶授权和跨源 CORS。外层代理须保留完整入口路径与 Host,只由 SDK 网关去掉前缀。
133
133
 
134
134
  自建配置保留 bucket/region/prefix/publicEndpoint/publicUrl/operationLog,只替换内部连接与托管凭据。首次增加网关或改变这些访问设置须全量部署,增量预检按配置摘要拒绝不一致;普通应用/存储增量刷新保留代理的服务 DNS,失败恢复使用旧拓扑。外部/保持连接不生成本地存储路由,Local 不公开私有目录。详见[存储配置手册](../../docs/usage/configuration/data.md#配置人员区分三个访问地址)。
135
135
 
@@ -271,6 +271,8 @@ Serv 的 `http.access`、SvelteKit 的 `access` 可引用独立 yml/yaml。部
271
271
 
272
272
  全量与包含后端的增量都交付自己的快照;仅更新前端时保留后端原挂载。失败恢复使用旧成功发布的 Compose 和策略文件。运行账号无需 root:发布目录私有,策略文件在受限目录内以只读权限供容器读取。回滚镜像和策略不撤销数据库迁移或已经产生的业务效果。
273
273
 
274
+ 如需由运维独立改规则、重启后生效,可在交付之外管理只读文件,并用 Compose override 替换同一 `/run/hai/request-access.yml` 挂载。该外部文件不参与 SDK 自动回滚,首次附加挂载需要重新创建容器;后续文件修改后重启即可。`env_file` 的环境变量改变仍需重新创建容器,通常无需重建镜像。步骤与验证边界见[独立文件重启配置](../../docs/usage/configuration/request-access.md#44-运维独立管理文件修改后重启生效)。
275
+
274
276
  受管 Compose 只有存在 Web/gateway 代理且后端没有公开端口时,才设置 `HAI_DEPLOY_PRIVATE_PROXY=1`。Serv/Kit 默认 `trustedProxies: auto` 在此采用容器网段,其他宿主默认忽略转发头。生成 Nginx 用实际对端覆盖客户端提交的转发头;外层负载均衡需在 Docker SSH Provider 的 `trustedEdgeProxies` 显式配置真实 IP/CIDR,省略时不信任上游,`[]` 同样关闭信任。根本未传到 Nginx 的源 IP 无法恢复,Podman rootless 等环境应实际核验入口日志。完整示例和验证步骤见[请求访问控制手册](../../docs/usage/configuration/request-access.md)。
275
277
 
276
278
  ## 只读部署计划
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ var en_US_default = {
117
117
  deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
118
118
  deploy_webBaseConflict: "The website mount path conflicts with an API or health endpoint. Choose a different PUBLIC_BASE_PATH.",
119
119
  deploy_storageProxyInvalid: "Cannot generate the storage proxy. Use a valid DNS bucket name whose path does not overlap the website, API, or health endpoints.",
120
- deploy_storagePublicEndpointInvalid: "Invalid storage.publicEndpoint. Use a browser-accessible HTTP(S) origin without a bucket, path, credentials, query, or fragment.",
120
+ 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.",
121
121
  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.",
122
122
  deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
123
123
  deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
@@ -225,7 +225,7 @@ var ja_JP_default = {
225
225
  deploy_webProxyInvalid: "API\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3001\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30B9\u30E9\u30C3\u30B7\u30E5\u306E\u307F\u3092\u542B\u3080\u7D76\u5BFE\u30D1\u30B9\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
226
226
  deploy_webBaseConflict: "\u30B5\u30A4\u30C8\u306E\u30DE\u30A6\u30F3\u30C8\u30D1\u30B9\u304CAPI\u307E\u305F\u306F\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u3068\u7AF6\u5408\u3057\u3066\u3044\u307E\u3059\u3002\u5225\u306EPUBLIC_BASE_PATH\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
227
227
  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",
228
- deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u304C\u7121\u52B9\u3067\u3059\u3002\u30D0\u30B1\u30C3\u30C8\u3001\u30D1\u30B9\u3001\u8A8D\u8A3C\u60C5\u5831\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u3092\u542B\u307E\u306A\u3044\u3001\u30D6\u30E9\u30A6\u30B6\u30FC\u304B\u3089\u5230\u9054\u53EF\u80FD\u306A HTTP(S) \u30AA\u30EA\u30B8\u30F3\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
228
+ 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",
229
229
  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",
230
230
  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",
231
231
  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",
@@ -333,7 +333,7 @@ var zh_CN_default = {
333
333
  deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
334
334
  deploy_webBaseConflict: "\u7F51\u7AD9\u6302\u8F7D\u76EE\u5F55\u4E0E API \u6216\u5065\u5EB7\u68C0\u67E5\u5165\u53E3\u51B2\u7A81\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C\u7684 PUBLIC_BASE_PATH\u3002",
335
335
  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",
336
- deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u65E0\u6548\uFF1A\u8BF7\u586B\u5199\u6D4F\u89C8\u5668\u53EF\u8BBF\u95EE\u7684 HTTP(S) \u6E90\u5730\u5740\uFF0C\u4E0D\u80FD\u5305\u542B\u6876\u540D\u3001\u8DEF\u5F84\u3001\u51ED\u636E\u3001\u67E5\u8BE2\u6216\u7247\u6BB5\u3002",
336
+ 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",
337
337
  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",
338
338
  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",
339
339
  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",
@@ -3791,8 +3791,8 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
3791
3791
  throw new Error("Invalid web proxy target");
3792
3792
  }
3793
3793
  const webLocation = renderWebLocation(webBasePath);
3794
- const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, webBasePath]) : "";
3795
- return `server {
3794
+ const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], options.storageBasePath) : "";
3795
+ return `${options.storageBucket ? renderStorageUpstream(options.storageBucket, options.storageBasePath) : ""}server {
3796
3796
  listen ${webPort};
3797
3797
  server_name _;
3798
3798
  ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} root /usr/share/nginx/html;
@@ -3828,9 +3828,11 @@ ${storageLocation}${webLocation} location = ${webBasePath}/runtime-config.js {
3828
3828
  }
3829
3829
  `;
3830
3830
  }
3831
- function renderStorageLocation(bucket, reservedPaths) {
3832
- const path = `/${bucket}`;
3833
- if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || ["health", "_hai"].includes(bucket) || reservedPaths.some((reserved) => reserved && (reserved === path || reserved.startsWith(`${path}/`) || path.startsWith(`${reserved}/`)))) {
3831
+ function renderStorageLocation(bucket, reservedPaths, storageBasePath = "") {
3832
+ renderWebLocation(storageBasePath);
3833
+ const path = `${storageBasePath}/${bucket}`;
3834
+ const reserved = ["/health", "/_hai", ...reservedPaths].filter(Boolean);
3835
+ if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || ["health", "_hai"].includes(bucket) || reserved.some((item) => item === path || item.startsWith(`${path}/`) || path.startsWith(`${item}/`) || storageBasePath && (item === storageBasePath || item.startsWith(`${storageBasePath}/`) || storageBasePath.startsWith(`${item}/`)))) {
3834
3836
  throw new Error("Storage bucket conflicts with application routes or is not a valid DNS bucket name");
3835
3837
  }
3836
3838
  return ` location ^~ ${path}/ {
@@ -3847,7 +3849,8 @@ function renderStorageLocation(bucket, reservedPaths) {
3847
3849
  add_header Cache-Control "private, no-store" always;
3848
3850
  limit_except GET PUT OPTIONS { deny all; }
3849
3851
  client_max_body_size 0;
3850
- proxy_pass http://storage:9000;
3852
+ ${storageBasePath ? ` # map \u4FDD\u7559\u539F\u59CB URI \u4E0E\u67E5\u8BE2\uFF1B\u4E0D\u5728 location \u5185\u521B\u5EFA\u6539\u53D8\u4EE3\u7406\u7EE7\u627F\u7684 if \u914D\u7F6E\u3002
3853
+ proxy_pass http://hai_storage_proxy$hai_storage_uri;` : " proxy_pass http://storage:9000;"}
3851
3854
  proxy_http_version 1.1;
3852
3855
  proxy_set_header Host $http_host;
3853
3856
  proxy_set_header Connection "";
@@ -3859,16 +3862,27 @@ function renderStorageLocation(bucket, reservedPaths) {
3859
3862
  }
3860
3863
  `;
3861
3864
  }
3865
+ function renderStorageUpstream(bucket, storageBasePath = "") {
3866
+ if (!storageBasePath)
3867
+ return "";
3868
+ const path = `${storageBasePath}/${bucket}`.replaceAll(".", "\\\\.");
3869
+ return `upstream hai_storage_proxy { server storage:9000; }
3870
+ map $request_uri $hai_storage_uri {
3871
+ default $request_uri;
3872
+ "~^${path}(/.*)$" /${bucket}$1;
3873
+ }
3874
+ `;
3875
+ }
3862
3876
  function generateApplicationProxyConfig(options) {
3863
3877
  const { primaryService, port, bucket, reservedPaths, maxPlaintextBytes } = options;
3864
3878
  if (!Number.isInteger(port) || port < 1 || port > 65535)
3865
3879
  throw new Error("Invalid application proxy target");
3866
3880
  const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
3867
- return `server {
3881
+ return `${renderStorageUpstream(bucket, options.storageBasePath)}server {
3868
3882
  listen 8080;
3869
3883
  server_name _;
3870
3884
  ${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
3871
- ${renderStorageLocation(bucket, reservedPaths)} location = /_hai/gateway-health {
3885
+ ${renderStorageLocation(bucket, reservedPaths, options.storageBasePath)} location = /_hai/gateway-health {
3872
3886
  access_log off;
3873
3887
  return 200 'ok';
3874
3888
  }
@@ -4056,13 +4070,17 @@ function createDockerSshProvider(config, container) {
4056
4070
  const healthPaths = hasWebProxy ? ["/health", "/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
4057
4071
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
4058
4072
  const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
4073
+ let storageBasePath = "";
4059
4074
  if (managedStorage?.publicEndpoint !== void 0) {
4060
4075
  try {
4061
4076
  if (typeof managedStorage.publicEndpoint !== "string")
4062
4077
  throw new Error("Invalid public endpoint type");
4078
+ const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
4063
4079
  const endpoint = new URL(managedStorage.publicEndpoint);
4064
- if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash)
4080
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !/^\/(?:[\w.-]+(?:\/[\w.-]+)*\/?)?$/.test(endpoint.pathname) || unsafeCharacters || /(?:^|\/)\.{1,2}(?:\/|$)/.test(managedStorage.publicEndpoint)) {
4065
4081
  throw new Error("Invalid public endpoint origin");
4082
+ }
4083
+ storageBasePath = endpoint.pathname.replace(/\/+$/, "");
4066
4084
  } catch {
4067
4085
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storagePublicEndpointInvalid"));
4068
4086
  }
@@ -4146,7 +4164,7 @@ function createDockerSshProvider(config, container) {
4146
4164
  try {
4147
4165
  const configured = transport ? transport.maxPlaintextBytes : void 0;
4148
4166
  const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
4149
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, storageBucket: managedStorage?.bucket, trustedEdgeProxies: config.trustedEdgeProxies });
4167
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config.trustedEdgeProxies });
4150
4168
  } catch (error) {
4151
4169
  return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
4152
4170
  }
@@ -4158,7 +4176,7 @@ function createDockerSshProvider(config, container) {
4158
4176
  const transport = server.data.transport;
4159
4177
  applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
4160
4178
  }
4161
- webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage.bucket, reservedPaths: [webBasePath, http?.apiPrefix ?? "/api/v1"], maxPlaintextBytes: applicationPlaintextBytes, trustedEdgeProxies: config.trustedEdgeProxies });
4179
+ webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage.bucket, storageBasePath, reservedPaths: [http?.apiPrefix ?? "/api/v1", ...storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], maxPlaintextBytes: applicationPlaintextBytes, trustedEdgeProxies: config.trustedEdgeProxies });
4162
4180
  } catch {
4163
4181
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
4164
4182
  }