@h-ai/storage 0.1.0-alpha.66 → 0.1.0-alpha.68

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,16 +4,34 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
6
6
 
7
+ ## Introduction
8
+
7
9
  An object storage module with a unified `storage` entry point for S3-compatible storage and local filesystems.
8
10
 
9
- ## Supported backends
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
10
26
 
11
27
  - S3 protocol (AWS S3 / MinIO / RustFS / OSS, etc.)
12
28
  - Local filesystem
13
29
 
14
- ## Quick start
30
+ ## Getting started
31
+
32
+ ### Quick start
15
33
 
16
- ### Node.js server
34
+ #### Node.js server
17
35
 
18
36
  ```ts
19
37
  import { storage } from '@h-ai/storage'
@@ -36,6 +54,10 @@ const file = await storage.file.get('uploads/image.png')
36
54
 
37
55
  // 签名 URL
38
56
  const url = await storage.presign.getUrl('uploads/image.png', { expiresIn: 3600 })
57
+ if (!url.success) {
58
+ // local 不支持预签名;呈现错误或使用应用已有的鉴权文件接口。
59
+ // 失败结果不包含上传/下载 URL。
60
+ }
39
61
 
40
62
  // 关闭连接
41
63
  const closeResult = await storage.close()
@@ -44,31 +66,38 @@ if (!closeResult.success) {
44
66
  }
45
67
  ```
46
68
 
47
- ### Browser client
69
+ #### Browser client
48
70
 
49
71
  The browser entry exports only client capabilities (signed URL uploads/downloads), without the `storage` service object.
50
72
 
51
73
  ```ts
74
+ import { apiClient } from '@h-ai/api-client'
52
75
  import { downloadAndSave, uploadWithPresignedUrl } from '@h-ai/storage/client'
53
76
 
54
- // 业务端点路径由应用自行定义(例如 /api/files/presign-put)
55
- const { uploadUrl } = await fetch('/api/files/presign-put').then(r => r.json())
56
- await uploadWithPresignedUrl(uploadUrl, file)
77
+ // apiClient 已初始化,服务端使用 S3。
78
+ const signed = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
79
+ if (!signed.success)
80
+ throw new Error(signed.error.message)
81
+ await uploadWithPresignedUrl(signed.data.url, file)
57
82
  ```
58
83
 
59
84
  > Note: `@h-ai/storage` does not include HTTP routes. Implement the signed URL API in the application layer.
60
85
 
61
- ## HTTP API contracts
86
+ ### HTTP API contracts
62
87
 
63
- Public HTTP contracts come from `@h-ai/api-contract`; `createStorageProcedures({ storage })` in `@h-ai/serv` binds them to this module.
88
+ Public HTTP contracts come from `@h-ai/api-contract`; `createStorageProcedures({ storage, authorize })` in `@h-ai/serv` binds them to this module.
64
89
 
65
90
  ```ts
66
91
  import { apiClient } from '@h-ai/api-client'
67
92
 
68
93
  const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
94
+ if (!result.success)
95
+ throw new Error(result.error.message)
69
96
  ```
70
97
 
71
- ## API overview
98
+ Presigning requires S3. Local `getUrl()` / `putUrl()` return `HaiStorageError.UNSUPPORTED_OPERATION` (`hai:storage:018`), and `publicUrl()` returns `null`; no virtual URL is returned. serv and apiClient preserve the failure, so check `success` before consuming a URL. Use the application's existing authenticated file endpoints for local development; this module includes no HTTP file route or signature-verification service.
99
+
100
+ ### API overview
72
101
 
73
102
  - `storage.file`
74
103
  - `put/get/getStream/head/exists/delete/deleteMany/copy`
@@ -83,6 +112,21 @@ const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar
83
112
 
