@h-ai/storage 1.0.0-beta.19 → 1.0.0-beta.20

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
@@ -4,34 +4,11 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
6
6
 
7
- ## Introduction
8
-
9
7
  An object storage module with a unified `storage` entry point for S3-compatible storage and local filesystems.
10
8
 
11
- Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `storage.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
12
-
13
- ```ts
14
- const closed = await storage.close()
15
- if (!closed.success) {
16
- // Retry once after addressing the cleanup failure; report another failure.
17
- const retried = await storage.close()
18
- if (!retried.success)
19
- throw new Error(retried.error.message)
20
- }
21
- ```
22
-
23
- S3 disposes its normal client and its separate batch-delete client (SDK request retries disabled) independently. One failure does not prevent the other cleanup; successfully disposed clients are not destroyed again.
24
-
25
- ## Core features
26
-
27
- - S3 protocol (AWS S3 / MinIO / RustFS / OSS, etc.)
28
- - Local filesystem
29
-
30
- ## Getting started
31
-
32
- ### Quick start
9
+ ## Quick Start
33
10
 
34
- #### Node.js server
11
+ ### Node.js server
35
12
 
36
13
  ```ts
37
14
  import { storage } from '@h-ai/storage'
@@ -83,6 +60,72 @@ await uploadWithPresignedUrl(signed.data.url, file)
83
60
 
84
61
  > Note: `@h-ai/storage` does not include HTTP routes. Implement the signed URL API in the application layer.
85
62
 
63
+ ## Features
64
+
65
+ Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `storage.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
66
+
67
+ ```ts
68
+ const closed = await storage.close()
69
+ if (!closed.success) {
70
+ // Retry once after addressing the cleanup failure; report another failure.
71
+ const retried = await storage.close()
72
+ if (!retried.success)
73
+ throw new Error(retried.error.message)
74
+ }
75
+ ```
76
+
77
+ S3 disposes its normal client and its separate batch-delete client (SDK request retries disabled) independently. One failure does not prevent the other cleanup; successfully disposed clients are not destroyed again.
78
+
79
+ ### Operations and results
80
+
81
+ Use the complete initialization flow in this section first. The host validates and authorizes example business values; check success before reading data from asynchronous domain results. Here void means completion without data, null means absent; synchronous factories/tools return the listed type directly.
82
+
83
+ #### storage.file
84
+
85
+ Object lifecycle uses raw keys, bytes/streams and options: put/copy/head yield metadata, get Buffer, getStream an async byte stream, exists boolean, delete void; deleteMany reports per-key confirmed progress.
86
+
87
+ | Entry | Input parameters | Successful result | Call example |
88
+ | ------------------------- | --------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
89
+ | `storage.file.put` | key, data, options | `FileMetadata` | `await storage.file.put('report.txt', 'text', { contentType: 'text/plain' })` |
90
+ | `storage.file.get` | key, options | `Buffer<ArrayBufferLike>` | `await storage.file.get('report.txt')` |
91
+ | `storage.file.getStream` | key, options | `AsyncIterable<Uint8Array<ArrayBufferLike>>` | `await storage.file.getStream('report.txt')` |
92
+ | `storage.file.head` | key | `FileMetadata` | `await storage.file.head('report.txt')` |
93
+ | `storage.file.exists` | key | `boolean` | `await storage.file.exists('report.txt')` |
94
+ | `storage.file.copy` | sourceKey, destKey, options | `FileMetadata` | `await storage.file.copy('report.txt', 'archive/report.txt')` |
95
+ | `storage.file.delete` | key | `void` | `await storage.file.delete('report.txt')` |
96
+ | `storage.file.deleteMany` | keys | `StorageDeleteResult` | `await storage.file.deleteMany(['report.txt', 'archive/report.txt'])` |
97
+
98
+ #### storage.dir
99
+
100
+ Browse/clean by prefix: list takes prefix/pagination and returns files with continuation; delete takes a prefix and yields void without preventing concurrent new objects.
101
+
102
+ | Entry | Input parameters | Successful result | Call example |
103
+ | -------------------- | ---------------- | ----------------- | ------------------------------------------------------------- |
104
+ | `storage.dir.list` | options | `ListResult` | `await storage.dir.list({ prefix: 'reports/', maxKeys: 20 })` |
105
+ | `storage.dir.delete` | prefix | `void` | `await storage.dir.delete('reports/')` |
106
+
107
+ #### storage.presign
108
+
109
+ Direct S3 upload/download: key plus expiry/response options yield a URL HaiResult; publicUrl synchronously yields a URL/null and grants no authorization.
110
+
111
+ | Entry | Input parameters | Successful result | Call example |
112
+ | --------------------------- | ---------------- | ----------------- | ------------------------------------------------------------------------------------------- |
113
+ | `storage.presign.getUrl` | key, options | `string` | `await storage.presign.getUrl('report.txt', { expiresIn: 300 })` |
114
+ | `storage.presign.putUrl` | key, options | `string` | `await storage.presign.putUrl('report.txt', { expiresIn: 300, contentType: 'text/plain' })` |
115
+ | `storage.presign.publicUrl` | key | `string \| null` | `storage.presign.publicUrl('report.txt')` |
116
+
117
+ #### storage.checksum
118
+
119
+ Stateless integrity: data plus algorithm/encoding yields a digest, expected digest yields matching boolean; a stream is consumed once.
120
+
121
+ | Entry | Input parameters | Successful result | Call example |
122
+ | --------------------------- | ----------------------- | ----------------- | ------------------------------------------------------ |
123
+ | `storage.checksum.generate` | data, options | `string` | `await storage.checksum.generate('text')` |
124
+ | `storage.checksum.verify` | data, expected, options | `boolean` | `await storage.checksum.verify(bytes, expectedDigest)` |
125
+
126
+ - S3 protocol (AWS S3 / MinIO / RustFS / OSS, etc.)
127
+ - Local filesystem
128
+
86
129
  ### HTTP API contracts
87
130
 
88
131
  Public HTTP contracts come from `@h-ai/api-contract`; `createStorageProcedures({ storage, authorize })` in `@h-ai/serv` binds them to this module.
@@ -112,7 +155,7 @@ Presigning requires S3. Local `getUrl()` / `putUrl()` return `HaiStorageError.UN
112
155
 
113
156
  `{ ifNoneMatch: '*' }` uses standard S3 conditional writes or atomic local publication to reject overwrites with `ALREADY_EXISTS`. Unsupported S3 conditions fail without falling back to an overwrite. Failed/cancelled uploads clean temporary files or unfinished parts; cancellation cannot undo an already published object.
114
157
 