84
113
  `{ 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.
85
114
 
115
+ `file.deleteMany(keys)` returns `StorageDeleteResult`. Success data is `{ deletedKeys, failures: [] }`; any unconfirmed deletion returns `OPERATION_FAILED` with progress in `error.ext?.deleteMany` (precondition errors such as uninitialized storage may omit progress). Duplicate inputs are processed once, and returned keys exclude the configured prefix. S3 batches contain at most 1000 keys and verify every `Deleted`/`Errors` receipt. HTTP 200 alone is insufficient; missing, duplicate or unknown receipts produce `unknown` outcomes. Local deletion uses at most 32 concurrent operations.
116
+
117
+ Only explicit `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` items receive up to three attempts. Confirmed successes and permission failures are not resent. A dedicated `DeleteObjects` client disables SDK request retries. Network interruption immediately produces `unknown`; later unsent batches are `notAttempted`, without replaying unknown outcomes. `retryable` describes the fault category; callers must reconcile unknown results and concurrent writes before retrying. `dir.delete()` propagates failures but cannot prevent concurrent new objects. S3 deletes omit VersionId and may create delete markers in versioned buckets; historical versions are not purged.
118
+
119
+ `@h-ai/serv` / `@h-ai/api-client` expose the same success data and failure progress through `storage.files.deleteMany`.
120
+
121
+ ```ts
122
+ const result = await storage.file.deleteMany(['a.txt', 'b.txt'])
123
+ if (!result.success) {
124
+ const progress = result.error.ext?.deleteMany
125
+ // Reconcile unknown outcomes with application writes before compensating.
126
+ const knownFailures = progress?.failures.filter(item => item.outcome === 'failed' && item.retryable) ?? []
127
+ }
128
+ ```
129
+
86
130
  `checksum` is a stateless Node tool requiring no initialization. It generates/verifies the actual input bytes and consumes streams once. Defaults: SHA-256, lowercase hex. Options: `algorithm: 'sha256' | 'sha384' | 'sha512' | 'sha1' | 'md5'`, `encoding: 'hex' | 'base64'`, and `signal`. Mismatches return `ok(false)`; invalid format/algorithm and read failures return errors. MD5/SHA-1 are for compatibility integrity checks, not security authentication. ETags and S3 multipart composite checksums are not whole-file digests. The tool does not require compatible services to implement AWS checksum extensions.
87
131
 
88
132
  Cancelling a checksum stops waiting for the next chunk and requests closure of the input iterator. Local `getStream()` rejects directories with `INVALID_PATH` before returning a stream.
@@ -91,7 +135,7 @@ Cancelling a checksum stops waiting for the next chunk and requests closure of t
91
135
 
92
136
  > Note: `storage.config` returns a **redacted configuration snapshot**. S3 secret fields are replaced with `[REDACTED]`, so it cannot be used for reinitialization.
93
137
 
94
- ## Client helpers
138
+ ### Client helpers
95
139
 
96
140
  `@h-ai/storage/client` also provides these browser utilities:
97
141
 
@@ -102,7 +146,7 @@ Cancelling a checksum stops waiting for the next chunk and requests closure of t
102
146
  - `getMimeType(extension)`
103
147
  - `formatFileSize(bytes)`
104
148
 
105
- ## Configuration
149
+ ### Configuration
106
150
 
107
151
  - **S3**: requires `bucket / region / accessKeyId / secretAccessKey`; optional `endpoint / forcePathStyle / prefix / publicUrl`.
108
152
  - **Local**: requires `root`; optional `directoryMode / fileMode`.
@@ -124,7 +168,7 @@ if (!initResult.success) {
124
168
  }
125
169
  ```
126
170
 
127
- ### Operation logging
171
+ #### Operation logging
128
172
 
129
173
  Enable file, directory, and presigned URL operation logs with `operationLog`. Logs are emitted at the actual Local/S3 provider operations, without a shared wrapper in `storage-main.ts`. File contents are never logged; uploads log only the byte length.
130
174
 