115
- Local prepares metadata bound to the content file identity in the reserved `.hai-storage-meta/` directory beside the file, then atomically publishes the content as the commit point. Metadata preparation failure cannot create or overwrite content. Default metadata has an explicit empty record and never inherits old object fields. Failure to clean a known old generation after publication is diagnostic only; concurrent writes or crashes may leave internal records that cannot safely be reclaimed, which are not deleted speculatively. Every path segment reserves `.meta.json`, `.upload.tmp` and `.hai-storage-meta`. Generation records are the only metadata format; legacy `<key>.meta.json` is not read. A missing current record returns an explicit error. Existing data requires offline migration or a new `file.put()` upload. Back up content together with internal records and do not mix older writers with this format.
158
+ Local prepares metadata bound to the content file identity in the reserved `.hai-storage-meta/` directory beside the file, then atomically publishes the content as the commit point. Metadata preparation failure cannot create or overwrite content. Default metadata has an explicit empty record and never inherits old object fields. Failure to clean a known old generation after publication is diagnostic only; concurrent writes or crashes may leave internal records that cannot safely be reclaimed, which are not deleted speculatively. Every path segment reserves `.meta.json`, `.upload.tmp` and `.hai-storage-meta`. Generation records are the only metadata format; a missing valid record returns an explicit error. Rebuild objects through `file.put()` with trusted content and business metadata. Back up content together with internal records and prohibit direct writes that bypass this format.
116
159
 
117
160
  S3 `file.copy()` encodes each source path segment, preserving Unicode, spaces and URL delimiters as part of the object key. Pass raw keys without pre-encoding.
118
161
 
@@ -150,7 +193,21 @@ Cancelling a checksum stops waiting for the next chunk and requests closure of t
150
193
  - `getMimeType(extension)`
151
194
  - `formatFileSize(bytes)`
152
195
 
153
- ### Configuration
196
+ ### Production shared state
197
+
198
+ Production requires shared S3 or a compatible service; local is for development/explicit tests. Applications own object authorization and conditional read-modify-write commits. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md).
199
+
200
+ ### Input limits and HTTP capabilities
201
+
202
+ Object keys are limited to 1024 UTF-8 bytes and reject absolute paths, backslashes, control characters and dot segments. Each public HTTP batch delete accepts 1–1000 entries. `@h-ai/storage/schemas` exposes the shared browser-safe rules. Presign expiry is an integer from 1 to 604800 seconds (Node default 3600, HTTP default 900). Upload signing accepts only key/contentType/expiresIn; contentLength/maxSize/metadata/checksum are unsupported. Invalid input returns HTTP 400 and the typed client maps it to VALIDATION_FAILED. FileUpload limits guide file selection; applications must validate actual objects on the server. Node streaming/checksum/conditional-write options have no default HTTP endpoints. See the [capability and authorization matrix (Chinese)](README.md#存储输入约束与跨端能力).
203
+
204
+ ### Browser upload cancellation
205
+
206
+ `uploadWithPresignedUrl(url, data, { onProgress, abortController })` uses native XHR for progress and fetch when `onProgress` is omitted. An already aborted controller sends no request. Cancellation during setup or upload, external XHR abort, network/timeout errors, and progress callback exceptions settle once with a failure and remove this operation's listeners. Aborting after completion does not touch the old XHR. Cancellation only stops the local request; it cannot roll back an object already written remotely. Check the object state and authorize deletion separately.
207
+
208
+ ## Configuration
209
+
210
+ Lifecycle: init takes backend/business configuration; use the module only after success. Await close before releasing borrowed dependencies. Follow the quick-start example and public signatures for exact return values.
154
211
 
155
212
  Local storage rejects file symlinks and directory junctions in key paths; reserved `.meta.json` / `.upload.tmp` suffixes are case insensitive. Overwrite put replaces old metadata, while copy inherits source metadata by default. The browser root exports schemas, types and client helpers, without the server singleton.
156
213
 
@@ -183,7 +240,7 @@ if (!initResult.success) {
183
240
  }
184
241
  ```
185
242
 
186
- #### Internal connections and browser access
243
+ ### Internal connections and browser access
187
244
 
188
245
  `endpoint` handles server-side CRUD, bucket creation and health checks. Optional `publicEndpoint` selects the external address for `presign.getUrl/putUrl`; omission preserves endpoint-based signing. It accepts a proxy directory such as `https://app.example.com/delivery/files`. Path segments allow letters, numbers, underscores, dots and hyphens, excluding dot segments, repeated slashes and encoded separators. Do not include the bucket, credentials, query or fragment. `publicUrl` constructs public-resource URLs without authorizing private objects.
189
246
 
@@ -238,7 +295,13 @@ do {
238
295
  } while (continuationToken)
239
296
  ```
240
297
 
241
- ### Error handling
298
+ The host loads and validates configuration before passing it to this module. Keep secrets in private server environment files. Select the backend, capacity, deadlines and logging for the actual use case; reinitialize and check the result after changes.
299
+
300
+ [Per-option purpose, use cases and examples](../../docs/usage/configuration/data.md).
301
+
302
+ ## Error Handling
303
+
304
+ Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
242
305
 
243
306
  ```ts
244
307
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -263,19 +326,7 @@ Common error codes:
263
326
  | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | Invalid local list options or cursor, including mismatched query scope |
264
327
  | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | Invalid configuration |
265
328
 
266
- ## Production shared state
267
-
268
- Production requires shared S3 or a compatible service; local is for development/explicit tests. Applications own object authorization and conditional read-modify-write commits. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
269
-
270
- ## Input limits and HTTP capabilities
271
-
272
- Object keys are limited to 1024 UTF-8 bytes and reject absolute paths, backslashes, control characters and dot segments. Each public HTTP batch delete accepts 1–1000 entries. `@h-ai/storage/schemas` exposes the shared browser-safe rules. Presign expiry is an integer from 1 to 604800 seconds (Node default 3600, HTTP default 900). Upload signing accepts only key/contentType/expiresIn; contentLength/maxSize/metadata/checksum are unsupported. Invalid input returns HTTP 400 and the typed client maps it to VALIDATION_FAILED. FileUpload limits guide file selection; applications must validate actual objects on the server. Node streaming/checksum/conditional-write options have no default HTTP endpoints. See the [capability and authorization matrix (Chinese)](README.md#存储输入约束与跨端能力).
273
-
274
- ## Browser upload cancellation
275
-
276
- `uploadWithPresignedUrl(url, data, { onProgress, abortController })` uses native XHR for progress and fetch when `onProgress` is omitted. An already aborted controller sends no request. Cancellation during setup or upload, external XHR abort, network/timeout errors, and progress callback exceptions settle once with a failure and remove this operation's listeners. Aborting after completion does not touch the old XHR. Cancellation only stops the local request; it cannot roll back an object already written remotely. Check the object state and authorize deletion separately.
277
-
278
- ## Development and testing
329
+ ## Development
279
330
 
280
331
  ```bash
281
332
  pnpm --filter @h-ai/storage test
package/README.ja.md CHANGED
@@ -4,34 +4,11 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
6
 
7
- ## 概要
8
-
9
7
  共通の `storage` 入口で S3 互換ストレージとローカルファイルシステムを操作するオブジェクトストレージモジュールです。
10
8
 
11
- 接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`storage.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
12
-
13
- ```ts
14
- const closed = await storage.close()
15
- if (!closed.success) {
16
- // 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
17
- const retried = await storage.close()
18
- if (!retried.success)
19
- throw new Error(retried.error.message)
20
- }
21
- ```
22
-
23
- S3 の通常クライアントと、リクエスト再試行を無効化した一括削除クライアントは個別に解放します。一方の失敗でも他方は解放し、解放済みクライアントは再度破棄しません。
24
-
25
- ## 主な機能
26
-
27
- - S3 プロトコル(AWS S3 / MinIO / RustFS / OSS など)
28
- - ローカルファイルシステム
29
-
30
- ## 使い始める
31
-
32
- ### クイックスタート
9
+ ## クイックスタート
33
10
 
34
- #### Node.js サーバー
11
+ ### Node.js サーバー
35
12
 
36
13
  ```ts
37
14
  import { storage } from '@h-ai/storage'
@@ -83,6 +60,72 @@ await uploadWithPresignedUrl(signed.data.url, file)
83
60
 
84
61
  > 注:`@h-ai/storage` に HTTP ルートは組み込まれていません。署名付き URL API はアプリケーション層で実装してください。
85
62
 
63
+ ## 機能
64
+
65
+ 接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`storage.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
66
+
67
+ ```ts
68
+ const closed = await storage.close()
69
+ if (!closed.success) {
70
+ // 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
71
+ const retried = await storage.close()
72
+ if (!retried.success)
73
+ throw new Error(retried.error.message)
74
+ }
75
+ ```
76
+
77
+ S3 の通常クライアントと、リクエスト再試行を無効化した一括削除クライアントは個別に解放します。一方の失敗でも他方は解放し、解放済みクライアントは再度破棄しません。
78
+
79
+ ### 操作と入出力
80
+
81
+ 本節の完全な初期化手順を先に実行します。例の業務値はホストが検証・認可し、非同期領域結果は success を確認してから data を読みます。void はデータなし完了、null は不存在、同期ファクトリー/ツールは表の型を直接返します。
82
+
83
+ #### storage.file
84
+
85
+ 元のキー・バイト/ストリーム・オプションで操作。put/copy/head はメタデータ、get は Buffer、getStream は非同期バイト列、exists は boolean、delete は void、deleteMany は項目別進捗。
86
+
87
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
88
+ | ------------------------- | --------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
89
+ | `storage.file.put` | key, data, options | `FileMetadata` | `await storage.file.put('report.txt', 'text', { contentType: 'text/plain' })` |
90
+ | `storage.file.get` | key, options | `Buffer<ArrayBufferLike>` | `await storage.file.get('report.txt')` |
91
+ | `storage.file.getStream` | key, options | `AsyncIterable<Uint8Array<ArrayBufferLike>>` | `await storage.file.getStream('report.txt')` |
92
+ | `storage.file.head` | key | `FileMetadata` | `await storage.file.head('report.txt')` |
93
+ | `storage.file.exists` | key | `boolean` | `await storage.file.exists('report.txt')` |
94
+ | `storage.file.copy` | sourceKey, destKey, options | `FileMetadata` | `await storage.file.copy('report.txt', 'archive/report.txt')` |
95
+ | `storage.file.delete` | key | `void` | `await storage.file.delete('report.txt')` |
96
+ | `storage.file.deleteMany` | keys | `StorageDeleteResult` | `await storage.file.deleteMany(['report.txt', 'archive/report.txt'])` |
97
+
98
+ #### storage.dir
99
+
100
+ 接頭辞で一覧/削除。list は prefix とページ指定からファイル/継続情報、delete は prefix から void。同時追加は防がない。
101
+
102
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
103
+ | -------------------- | -------- | ------------ | ------------------------------------------------------------- |
104
+ | `storage.dir.list` | options | `ListResult` | `await storage.dir.list({ prefix: 'reports/', maxKeys: 20 })` |
105
+ | `storage.dir.delete` | prefix | `void` | `await storage.dir.delete('reports/')` |
106
+
107
+ #### storage.presign
108
+
109
+ S3 直接転送。キーと期限/応答指定から URL HaiResult、publicUrl は同期 URL/null を返しアクセス権は付与しない。
110
+
111
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
112
+ | --------------------------- | ------------ | ---------------- | ------------------------------------------------------------------------------------------- |
113
+ | `storage.presign.getUrl` | key, options | `string` | `await storage.presign.getUrl('report.txt', { expiresIn: 300 })` |
114
+ | `storage.presign.putUrl` | key, options | `string` | `await storage.presign.putUrl('report.txt', { expiresIn: 300, contentType: 'text/plain' })` |
115
+ | `storage.presign.publicUrl` | key | `string \| null` | `storage.presign.publicUrl('report.txt')` |
116
+
117
+ #### storage.checksum
118
+
119
+ 初期化不要の完全性確認。データとアルゴリズム/符号化から摘要、expected との比較は boolean。ストリームは一度消費。
120
+
121
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
122
+ | --------------------------- | ----------------------- | --------- | ------------------------------------------------------ |
123
+ | `storage.checksum.generate` | data, options | `string` | `await storage.checksum.generate('text')` |
124
+ | `storage.checksum.verify` | data, expected, options | `boolean` | `await storage.checksum.verify(bytes, expectedDigest)` |
125
+
126
+ - S3 プロトコル(AWS S3 / MinIO / RustFS / OSS など)
127
+ - ローカルファイルシステム
128
+
86
129
  ### HTTP API 契約
87
130
 
88
131
  公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/serv` の `createStorageProcedures({ storage, authorize })` で本モジュールの機能に接続します。
@@ -112,7 +155,7 @@ if (!result.success)
112
155
 
113
156
  `{ ifNoneMatch: '*' }` は標準 S3 条件付き書き込み、またはローカルの原子的公開で上書きを拒否し、競合時に `ALREADY_EXISTS` を返します。非対応の S3 サービスではエラーとし、上書きに切り替えません。失敗・中止時は一時ファイルや未完了パートを削除しますが、公開済みオブジェクトは中止で取り消せません。
114
157
 
115
- Local はファイル横の予約ディレクトリ `.hai-storage-meta/` に内容の識別情報と結び付いた世代メタデータを準備し、内容ファイルの原子的公開をコミット点にします。準備失敗時は内容を作成・上書きしません。既定値にも空の記録を保存し、古いオブジェクトの属性を継承しません。公開後の既知の旧世代の削除失敗は診断だけに記録します。並行書き込みやクラッシュで安全な回収を確認できない内部記録が残る場合があり、推測で削除しません。各パス要素で `.meta.json`、`.upload.tmp`、`.hai-storage-meta` を予約します。世代記録が唯一のメタデータ形式で、旧 `<key>.meta.json` は読みません。現世代の記録がなければ明示的に失敗します。既存データはオフラインで移行するか `file.put()` で再アップロードしてください。内容と内部記録を一緒にバックアップし、旧プログラムからの書き込みを混在させないでください。
158
+ Local はファイル横の予約ディレクトリ `.hai-storage-meta/` に内容の識別情報と結び付いた世代メタデータを準備し、内容ファイルの原子的公開をコミット点にします。準備失敗時は内容を作成・上書きしません。既定値にも空の記録を保存し、古いオブジェクトの属性を継承しません。公開後の既知の旧世代の削除失敗は診断だけに記録します。並行書き込みやクラッシュで安全な回収を確認できない内部記録が残る場合があり、推測で削除しません。各パス要素で `.meta.json`、`.upload.tmp`、`.hai-storage-meta` を予約します。世代記録が唯一のメタデータ形式で、有効な記録がなければ明示的に失敗します。再構築時は信頼する内容と業務メタデータを `file.put()` で保存します。内容と内部記録を一緒にバックアップし、この形式を迂回した直接書込を混在させないでください。
116
159
 
117
160
  S3 `file.copy()` はコピー元の各パス要素をエンコードし、日本語・空白・URL 区切り文字をオブジェクトキーの一部として保持します。呼び出し側では事前エンコードせず元の key を渡します。
118
161
 
@@ -150,7 +193,21 @@ checksum の中止時は次のチャンクを待つ処理を終了し、入力
150
193
  - `getMimeType(extension)`
151
194
  - `formatFileSize(bytes)`
152
195
 
153
- ### 設定
196
+ ### 本番の共有状態
197
+
198
+ 本番は共有 S3 または互換サービスを使います。local は開発・明示的テスト用です。オブジェクト認可と読取後更新の条件付きコミットはアプリが担当します。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md).
199
+
200
+ ### 入力制約と HTTP 機能
201
+
202
+ キーは UTF-8 で最大 1024 バイトで、絶対パス、バックスラッシュ、制御文字、ドットセグメントを拒否します。公開 HTTP 一括削除は一度に 1–1000 件です。共通規則はブラウザー対応の `@h-ai/storage/schemas` から参照できます。署名有効期限は整数 1–604800 秒(Node 既定 3600、HTTP 既定 900)。アップロード署名の入力は key/contentType/expiresIn のみで、contentLength/maxSize/metadata/checksum は未対応です。不正入力は HTTP 400、typed client では VALIDATION_FAILED です。FileUpload の制限は選択時の補助であり、実際のオブジェクトはサーバーで検証してください。Node のストリーム/checksum/条件付き書き込みには既定の HTTP エンドポイントがありません。[機能と認可の対応表(中国語)](README.md#存储输入约束与跨端能力)を参照してください。
203
+
204
+ ### ブラウザーアップロードのキャンセル
205
+
206
+ `uploadWithPresignedUrl(url, data, { onProgress, abortController })` は進捗通知にネイティブXHRを使用し、`onProgress` を省略するとfetchを使用します。事前にキャンセルされた場合は送信しません。準備中・送信中のキャンセル、外部からのXHR abort、通信・タイムアウト、進捗コールバックの例外は一度だけ失敗として確定し、この操作のリスナーを解除します。完了後のキャンセルは古いXHRを操作しません。キャンセルはローカル要求の停止であり、リモートに保存済みのオブジェクトを取り消せません。状態を確認してから、別途認可して削除してください。
207
+
208
+ ## 設定
209
+
210
+ ライフサイクル。init にバックエンド/業務設定を渡し成功後に使用します。借用依存先の解放前に close の完了を待ち、正確な戻り値は例と公開署名に従います。
154
211
 
155
212
  Local は key 内のシンボリックリンクと junction を拒否し、予約拡張子 `.meta.json` / `.upload.tmp` は大文字小文字を区別せず保護します。put 上書きは旧メタデータを置換し、copy は既定で元を継承します。ブラウザー入口にサーバー単一インスタンスは含まれません。
156
213
 
@@ -183,7 +240,7 @@ if (!initResult.success) {
183
240
  }
184
241
  ```
185
242
 
186
- #### 内部接続とブラウザーからの直接アクセス
243
+ ### 内部接続とブラウザーからの直接アクセス
187
244
 
188
245
  `endpoint` はサーバーの CRUD、バケット作成、接続確認に使います。`publicEndpoint` は `presign.getUrl/putUrl` の外部アドレスで既定値はなく、省略時は endpoint を使います。`https://app.example.com/delivery/files` のようなプロキシパスを指定できます。パスには英数字、アンダースコア、ドット、ハイフンを使用でき、`.`/`..`、連続スラッシュ、符号化された区切りは使用できません。バケット、認証情報、クエリ、フラグメントは含めません。`publicUrl` は公開 URL の組み立て用で、私有オブジェクトを認可しません。
189
246
 
@@ -238,7 +295,13 @@ do {
238
295
  } while (continuationToken)
239
296
  ```
240
297
 
241
- ### エラー処理
298
+ 設定はホストが読込・検証してから渡します。秘密はサーバーの非公開環境に保存し、利用場面に応じてバックエンド・容量・期限・ログを選びます。変更後は再初期化の結果を確認してください。
299
+
300
+ [項目別の用途・適用場面・例](../../docs/usage/configuration/data.md).
301
+
302
+ ## エラー処理
303
+
304
+ 領域操作は result.success を確認し、失敗時はモジュールのエラー定義と安全な説明を使います。コード・関連 ID・秘匿した原因をサーバーログに残します。同期ツール・ファクトリー・ストリーム反復・ライフサイクルは各署名に従い、Promise 完了だけで成功と判断しません。回復条件は各機能を参照してください。
242
305
 
243
306
  ```ts
244
307
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -263,18 +326,6 @@ if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.cod
263
326
  | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | ローカル一覧の引数・カーソルが不正、または検索範囲が不一致 |
264
327
  | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 設定エラー |
265
328
 
266
- ## 本番の共有状態
267
-
268
- 本番は共有 S3 または互換サービスを使います。local は開発・明示的テスト用です。オブジェクト認可と読取後更新の条件付きコミットはアプリが担当します。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
269
-
270
- ## 入力制約と HTTP 機能
271
-
272
- キーは UTF-8 で最大 1024 バイトで、絶対パス、バックスラッシュ、制御文字、ドットセグメントを拒否します。公開 HTTP 一括削除は一度に 1–1000 件です。共通規則はブラウザー対応の `@h-ai/storage/schemas` から参照できます。署名有効期限は整数 1–604800 秒(Node 既定 3600、HTTP 既定 900)。アップロード署名の入力は key/contentType/expiresIn のみで、contentLength/maxSize/metadata/checksum は未対応です。不正入力は HTTP 400、typed client では VALIDATION_FAILED です。FileUpload の制限は選択時の補助であり、実際のオブジェクトはサーバーで検証してください。Node のストリーム/checksum/条件付き書き込みには既定の HTTP エンドポイントがありません。[機能と認可の対応表(中国語)](README.md#存储输入约束与跨端能力)を参照してください。
273
-
274
- ## ブラウザーアップロードのキャンセル
275
-
276
- `uploadWithPresignedUrl(url, data, { onProgress, abortController })` は進捗通知にネイティブXHRを使用し、`onProgress` を省略するとfetchを使用します。事前にキャンセルされた場合は送信しません。準備中・送信中のキャンセル、外部からのXHR abort、通信・タイムアウト、進捗コールバックの例外は一度だけ失敗として確定し、この操作のリスナーを解除します。完了後のキャンセルは古いXHRを操作しません。キャンセルはローカル要求の停止であり、リモートに保存済みのオブジェクトを取り消せません。状態を確認してから、別途認可して削除してください。
277
-
278
329
  ## 開発とテスト
279
330
 
280
331
  ```bash
package/README.md CHANGED
@@ -4,34 +4,11 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/core](../core/README.md)
6
6
 
7
- ## 简介
8
-
9
7
  对象存储模块,提供统一 `storage` 访问入口,支持 S3 兼容存储与本地文件系统。
10
8
 
11
- 初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`storage.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
12
-
13
- ```ts
14
- const closed = await storage.close()
15
- if (!closed.success) {
16
- // 处理清理故障后有限重试;再次失败应向应用报告。
17
- const retried = await storage.close()
18
- if (!retried.success)
19
- throw new Error(retried.error.message)
20
- }
21
- ```
22
-
23
- S3 普通请求客户端与禁用请求重试的批量删除客户端分别释放;一个失败不阻止另一个清理,已释放的客户端不会重复释放。
24
-
25
- ## 核心功能清单
26
-
27
- - S3 协议(AWS S3 / MinIO / RustFS / OSS 等)
28
- - 本地文件系统
29
-
30
- ## 使用入门
9
+ ## 快速开始
31
10
 
32
- ### 快速开始
33
-
34
- #### Node.js 服务端
11
+ ### Node.js 服务端
35
12
 
36
13
  ```ts
37
14
  import { storage } from '@h-ai/storage'
@@ -83,6 +60,72 @@ await uploadWithPresignedUrl(signed.data.url, file)
83
60
 
84
61
  > 注意:`@h-ai/storage` 不内置 HTTP 路由;请在应用层自行实现签名 URL API。
85
62
 
63
+ ## 功能
64
+
65
+ 初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`storage.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
66
+
67
+ ```ts
68
+ const closed = await storage.close()
69
+ if (!closed.success) {
70
+ // 处理清理故障后有限重试;再次失败应向应用报告。
71
+ const retried = await storage.close()
72
+ if (!retried.success)
73
+ throw new Error(retried.error.message)
74
+ }
75
+ ```
76
+
77
+ S3 普通请求客户端与禁用请求重试的批量删除客户端分别释放;一个失败不阻止另一个清理,已释放的客户端不会重复释放。
78
+
79
+ ### 功能输入输出
80
+
81
+ 以下片段依赖本节完整初始化流程。示例中的业务值由宿主校验和授权后传入;异步领域结果先判断 success,再读取 data。表中 void 表示操作已完成且没有返回数据,null 表示不存在;同步工厂/工具直接返回表中类型。
82
+
83
+ #### storage.file
84
+
85
+ 对象生命周期输入原始 key、字节/流和选项;put/copy/head 返回元数据,get 返回 Buffer,getStream 返回异步字节流,exists 返回 boolean,delete 返回 void;deleteMany 返回逐项确认进度。
86
+
87
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
88
+ | ------------------------- | --------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
89
+ | `storage.file.put` | key, data, options | `FileMetadata` | `await storage.file.put('report.txt', 'text', { contentType: 'text/plain' })` |
90
+ | `storage.file.get` | key, options | `Buffer<ArrayBufferLike>` | `await storage.file.get('report.txt')` |
91
+ | `storage.file.getStream` | key, options | `AsyncIterable<Uint8Array<ArrayBufferLike>>` | `await storage.file.getStream('report.txt')` |
92
+ | `storage.file.head` | key | `FileMetadata` | `await storage.file.head('report.txt')` |
93
+ | `storage.file.exists` | key | `boolean` | `await storage.file.exists('report.txt')` |
94
+ | `storage.file.copy` | sourceKey, destKey, options | `FileMetadata` | `await storage.file.copy('report.txt', 'archive/report.txt')` |
95
+ | `storage.file.delete` | key | `void` | `await storage.file.delete('report.txt')` |
96
+ | `storage.file.deleteMany` | keys | `StorageDeleteResult` | `await storage.file.deleteMany(['report.txt', 'archive/report.txt'])` |
97
+
98
+ #### storage.dir
99
+
100
+ 按前缀浏览/清理;list 接收 prefix、分页选项返回文件列表/续页信息,delete 接收 prefix 返回 void,不能阻止并发新增。
101
+
102
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
103
+ | -------------------- | -------- | ------------ | ------------------------------------------------------------- |
104
+ | `storage.dir.list` | options | `ListResult` | `await storage.dir.list({ prefix: 'reports/', maxKeys: 20 })` |
105
+ | `storage.dir.delete` | prefix | `void` | `await storage.dir.delete('reports/')` |
106
+
107
+ #### storage.presign
108
+
109
+ S3 客户端直传/下载。key 和期限/响应选项生成 URL HaiResult;publicUrl 同步返回公开 URL 或 null,不代表获得访问授权。
110
+
111
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
112
+ | --------------------------- | ------------ | ---------------- | ------------------------------------------------------------------------------------------- |
113
+ | `storage.presign.getUrl` | key, options | `string` | `await storage.presign.getUrl('report.txt', { expiresIn: 300 })` |
114
+ | `storage.presign.putUrl` | key, options | `string` | `await storage.presign.putUrl('report.txt', { expiresIn: 300, contentType: 'text/plain' })` |
115
+ | `storage.presign.publicUrl` | key | `string \| null` | `storage.presign.publicUrl('report.txt')` |
116
+
117
+ #### storage.checksum
118
+
119
+ 无需初始化的字节完整性校验;数据及算法/编码得到摘要,expected 得到匹配 boolean;流输入消费一次。
120
+
121
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
122
+ | --------------------------- | ----------------------- | --------- | ------------------------------------------------------ |
123
+ | `storage.checksum.generate` | data, options | `string` | `await storage.checksum.generate('text')` |
124
+ | `storage.checksum.verify` | data, expected, options | `boolean` | `await storage.checksum.verify(bytes, expectedDigest)` |
125
+
126
+ - S3 协议(AWS S3 / MinIO / RustFS / OSS 等)
127
+ - 本地文件系统
128
+
86
129
  ### HTTP API 契约
87
130
 
88
131
  公共 HTTP API 统一由 `@h-ai/api-contract` 提供,并由 `@h-ai/serv` 的 `createStorageProcedures({ storage, authorize })` 绑定到本模块能力。
@@ -112,7 +155,7 @@ if (!result.success)
112
155
 
113
156
  上传选项 `{ ifNoneMatch: '*' }` 通过标准 S3 条件写或本地原子发布拒绝覆盖,冲突返回 `ALREADY_EXISTS`;S3 服务不支持条件写时返回错误,不降级为覆盖。失败/取消会清理临时文件或未完成分片;取消不能撤销已经发布的对象。
114
157
 
115
- Local 在内容文件旁的保留目录 `.hai-storage-meta/` 中准备与文件身份绑定的代际元数据,再以内容文件的原子发布作为提交点。元数据准备失败不会新增或覆盖内容;默认元数据也有空记录,不继承旧对象的类型或业务字段。发布后清理明确旧代际失败只记录诊断,不将已提交写入报告为失败;并发或崩溃可能留下尚未确认可回收的内部记录,不做推测性删除。`.meta.json`、`.upload.tmp` 后缀和 `.hai-storage-meta` 在每个路径段均保留,不能直接读写或删除。代际记录是唯一元数据格式,不读取旧 `<key>.meta.json`;缺少当前记录时明确返回错误,旧数据需要离线迁移或通过 `file.put()` 重新上传。备份/迁移必须同时保存内容与内部记录,不混用旧程序写入。
158
+ Local 在内容文件旁的保留目录 `.hai-storage-meta/` 中准备与文件身份绑定的代际元数据,再以内容文件的原子发布作为提交点。元数据准备失败不会新增或覆盖内容;默认元数据也有空记录,不继承旧对象的类型或业务字段。发布后清理明确旧代际失败只记录诊断,不将已提交写入报告为失败;并发或崩溃可能留下尚未确认可回收的内部记录,不做推测性删除。`.meta.json`、`.upload.tmp` 后缀和 `.hai-storage-meta` 在每个路径段均保留,不能直接读写或删除。代际记录是唯一元数据格式,缺少有效记录时明确返回错误;重建对象使用可信内容及业务元数据,通过 `file.put()` 写入。备份同时保存内容与内部记录,禁止绕过此格式直接混写。
116
159
 
117
160
  S3 `file.copy()` 对源键按路径段编码,中文、空格及 URL 分隔字符仍作为原始对象键处理;调用方传原始 key,不要提前编码。
118
161
 
@@ -150,7 +193,63 @@ if (!result.success) {
150
193
  - `getMimeType(extension)`
151
194
  - `formatFileSize(bytes)`
152
195
 
153
- ### 配置
196
+ ### 生产共享状态
197
+
198
+ 生产使用共享 S3 或兼容服务;local 只用于开发/明确测试。对象业务授权和读改写的条件提交由应用负责。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md).
199
+
200
+ ### 输入约束与 HTTP 能力
201
+
202
+ 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](README.md#存储输入约束与跨端能力).
203
+
204
+ ### 浏览器上传取消
205
+
206
+ `uploadWithPresignedUrl(url, data, { onProgress, abortController })` 使用原生 XHR 报告进度;不传 `onProgress` 时使用 fetch。预先取消不发送请求,准备阶段和进行中取消、外部 XHR abort、网络/超时及进度回调异常都只结算一次失败,并移除本次监听。完成后再取消不会操作旧 XHR。取消只终止本地请求,不能回滚远端已写入的对象;应用须先核对对象状态,再授权删除。
207
+
208
+ ### 存储输入约束与跨端能力
209
+
210
+ `@h-ai/storage/schemas` 是仅依赖 Zod 的浏览器安全入口;`api-contract` 与 `serv` 复用同一规则。Schema 只校验输入,服务端仍先认证、再按可信 session 授权每个资源,Local Provider 仍执行文件系统防逃逸。调用方不能把前缀校验或分页游标当成授权。
211
+
212
+ #### 输入边界
213
+
214
+ - `StorageKeySchema`:非空、不是纯空白,UTF-8 最多 1024 **字节**;不允许开头 `/`、反斜线、控制字符,以及独立的 `.` / `..` 路径段。不截断、不归一化 key。
215
+ - `StoragePrefixSchema`:同样的路径和字节限制,另允许空字符串;允许部分文件名,`report` 不等于目录 `report/`。
216
+ - `StorageDeleteKeysSchema`:每个 HTTP 请求 1–1000 个键,按原始数组数量计数,重复项不能绕过上限。Node `file.deleteMany` 允许更大数组,S3 每批最多 1000、Local 并发最多 32;失败时检查 `error.ext.deleteMany` 的已完成/失败/未知/未尝试进度,不能盲目重放整个数组。
217
+ - 列表 `maxKeys`:整数 1–1000,`files` 与 `commonPrefixes` 共用预算。续页使用响应原样返回的 token,并保持相同 Provider/根目录/prefix/delimiter;不保证并发写入期间的快照。
218
+ - 签名有效期:整数 1–604800 秒,Node 默认 3600 秒,默认 HTTP procedures 使用 900 秒。HTTP 上传签名只接受 `key`、`contentType`、`expiresIn`;下载签名只接受 `key`、`expiresIn`。未知字段直接拒绝,包括未实现的 `contentLength` / `maxSize`,不静默丢弃或假装限制已生效。
219
+ - 输入校验在 HTTP 层返回 400;`apiClient` 返回 `HaiApiClientError.VALIDATION_FAILED`,`error.cause.status` 保留 400。合法输入的业务失败仍按 `HaiResult.success` 判断。
220
+
221
+ ```ts
222
+ import { StorageDeleteFilesInputSchema } from '@h-ai/api-contract/storage'
223
+ import { StorageKeySchema } from '@h-ai/storage/schemas'
224
+
225
+ const key = StorageKeySchema.safeParse('users/u1/report.pdf')
226
+ if (key.success) {
227
+ const input = StorageDeleteFilesInputSchema.parse({
228
+ keys: [key.data]
229
+ })
230
+ // 把 input 交给已初始化的 client.storage.files.deleteMany;仍须检查返回值 success。
231
+ }
232
+ ```
233
+
234
+ #### 能力映射
235
+
236
+ | Node storage 能力 | api-contract / serv 默认端点 | apiClient / 浏览器消费 | 边界 |
237
+ | -------------------------------------------------- | --------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
238
+ | `presign.getUrl` | `POST /storage/presigned-urls/download` | `storage.presignedUrls.createDownload` | 仅 S3;Local 返回 `hai:storage:018`,不返回虚拟 URL |
239
+ | `presign.putUrl` | `POST /storage/presigned-urls/upload` | `storage.presignedUrls.createUpload` 后直接 PUT 到 S3 | 仅内容类型与有效期;不是文件代理上传,也不提供上传大小/内容校验保证 |
240
+ | `dir.list` | `GET /storage/files` | `storage.files.list` | 共享前缀约束与分页预算;逐次授权 prefix |
241
+ | `file.head` | `POST /storage/files/metadata` | `storage.files.getMetadata` | 返回元数据,不能把 ETag 当内容 checksum |
242
+ | `file.delete` | `DELETE /storage/files` | `storage.files.delete` | 逐资源授权 |
243
+ | `file.deleteMany` | `POST /storage/files/delete-many` | `storage.files.deleteMany` | HTTP 总量上限 1000,返回可确认的删除进度 |
244
+ | `file.put/get/getStream/copy/exists`、`dir.delete` | 无默认端点 | 应用自行提供有界且鉴权的服务端流程 | 流、Range、AbortSignal、metadata、cacheControl、条件写等 Node 选项不能直接作为预签名 API 入参 |
245
+ | `checksum.generate/verify` | 无默认端点 | 应用选择明确的浏览器实现或服务端校验流程 | Node 无状态工具;不是上传接口自动校验 |
246
+ | `presign.publicUrl` | 无默认端点 | 应用按公开资源策略使用 | 仅配置公开 S3 地址时有 URL;不是鉴权链接,Local 为 null |
247
+
248
+ `FileUpload` / `ImageUpload` / `AvatarUpload` 的 `uploadHandler` 由应用实现。组件的 `maxSize`、`accept` 只负责前端选择体验,不能约束拿到签名后绕过组件的上传;服务端应在对象落库/回调或独立代理上传处验证实际大小、类型、内容和归属,再发布业务记录。不要把 Node `UploadOptions` 当成签名选项,也不要给组件编造不存在的框架上传端点。
249
+
250
+ ## 配置
251
+
252
+ 生命周期:init 输入后端/业务配置,成功后才能使用;close 在释放借用依赖前等待完成。返回值以快速开始示例和公开签名为准。
154
253
 
155
254
  Local 后端拒绝 key 路径中的文件符号链接和目录 junction,保留后缀 `.meta.json` / `.upload.tmp` 的保护不区分大小写。覆盖 put 会替换旧元数据,copy 默认继承源元数据;浏览器根入口仅提供 Schema、类型和客户端工具,不包含服务端 storage 单例。
156
255
 
@@ -183,7 +282,7 @@ if (!initResult.success) {
183
282
  }
184
283
  ```
185
284
 
186
- #### 内部连接与浏览器直连
285
+ ### 内部连接与浏览器直连
187
286
 
188
287
  `endpoint` 用于服务端读写、建桶和连接检查;`publicEndpoint` 用于 `presign.getUrl/putUrl` 的外部地址,无默认值,省略时继续使用 endpoint。可填写 `https://app.example.com/delivery/files`;目录段支持字母、数字、下划线、点和连字符,拒绝 `.`/`..`、重复斜杠及编码的分隔符,不能包含桶名、凭据、查询或片段。`publicUrl` 只拼接公开资源地址,不会为私有对象授权。
189
288
 
@@ -223,7 +322,7 @@ await storage.init({
223
322
 
224
323
  `storage.presign.putUrl()` 当前仅支持 `contentType` 与 `expiresIn` 等签名参数,不提供 `maxSize` 约束。
225
324
 
226
- 本地 `storage.dir.list()` 将 `prefix` 作为 key 字符串前缀(如 `reports/2026-`),按 UTF-8 字节序分页。`maxKeys` 为 1–1000 的整数,默认 1000,`files` 与 `commonPrefixes` 共用该预算。`delimiter` 将前缀之后首次出现的分隔符及其后内容折叠为一个 `commonPrefixes` 条目;空字符串不折叠,空物理目录不生成条目。这些分组与计数规则参照 [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)。
325
+ 本地 `storage.dir.list()` 将 `prefix` 作为 key 字符串前缀(如 `reports/2026-`),按 UTF-8 字节序分页。`maxKeys` 为 1–1000 的整数,默认 1000,`files` 与 `commonPrefixes` 共用该预算。`delimiter` 将前缀之后首次出现的分隔符及其后内容折叠为一个 `commonPrefixes` 条目;空字符串不折叠,空物理目录不生成条目。这些分组与计数规则参照 [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).
227
326
 
228
327
  仅当还有条目时返回 `isTruncated: true` 与 `nextContinuationToken`。本地游标无需进程内缓存,绑定规范根目录、`prefix` 和 `delimiter`;相同根目录的独立实例或重启后可续页,也可调整下一页的 `maxKeys`。跨范围、格式非法或版本不支持的游标返回 `INVALID_LIST_OPTIONS`。游标不是授权凭据或目录快照:调用方仍须授权前缀,并发增删可能改变后续页;不支持将不同挂载路径的根目录视为同一范围。本地每次请求仍扫描、排序匹配目录,大目录应缩小前缀或使用 S3。
229
328
 
@@ -238,7 +337,13 @@ do {
238
337
  } while (continuationToken)
239
338
  ```
240
339
 
241
- ### 错误处理
340
+ 配置由宿主加载和校验,再传入本模块;私密值放服务端私有环境,不传到浏览器。按实际使用场景逐项选择后端、容量、期限和日志策略,修改后重新初始化并核验成功结果。
341
+
342
+ [逐项配置说明、适用场景和示例](../../docs/usage/configuration/data.md).
343
+
344
+ ## 错误处理
345
+
346
+ 领域操作先判断 `result.success`,失败按本模块错误定义与安全提示处理,服务端保留错误码、关联编号和脱敏原因。同步工具、工厂、流迭代及生命周期按各自签名处理,不把 Promise 完成视为业务成功。具体失败与恢复边界见对应功能。
242
347
 
243
348
  ```ts
244
349
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -263,60 +368,6 @@ if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.cod
263
368
  | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | 本地列表参数或游标无效、游标查询范围不匹配 |
264
369
  | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 配置错误 |
265
370
 
266
- ## 生产共享状态
267
-
268
- 生产使用共享 S3 或兼容服务;local 只用于开发/明确测试。对象业务授权和读改写的条件提交由应用负责。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
269
-
270
- ## 输入约束与 HTTP 能力
271
-
272
- 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](README.md#存储输入约束与跨端能力)。
273
-
274
- ## 浏览器上传取消
275
-
276
- `uploadWithPresignedUrl(url, data, { onProgress, abortController })` 使用原生 XHR 报告进度;不传 `onProgress` 时使用 fetch。预先取消不发送请求,准备阶段和进行中取消、外部 XHR abort、网络/超时及进度回调异常都只结算一次失败,并移除本次监听。完成后再取消不会操作旧 XHR。取消只终止本地请求,不能回滚远端已写入的对象;应用须先核对对象状态,再授权删除。
277
-
278
- ## 存储输入约束与跨端能力
279
-
280
- `@h-ai/storage/schemas` 是仅依赖 Zod 的浏览器安全入口;`api-contract` 与 `serv` 复用同一规则。Schema 只校验输入,服务端仍先认证、再按可信 session 授权每个资源,Local Provider 仍执行文件系统防逃逸。调用方不能把前缀校验或分页游标当成授权。
281
-
282
- ### 输入边界
283
-
284
- - `StorageKeySchema`:非空、不是纯空白,UTF-8 最多 1024 **字节**;不允许开头 `/`、反斜线、控制字符,以及独立的 `.` / `..` 路径段。不截断、不归一化 key。
285
- - `StoragePrefixSchema`:同样的路径和字节限制,另允许空字符串;允许部分文件名,`report` 不等于目录 `report/`。
286
- - `StorageDeleteKeysSchema`:每个 HTTP 请求 1–1000 个键,按原始数组数量计数,重复项不能绕过上限。Node `file.deleteMany` 允许更大数组,S3 每批最多 1000、Local 并发最多 32;失败时检查 `error.ext.deleteMany` 的已完成/失败/未知/未尝试进度,不能盲目重放整个数组。
287
- - 列表 `maxKeys`:整数 1–1000,`files` 与 `commonPrefixes` 共用预算。续页使用响应原样返回的 token,并保持相同 Provider/根目录/prefix/delimiter;不保证并发写入期间的快照。
288
- - 签名有效期:整数 1–604800 秒,Node 默认 3600 秒,默认 HTTP procedures 使用 900 秒。HTTP 上传签名只接受 `key`、`contentType`、`expiresIn`;下载签名只接受 `key`、`expiresIn`。未知字段直接拒绝,包括未实现的 `contentLength` / `maxSize`,不静默丢弃或假装限制已生效。
289
- - 输入校验在 HTTP 层返回 400;`apiClient` 返回 `HaiApiClientError.VALIDATION_FAILED`,`error.cause.status` 保留 400。合法输入的业务失败仍按 `HaiResult.success` 判断。
290
-
291
- ```ts
292
- import { StorageDeleteFilesInputSchema } from '@h-ai/api-contract/storage'
293
- import { StorageKeySchema } from '@h-ai/storage/schemas'
294
-
295
- const key = StorageKeySchema.safeParse('users/u1/report.pdf')
296
- if (key.success) {
297
- const input = StorageDeleteFilesInputSchema.parse({
298
- keys: [key.data]
299
- })
300
- // 把 input 交给已初始化的 client.storage.files.deleteMany;仍须检查返回值 success。
301
- }
302
- ```
303
-
304
- ### 能力映射
305
-
306
- | Node storage 能力 | api-contract / serv 默认端点 | apiClient / 浏览器消费 | 边界 |
307
- | -------------------------------------------------- | --------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
308
- | `presign.getUrl` | `POST /storage/presigned-urls/download` | `storage.presignedUrls.createDownload` | 仅 S3;Local 返回 `hai:storage:018`,不返回虚拟 URL |
309
- | `presign.putUrl` | `POST /storage/presigned-urls/upload` | `storage.presignedUrls.createUpload` 后直接 PUT 到 S3 | 仅内容类型与有效期;不是文件代理上传,也不提供上传大小/内容校验保证 |
310
- | `dir.list` | `GET /storage/files` | `storage.files.list` | 共享前缀约束与分页预算;逐次授权 prefix |
311
- | `file.head` | `POST /storage/files/metadata` | `storage.files.getMetadata` | 返回元数据,不能把 ETag 当内容 checksum |
312
- | `file.delete` | `DELETE /storage/files` | `storage.files.delete` | 逐资源授权 |
313
- | `file.deleteMany` | `POST /storage/files/delete-many` | `storage.files.deleteMany` | HTTP 总量上限 1000,返回可确认的删除进度 |
314
- | `file.put/get/getStream/copy/exists`、`dir.delete` | 无默认端点 | 应用自行提供有界且鉴权的服务端流程 | 流、Range、AbortSignal、metadata、cacheControl、条件写等 Node 选项不能直接作为预签名 API 入参 |
315
- | `checksum.generate/verify` | 无默认端点 | 应用选择明确的浏览器实现或服务端校验流程 | Node 无状态工具;不是上传接口自动校验 |
316
- | `presign.publicUrl` | 无默认端点 | 应用按公开资源策略使用 | 仅配置公开 S3 地址时有 URL;不是鉴权链接,Local 为 null |
317
-
318
- `FileUpload` / `ImageUpload` / `AvatarUpload` 的 `uploadHandler` 由应用实现。组件的 `maxSize`、`accept` 只负责前端选择体验,不能约束拿到签名后绕过组件的上传;服务端应在对象落库/回调或独立代理上传处验证实际大小、类型、内容和归属,再发布业务记录。不要把 Node `UploadOptions` 当成签名选项,也不要给组件编造不存在的框架上传端点。
319
-
320
371
  ## 开发测试
321
372
 
322
373
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/storage",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.19",
4
+ "version": "1.0.0-beta.20",
5
5
  "description": "Hai Framework object storage module for S3-compatible and local backends.",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -39,7 +39,7 @@
39
39
  "@aws-sdk/client-s3": "^3.1164.0",
40
40
  "@aws-sdk/lib-storage": "^3.1164.0",
41
41
  "@aws-sdk/s3-request-presigner": "^3.1164.0",
42
- "@h-ai/core": "1.0.0-beta.19",
42
+ "@h-ai/core": "1.0.0-beta.20",
43
43
  "zod": "^4.6.7"
44
44
  },
45
45
  "devDependencies": {