@@ -154,9 +198,22 @@ await storage.init({
154
198
 
155
199
  `storage.presign.putUrl()` currently supports signing options such as `contentType` and `expiresIn`, but no `maxSize` constraint.
156
200
 
157
- For local storage, `storage.dir.list()` recursively scans files under the matching prefix. It supports `maxKeys` truncation but no true pagination with `continuationToken`. For large directories, narrow `prefix` or use the S3 provider's native pagination.
201
+ Local `storage.dir.list()` treats `prefix` as a key string prefix (for example, `reports/2026-`) and paginates in UTF-8 byte order. `maxKeys` is an integer from 1 to 1000, default 1000; files and common prefixes share that budget. `delimiter` folds keys at its first occurrence after the prefix into one `commonPrefixes` entry. An empty delimiter disables grouping; empty physical directories produce no entries. The grouping and counting rules follow [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).
202
+
203
+ `isTruncated: true` and `nextContinuationToken` appear only when another entry exists. Local cursors use no process cache and bind the canonical root directory, prefix and delimiter. Independent instances or restarted processes using the same root can continue a page, with a different `maxKeys` if needed. Malformed, unsupported-version or mismatched-scope cursors return `INVALID_LIST_OPTIONS`. Cursors grant no authorization and provide no snapshot: authorize the prefix, expect concurrent changes to affect later pages, and use the same canonical mount path. Each local request still scans and sorts matching directories; narrow the prefix or use S3 for large directories.
158
204
 
159
- ## Error handling
205
+ ```ts
206
+ let continuationToken: string | undefined
207
+ do {
208
+ const page = await storage.dir.list({ prefix: 'reports/2026-', maxKeys: 100, continuationToken })
209
+ if (!page.success)
210
+ return page
211
+ // Consume page.data.files and page.data.commonPrefixes.
212
+ continuationToken = page.data.nextContinuationToken
213
+ } while (continuationToken)
214
+ ```
215
+
216
+ ### Error handling
160
217
 
161
218
  ```ts
162
219
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -169,17 +226,31 @@ if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.cod
169
226
 
170
227
  Common error codes:
171
228
 
172
- | Error code | code | Description |
173
- | ----------------------------------- | ----------------- | ------------------------------- |
174
- | `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | Not initialized |
175
- | `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | Connection failed |
176
- | `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | Operation failed |
177
- | `HaiStorageError.NOT_FOUND` | `hai:storage:003` | File not found |
178
- | `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | Permission denied |
179
- | `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | Presigned URL generation failed |
180
- | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | Invalid configuration |
229
+ | Error code | code | Description |
230
+ | --------------------------------------- | ----------------- | ---------------------------------------------------------------------- |
231
+ | `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | Not initialized |
232
+ | `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | Connection failed |
233
+ | `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | Operation failed |
234
+ | `HaiStorageError.NOT_FOUND` | `hai:storage:003` | File not found |
235
+ | `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | Permission denied |
236
+ | `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | Presigned URL generation failed |
237
+ | `HaiStorageError.UNSUPPORTED_OPERATION` | `hai:storage:018` | Operation unsupported by the provider, including local presigning |
238
+ | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | Invalid local list options or cursor, including mismatched query scope |
239
+ | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | Invalid configuration |
240
+
241
+ ## Production shared state
181
242
 
182
- ## Tests
243
+ 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)。
244
+
245
+ ## Input limits and HTTP capabilities
246
+
247
+ 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#存储输入约束与跨端能力).
248
+
249
+ ## Browser upload cancellation
250
+
251
+ `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.
252
+
253
+ ## Development and testing
183
254
 
184
255
  ```bash
185
256
  pnpm --filter @h-ai/storage test
@@ -192,6 +263,10 @@ S3 integration tests require Docker or a compatible runtime. The same initializa
192
263
 
193
264
  Containers use temporary test credentials. Buckets are created through the S3 API after service readiness; containers and anonymous volumes are removed when the final lease is released.
194
265
 
266
+ Source JSDoc follows the [shared conventions](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证): public examples use published entry points, and internal examples are checked in their module scope. After building, run `pnpm check:jsdoc --package storage`; compiling examples does not replace backend behavior tests.
267
+
268
+ For the real Chrome regression gate, install Chrome, set `HAI_STORAGE_RUN_BROWSER_GATES=1`, then run `pnpm --filter @h-ai/storage exec vitest run tests/storage-client-browser.test.ts`. It sends actual HTTP PUT requests and checks native cancellation and listener cleanup; a missing browser fails the gate.
269
+
195
270
  ## License
196
271
 
197
272
  Apache-2.0
package/README.ja.md CHANGED
@@ -4,16 +4,34 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
6
 
7
+ ## 概要
8
+
7
9
  共通の `storage` 入口で S3 互換ストレージとローカルファイルシステムを操作するオブジェクトストレージモジュールです。
8
10
 
9
- ## 対応バックエンド
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
+ ## 主な機能
10
26
 
11
27
  - S3 プロトコル(AWS S3 / MinIO / RustFS / OSS など)
12
28
  - ローカルファイルシステム
13
29
 
14
- ## クイックスタート
30
+ ## 使い始める
31
+
32
+ ### クイックスタート
15
33
 
16
- ### Node.js サーバー
34
+ #### Node.js サーバー
17
35
 
18
36
  ```ts
19
37
  import { storage } from '@h-ai/storage'
@@ -36,6 +54,10 @@ const file = await storage.file.get('uploads/image.png')
36
54
 
37
55
  // 签名 URL
38
56
  const url = await storage.presign.getUrl('uploads/image.png', { expiresIn: 3600 })
57
+ if (!url.success) {
58
+ // local 不支持预签名;呈现错误或使用应用已有的鉴权文件接口。
59
+ // 失败结果不包含上传/下载 URL。
60
+ }
39
61
 
40
62
  // 关闭连接
41
63
  const closeResult = await storage.close()
@@ -44,31 +66,38 @@ if (!closeResult.success) {
44
66
  }
45
67
  ```
46
68
 
47
- ### ブラウザークライアント
69
+ #### ブラウザークライアント
48
70
 
49
71
  ブラウザー環境ではクライアント機能(署名付き URL のアップロード・ダウンロード)のみをエクスポートし、`storage` サービスオブジェクトは含みません。
50
72
 
51
73
  ```ts
74
+ import { apiClient } from '@h-ai/api-client'
52
75
  import { downloadAndSave, uploadWithPresignedUrl } from '@h-ai/storage/client'
53
76
 
54
- // 业务端点路径由应用自行定义(例如 /api/files/presign-put)
55
- const { uploadUrl } = await fetch('/api/files/presign-put').then(r => r.json())
56
- await uploadWithPresignedUrl(uploadUrl, file)
77
+ // apiClient 已初始化,服务端使用 S3。
78
+ const signed = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
79
+ if (!signed.success)
80
+ throw new Error(signed.error.message)
81
+ await uploadWithPresignedUrl(signed.data.url, file)
57
82
  ```
58
83
 
59
84
  > 注:`@h-ai/storage` に HTTP ルートは組み込まれていません。署名付き URL API はアプリケーション層で実装してください。
60
85
 
61
- ## HTTP API 契約
86
+ ### HTTP API 契約
62
87
 
63
- 公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/serv` の `createStorageProcedures({ storage })` で本モジュールの機能に接続します。
88
+ 公開 HTTP API は `@h-ai/api-contract` で定義し、`@h-ai/serv` の `createStorageProcedures({ storage, authorize })` で本モジュールの機能に接続します。
64
89
 
65
90
  ```ts
66
91
  import { apiClient } from '@h-ai/api-client'
67
92
 
68
93
  const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
94
+ if (!result.success)
95
+ throw new Error(result.error.message)
69
96
  ```
70
97
 
71
- ## API 一覧
98
+ 署名付き URL は S3 のみ対応します。local の `getUrl()` / `putUrl()` は `HaiStorageError.UNSUPPORTED_OPERATION`(`hai:storage:018`)、`publicUrl()` は `null` を返し、仮想 URL は返しません。serv と apiClient は失敗結果を保持するため、URL を使う前に `success` を確認してください。ローカル開発ではアプリの既存の認証付きファイル API を利用します。このモジュールに HTTP ファイルルートや署名検証サービスは含まれません。
99
+
100
+ ### API 一覧
72
101
 
73
102
  - `storage.file`
74
103
  - `put/get/getStream/head/exists/delete/deleteMany/copy`
@@ -83,6 +112,21 @@ const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar
83
112
 
84
113
  `{ ifNoneMatch: '*' }` は標準 S3 条件付き書き込み、またはローカルの原子的公開で上書きを拒否し、競合時に `ALREADY_EXISTS` を返します。非対応の S3 サービスではエラーとし、上書きに切り替えません。失敗・中止時は一時ファイルや未完了パートを削除しますが、公開済みオブジェクトは中止で取り消せません。
85
114
 
115
+ `file.deleteMany(keys)` は `StorageDeleteResult` を返します。成功時の data は `{ deletedKeys, failures: [] }`、削除未完了の項目がある場合は `OPERATION_FAILED` と `error.ext?.deleteMany` に進捗を返します(未初期化などの事前エラーには進捗がない場合があります)。重複入力は一度だけ処理し、返す key に設定 prefix を含めません。S3 は最大 1000 件ずつ送信し、すべての `Deleted` / `Errors` を照合します。HTTP 200 でも全件成功とは限らず、欠落・重複・不明な応答は `unknown` になります。ローカル削除の並行数は最大 32 件です。
116
+
117
+ 明示的な `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` の項目だけ最大 3 回試行し、成功済み・権限エラーは再送しません。`DeleteObjects` 専用クライアントは SDK のリクエスト再試行を無効化します。通信が中断した項目は直ちに `unknown`、後続の未送信バッチは `notAttempted` とし、不明な結果を再送しません。`retryable` は障害の分類であり、再試行前に不明な結果と並行書き込みを確認してください。`dir.delete()` は削除エラーを伝播しますが、並行して追加されるオブジェクトは防げません。S3 の削除は VersionId を指定しないため、バージョニングが有効な場合は削除マーカーを作成することがあり、過去のバージョンは消去しません。
118
+
119
+ `@h-ai/serv` / `@h-ai/api-client` の `storage.files.deleteMany` も同じ成功データと失敗進捗を返します。
120
+
121
+ ```ts
122
+ const result = await storage.file.deleteMany(['a.txt', 'b.txt'])
123
+ if (!result.success) {
124
+ const progress = result.error.ext?.deleteMany
125
+ // 不明な結果と業務上の書き込みを確認してから補償する。
126
+ const knownFailures = progress?.failures.filter(item => item.outcome === 'failed' && item.retryable) ?? []
127
+ }
128
+ ```
129
+
86
130
  `checksum` は初期化不要の Node 専用ツールです。実際の入力バイトを生成・検証し、ストリームは一度だけ消費します。既定値は SHA-256、小文字 hex。`algorithm: 'sha256' | 'sha384' | 'sha512' | 'sha1' | 'md5'`、`encoding: 'hex' | 'base64'`、`signal` を指定できます。不一致は `ok(false)`、不正形式・アルゴリズムや読み取り失敗はエラーです。MD5/SHA-1 は互換性確認のみで、認証用ではありません。ETag と multipart の複合 checksum はファイル全体の摘要と異なり、AWS checksum 拡張の実装は強制しません。
87
131
 
88
132
  checksum の中止時は次のチャンクを待つ処理を終了し、入力イテレーターの終了を要求します。ローカルの `getStream()` はディレクトリをストリームとして返さず、その場で `INVALID_PATH` を返します。
@@ -91,7 +135,7 @@ checksum の中止時は次のチャンクを待つ処理を終了し、入力
91
135
 
92
136
  > 注:`storage.config` は**機密情報をマスクした設定スナップショット**を返します。S3 の秘密情報は `[REDACTED]` に置換され、再初期化には使用できません。
93
137
 
94
- ## クライアントヘルパー
138
+ ### クライアントヘルパー
95
139
 
96
140
  `@h-ai/storage/client` は以下のブラウザー用ツールも提供します:
97
141
 
@@ -102,7 +146,7 @@ checksum の中止時は次のチャンクを待つ処理を終了し、入力
102
146
  - `getMimeType(extension)`
103
147
  - `formatFileSize(bytes)`
104
148
 
105
- ## 設定
149
+ ### 設定
106
150
 
107
151
  - **S3**:`bucket / region / accessKeyId / secretAccessKey` は必須。`endpoint / forcePathStyle / prefix / publicUrl` は任意。
108
152
  - **Local**:`root` は必須。`directoryMode / fileMode` は任意。
@@ -124,7 +168,7 @@ if (!initResult.success) {
124
168
  }
125
169
  ```
126
170
 
127
- ### 操作ログ
171
+ #### 操作ログ
128
172
 
129
173
  `operationLog` でファイル・ディレクトリー・署名付き URL の操作ログを有効にできます。Local/S3 Provider の実際の操作箇所で出力し、`storage-main.ts` で一括ラップしません。ファイル内容は記録せず、アップロードデータはバイト数のみを記録します。
130
174
 
@@ -154,9 +198,22 @@ await storage.init({
154
198
 
155
199
  `storage.presign.putUrl()` は現在 `contentType` や `expiresIn` などの署名パラメーターに対応しますが、`maxSize` 制限は提供しません。
156
200
 
157
- ローカルの `storage.dir.list()` は prefix に一致するファイルを再帰的に走査します。`maxKeys` による打ち切りのみ対応し、`continuationToken` による実際のページ分割はありません。大きなディレクトリーでは `prefix` を絞るか、S3 Provider のページ分割を使用してください。
201
+ ローカルの `storage.dir.list()` は `prefix` を key の文字列接頭辞(例:`reports/2026-`)として扱い、UTF-8 バイト順でページ分割します。`maxKeys` は 1–1000 の整数で既定値は 1000、`files` と `commonPrefixes` の合計件数に適用します。`delimiter` は prefix の後で最初に現れる区切りまでを一つの `commonPrefixes` 項目にまとめます。空文字列ではグループ化せず、空の物理ディレクトリーは項目になりません。グループ化と件数の規則は [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) に基づきます。
202
+
203
+ 後続項目がある場合にだけ `isTruncated: true` と `nextContinuationToken` を返します。ローカルカーソルはプロセス内キャッシュを使わず、正規化したルートディレクトリー、prefix、delimiter に結び付きます。同じルートを使用する別インスタンスや再起動後も継続でき、次ページの `maxKeys` は変更可能です。不正な形式、未対応バージョン、範囲が異なるカーソルは `INVALID_LIST_OPTIONS` になります。カーソルは認可情報でもスナップショットでもありません。prefix の認可は引き続き必要で、同時更新は後続ページに影響し、異なるマウントパスは同じ範囲として扱いません。各リクエストで対象ディレクトリーの走査と整列を行うため、大きな範囲では prefix を絞るか S3 を使用してください。
158
204
 
159
- ## エラー処理
205
+ ```ts
206
+ let continuationToken: string | undefined
207
+ do {
208
+ const page = await storage.dir.list({ prefix: 'reports/2026-', maxKeys: 100, continuationToken })
209
+ if (!page.success)
210
+ return page
211
+ // page.data.files と page.data.commonPrefixes を処理。
212
+ continuationToken = page.data.nextContinuationToken
213
+ } while (continuationToken)
214
+ ```
215
+
216
+ ### エラー処理
160
217
 
161
218
  ```ts
162
219
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -169,17 +226,31 @@ if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.cod
169
226
 
170
227
  主なエラーコード:
171
228
 
172
- | エラーコード | code | 説明 |
173
- | ----------------------------------- | ----------------- | ------------------------- |
174
- | `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | 未初期化 |
175
- | `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | 接続に失敗 |
176
- | `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | 操作に失敗 |
177
- | `HaiStorageError.NOT_FOUND` | `hai:storage:003` | ファイルが存在しない |
178
- | `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | 権限不足 |
179
- | `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | 署名付き URL の生成に失敗 |
180
- | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 設定エラー |
229
+ | エラーコード | code | 説明 |
230
+ | --------------------------------------- | ----------------- | ------------------------------------------------------------ |
231
+ | `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | 未初期化 |
232
+ | `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | 接続に失敗 |
233
+ | `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | 操作に失敗 |
234
+ | `HaiStorageError.NOT_FOUND` | `hai:storage:003` | ファイルが存在しない |
235
+ | `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | 権限不足 |
236
+ | `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | 署名付き URL の生成に失敗 |
237
+ | `HaiStorageError.UNSUPPORTED_OPERATION` | `hai:storage:018` | Provider が操作をサポートしない(local の署名付き URL など) |
238
+ | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | ローカル一覧の引数・カーソルが不正、または検索範囲が不一致 |
239
+ | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 設定エラー |
240
+
241
+ ## 本番の共有状態
181
242
 
182
- ## テスト
243
+ 本番は共有 S3 または互換サービスを使います。local は開発・明示的テスト用です。オブジェクト認可と読取後更新の条件付きコミットはアプリが担当します。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
244
+
245
+ ## 入力制約と HTTP 機能
246
+
247
+ キーは 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#存储输入约束与跨端能力)を参照してください。
248
+
249
+ ## ブラウザーアップロードのキャンセル
250
+
251
+ `uploadWithPresignedUrl(url, data, { onProgress, abortController })` は進捗通知にネイティブXHRを使用し、`onProgress` を省略するとfetchを使用します。事前にキャンセルされた場合は送信しません。準備中・送信中のキャンセル、外部からのXHR abort、通信・タイムアウト、進捗コールバックの例外は一度だけ失敗として確定し、この操作のリスナーを解除します。完了後のキャンセルは古いXHRを操作しません。キャンセルはローカル要求の停止であり、リモートに保存済みのオブジェクトを取り消せません。状態を確認してから、別途認可して削除してください。
252
+
253
+ ## 開発とテスト
183
254
 
184
255
  ```bash
185
256
  pnpm --filter @h-ai/storage test
@@ -192,6 +263,10 @@ S3 統合テストには Docker または互換ランタイムが必要です。
192
263
 
193
264
  コンテナーは一時生成したテスト認証情報を使用します。サービスの準備完了後に S3 API でバケットを作成し、最後のリース解放時にコンテナーと匿名ボリュームを削除します。
194
265
 
195
- ## License
266
+ ソースの JSDoc は[共通規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)に従います。公開例は実際の公開入口、内部例は元のモジュールスコープで型検査します。ビルド後に `pnpm check:jsdoc --package storage` を実行してください。例の型検査は実バックエンドの動作検証の代わりにはなりません。
267
+
268
+ 実Chromeの回帰ゲートはChromeを用意し、`HAI_STORAGE_RUN_BROWSER_GATES=1` を設定して `pnpm --filter @h-ai/storage exec vitest run tests/storage-client-browser.test.ts` を実行します。実際のHTTP PUTでネイティブキャンセルとリスナー解放を検証し、ブラウザーがなければ失敗します。
269
+
270
+ ## ライセンス
196
271
 
197
272
  Apache-2.0