@h-ai/audit 0.1.0-alpha.71 → 0.1.0-alpha.73

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
@@ -104,7 +104,7 @@ Record common events through `audit.helper`, with the public `AuditHelper` type.
104
104
  #### Input constraints
105
105
 
106
106
  - `audit.log(input)`: `action` / `resource` must be nonempty strings of at most 256 characters.
107
- - `audit.log(input)`: credential fields in `details` (such as `password` / `token` / `secret`) are redacted automatically before persistence; email addresses and phone numbers retain their original values by default.
107
+ - `audit.log(input)`: credential fields in `details` (such as `password` / `token` / `secret`) are redacted automatically before persistence; business privacy fields such as email and phone are also hidden by default and can be configured through privacy.
108
108
  - `audit.list(options)`: when both `startDate` and `endDate` are provided, `startDate <= endDate` is required.
109
109
  - `audit.getUserRecent(userId, limit)`: `userId` must be nonempty; `limit`, if provided, must be a positive integer.
110
110
  - `audit.cleanup(days)`: `days`, if provided, must be a nonnegative integer.
@@ -112,21 +112,79 @@ Record common events through `audit.helper`, with the public `AuditHelper` type.
112
112
 
113
113
  ### Error codes
114
114
 
115
- All operations return `HaiResult<T>`. Common error codes:
115
+ Business data operations return `HaiResult<T>`. Common error codes:
116
+
117
+ | Error code | code | Description |
118
+ | -------------------------------- | --------------- | ------------------------------------ |
119
+ | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | Recording failed |
120
+ | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | Query failed |
121
+ | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | Cleanup failed |
122
+ | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | Aggregation failed |
123
+ | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | Initialization in progress |
124
+ | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | Module not initialized |
125
+ | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | Invalid configuration |
126
+ | `HaiAuditError.BACKUP_DISABLED` | `hai:audit:020` | Backup disabled |
127
+ | `HaiAuditError.BACKUP_FAILED` | `hai:audit:021` | Backup or verification failed |
128
+ | `HaiAuditError.BACKUP_INVALID` | `hai:audit:022` | Invalid backup input |
129
+ | `HaiAuditError.BACKUP_CONFLICT` | `hai:audit:023` | Backup ID/export conditions conflict |
130
+ | `HaiAuditError.ALERTS_DISABLED` | `hai:audit:024` | Alerts disabled |
131
+ | `HaiAuditError.ALERTS_FAILED` | `hai:audit:025` | Alert check or notification failed |
132
+
133
+ > Note: `audit.close()` also returns `HaiResult<void>`. Email details in `audit.helper.passwordResetRequest()` are hidden by the default business privacy settings.
116
134
 
117
- | Error code | code | Description |
118
- | -------------------------------- | --------------- | -------------------------- |
119
- | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | Recording failed |
120
- | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | Query failed |
121
- | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | Cleanup failed |
122
- | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | Aggregation failed |
123
- | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | Initialization in progress |
124
- | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | Module not initialized |
125
- | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | Invalid configuration |
135
+ `audit.init()` awaits automatic table creation and repository readiness before success. DDL failures leave `isInitialized=false`; correct the database and retry. Custom joins, statistics and cleanup share the same barrier. Closing during initialization invalidates late publication. Check the init HaiResult before recording or querying. Audit does not own the shared RelDB connection; close audit before the database.
126
136
 
127
- > Note: `audit.close()` also returns `HaiResult<void>`. `audit.helper.passwordResetRequest()` records the original email address without additional masking by default.
137
+ ## Event context, privacy and retention
128
138
 
129
- `audit.init()` awaits automatic table creation and repository readiness before success. DDL failures leave `isInitialized=false`; correct the database and retry. Custom joins, statistics and cleanup share the same barrier. Closing during initialization invalidates late publication. Check the init HaiResult before recording or querying. Audit does not own the shared RelDB connection; close audit before the database.
139
+ Audit createdAt, alert windows and archive cutoffs share the database clock rather than individual node clocks. Scheduled backup/alert tasks declare idempotency based on their persistent deduplication guarantees; failures do not incorrectly block subsequent schedules as non-idempotent unknown tasks.
140
+
141
+ `capture: { enabled: true, includeReads: false }` controls automatic framework events. IAM records important account, authentication, password, authorization, API Key and session changes. Serv/Kit bind verified identity and trusted client addresses and capture writes and failures/denials; successful reads are skipped by default. Protocol or validation failures before identity is established remain anonymous. HTTP events contain operation/method/status or reason codes, without request bodies, tokens or error details. Applications still call `audit.log()` at domain completion/failure boundaries; disabling capture does not disable explicit logs. Initialize audit before IAM and close it after IAM. Trusted jobs/CLI callers must supply their actor and correlation context.
142
+
143
+ `audit.log(input, tx)` and `audit.capture(input, { tx })` use an existing RelDB transaction: events commit or roll back with the change, without an independent commit or a false success record after rollback. Record failed/denied attempts at the caller boundary after rollback. Automatic audit failures emit correlated diagnostics and preserve an already-applied business result to avoid unsafe retries. When recording is mandatory, check an explicit log's HaiResult in the same transaction before committing.
144
+
145
+ New records include category (system/business), outcome (success/failure/denied), severity (info/warning/error/critical), request/trace/scope IDs and actorSnapshot. Identity is copied at event time and survives account renames, role changes and deletion. With userId alone, configured name lookup captures the available username; lookup failure retains an explicitly unnamed identity and emits diagnostics. Trusted HTTP hosts should provide `audit.withContext({ actor, requestId, ipAddress, userAgent }, operation)` using verified identities. This context tool preserves the operation's return value or exception; business writes return HaiResult.
146
+
147
+ `audit.list({ category, outcome, severity, scopeId, requestId, traceId, sortBy, sortDirection })` filters and sorts through controlled fields. `getStats(7, { category: 'system' })` counts operations in that category. Business privacy is enabled by default; `privacy.fields` adds project field names. Credentials are always hidden, even with business privacy disabled. Hide the whole configured field when free text contains private information.
148
+
149
+ `retention: { enabled: true, minDays: 90 }` activates a shared database policy. Cleanup honors the actual minimum even with cleanup(0); triggers reject protected UPDATE, DELETE and conflicting replacement writes. An unconfigured fresh framework installation does not force retention protection: the default cleanup argument of 90 days alone is no immutability guarantee. Reinitialization without retention preserves the database policy; explicit trusted startup configuration can disable it. This covers ordinary row writes; database-owner DDL/privileges and stronger archive/storage protection belong to deployment management.
150
+
151
+ Stop old writers and back up before upgrading. `migrateAudit({ dryRun: true })` previews without writes; audit.init applies repeatable migrations. Unknown historical fields remain null. Legacy joined usernames are display compatibility, rather than historical identity evidence. Failed writes return failure and emit correlated diagnostics. close drains started writes before RelDB can close.
152
+
153
+ Redaction matches whole fields, including objects, arrays and sensitive values produced by custom JSON projections. capture returns not-initialized failure before initialization or after close; only an initialized module with capture disabled skips automatic events.
154
+
155
+ ## Private backups and scheduled archives
156
+
157
+ `backup.enabled` is off by default. Inject initialized `StorageFunctions` with `audit.init({ storage, backup: { enabled: true } })`; initialize RelDB first. `backup.batchSize` controls scan batches (default200), while `maxRecordBytes` bounds each UTF-8 record (default1 MiB). Oversized records fail the archive without truncating stored evidence.
158
+
159
+ `audit.backup.run({ id?, from?, to?, signal? })` writes private JSONL and returns its ID/key, count, SHA-256 and start/cutoff dates. The fixed namespace is `AUDIT_BACKUP_PREFIX` (hai-private/audit-backups/). Use a stable ID for one execution: a complete existing archive is verified and reused. Different export conditions, incomplete or damaged files fail rather than overwrite. Conditional-write races and uncertain writes are reconciled against the actual object. `audit.backup.verify(key, { signal? })` streams bounded format/count/checksum verification. The checksum establishes integrity, not authenticity.
160
+
161
+ Version1 contains a header, `{type:'entry',record}` lines and a footer. Entries preserve original IDs, ISO dates, stored details and nullable historical identities without joining current usernames. The footer SHA-256 covers all preceding UTF-8 lines including newlines; recordCount counts entry lines. Verified archives can be read offline or restored into a separate query store, without replacing protected live records. Online export uses a fixed cutoff and primary-key cursor; coverage reflects the actual scan. Pause writes/cleanup or use a native database snapshot for recovery at one exact point in time.
162
+
163
+ `backup.schedule` configures enabled, five-field cron (default `0 2 * * *`) and IANA timezone (default UTC). When both switches are enabled, `audit.schedulerPlan` returns tasks/hooks for explicit composition into `scheduler.init()`; it is null when no automatic backup or alert tasks are enabled. Its callback uses Scheduler's shared occurrence ID and cancellation signal and throws on failure so the task fails correctly. Reconcile unknown executions using Scheduler's rules. The example/fullstack runtimes wire this plan; other hosts explicitly compose existing tasks/hooks. Close Scheduler, IAM, Audit, Storage and RelDB in that order. Audit cancels/drains its own backups and leaves host Storage ownership intact.
164
+
165
+ ```ts
166
+ const plan = audit.schedulerPlan
167
+ if (plan) {
168
+ const initialized = await scheduler.init({ ...schedulerSettings, ...plan })
169
+ if (!initialized.success)
170
+ throw new Error(initialized.error.message)
171
+ const started = scheduler.start()
172
+ if (!started.success)
173
+ throw new Error(started.error.message)
174
+ }
175
+ ```
176
+
177
+ The host imports audit/scheduler and supplies validated schedulerSettings, with Audit/Storage/RelDB initialized. Use private archive storage: enabling backups rejects an S3 publicUrl setting, and deployment must restrict bucket/CDN access. Public asset endpoints should expose designated resources only. The example/fullstack runtimes expose canonical avatar paths; archives require an authorized audit download endpoint. Disabling backups preserves the private boundary of existing files.
178
+
179
+ ## Graded anomaly alerts
180
+
181
+ `alerts.enabled` defaults to false. Inject initialized Reach when enabling it: `audit.init({ reach, alerts: { enabled: true, rules: [...] } })`. Each rule has a stable id, exact actions, optional resource, outcomes and minimumSeverity filters, threshold/windowSeconds, notification severity, and targets containing id/provider/to. Windows default to 300 seconds (60–86400); default outcomes are failure/denied, while an empty outcomes list matches all. Rule and target IDs must be unique; providers must exist in Reach. Business roles remain application choices.
182
+
183
+ `audit.checkAlerts({signal?})` checks committed events in the current and previous fixed windows. Rolled-back events do not notify; downtime beyond two windows does not replay historical alerts. This is a fixed-window threshold check. maxNotificationsPerCheck defaults to 100; report.more indicates remaining targets. Shared database claims permit one external attempt per rule/target/window, using the database execution clock in the final atomic claim. Semantic rule or recipient changes create a new policy and may notify again.
184
+
185
+ Messages include only rule/target IDs, level, count and window, without event details or credentials. submitted means provider acceptance; queued means durable Reach enqueue, with queueId for inspection. Neither proves recipient delivery or reading. Failed, interrupted or crashed attempts retain unknown/sending/claimed receipts and are not automatically resent. Inspect audit.alert events and hai_audit_alert_claims, then reconcile with the notification endpoint. Reach-queued messages follow Reach recovery. Claims store no recipient address/body and confirmed submissions/enqueues are cleaned outside the check horizon; unresolved receipts remain for operational review; notification events follow audit retention.
186
+
187
+ alerts.schedule defaults to disabled, once per minute in UTC. audit.schedulerPlan composes enabled backup and alert tasks for explicit host Scheduler initialization; it is null without automatic tasks. Scheduler callbacks fail when checking fails or report.failed>0. Close Scheduler before Audit/Reach. Audit cancels further claims and drains in-flight sends within Reach provider timeouts without closing host Reach. Query/audit failures return a failed HaiResult; notification failures appear in a successful report.failed and must be checked by callers.
130
188
 
131
189
  ## Development and testing
132
190
 
package/README.ja.md CHANGED
@@ -104,7 +104,7 @@ await audit.init({
104
104
  #### 入力制約
105
105
 
106
106
  - `audit.log(input)`:`action` / `resource` は空でない文字列で、長さは 256 文字以内。
107
- - `audit.log(input)`:`details` 内の認証情報(`password` / `token` / `secret` など)は永続化前に自動的にマスクされます。メールアドレスと電話番号は既定で元の値を保持します。
107
+ - `audit.log(input)`:`details` 内の認証情報(`password` / `token` / `secret` など)は永続化前に自動的にマスクされます。メールアドレス・電話番号などの業務個人情報も既定で隠し、privacy で調整できます。
108
108
  - `audit.list(options)`:`startDate` と `endDate` を両方指定する場合は `startDate <= endDate` が必要です。
109
109
  - `audit.getUserRecent(userId, limit)`:`userId` は空でない文字列。`limit` を指定する場合は正の整数。
110
110
  - `audit.cleanup(days)`:`days` を指定する場合は 0 以上の整数。
@@ -112,21 +112,79 @@ await audit.init({
112
112
 
113
113
  ### エラーコード
114
114
 
115
- すべての操作は `HaiResult<T>` を返します。主なエラーコード:
115
+ 業務データ操作は `HaiResult<T>` を返します。主なエラーコード:
116
+
117
+ | エラーコード | code | 説明 |
118
+ | -------------------------------- | --------------- | -------------------------- |
119
+ | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | 記録に失敗 |
120
+ | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | 検索に失敗 |
121
+ | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | 削除に失敗 |
122
+ | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | 集計に失敗 |
123
+ | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | 初期化中 |
124
+ | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | モジュールが未初期化 |
125
+ | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | 設定エラー |
126
+ | `HaiAuditError.BACKUP_DISABLED` | `hai:audit:020` | バックアップ無効 |
127
+ | `HaiAuditError.BACKUP_FAILED` | `hai:audit:021` | バックアップ/検証失敗 |
128
+ | `HaiAuditError.BACKUP_INVALID` | `hai:audit:022` | バックアップ入力が無効 |
129
+ | `HaiAuditError.BACKUP_CONFLICT` | `hai:audit:023` | IDとエクスポート条件が衝突 |
130
+ | `HaiAuditError.ALERTS_DISABLED` | `hai:audit:024` | アラート無効 |
131
+ | `HaiAuditError.ALERTS_FAILED` | `hai:audit:025` | アラート検査・通知失敗 |
132
+
133
+ > 注:`audit.close()` も `HaiResult<void>` を返します。`audit.helper.passwordResetRequest()` のメール情報は既定の業務個人情報設定でマスクされます。
116
134
 
117
- | エラーコード | code | 説明 |
118
- | -------------------------------- | --------------- | -------------------- |
119
- | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | 記録に失敗 |
120
- | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | 検索に失敗 |
121
- | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | 削除に失敗 |
122
- | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | 集計に失敗 |
123
- | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | 初期化中 |
124
- | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | モジュールが未初期化 |
125
- | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | 設定エラー |
135
+ `audit.init()` は自動建表とリポジトリーの準備完了を待って成功します。DDL 失敗時は `isInitialized=false` のままで、DB を修正後に再試行できます。独自 JOIN・統計・削除も同じ準備完了を待ちます。初期化中の close は遅れた公開を無効化します。init の HaiResult を確認後に記録・検索し、共有 DB の接続は所有しないため audit の終了後に DB を終了します。
126
136
 
127
- > 注:`audit.close()` も `HaiResult<void>` を返します。`audit.helper.passwordResetRequest()` は既定でメールアドレスを追加マスクせず、そのまま記録します。
137
+ ## イベント文脈、個人情報と保持
128
138
 
129
- `audit.init()` は自動建表とリポジトリーの準備完了を待って成功します。DDL 失敗時は `isInitialized=false` のままで、DB を修正後に再試行できます。独自 JOIN・統計・削除も同じ準備完了を待ちます。初期化中の close は遅れた公開を無効化します。init の HaiResult を確認後に記録・検索し、共有 DB の接続は所有しないため audit の終了後に DB を終了します。
139
+ 監査 createdAt、アラート窓、アーカイブ終了時刻はノード時刻ではなく DB 時刻を共用します。バックアップ/アラート定時タスクは永続的な重複排除の保証により冪等を宣言し、失敗を非冪等の不明タスクとして後続スケジュールを阻止しません。
140
+
141
+ `capture: { enabled: true, includeReads: false }` でフレームワークの自動記録を制御します。IAM はアカウント、認証、パスワード、権限、API Key、セッションの重要変更を記録します。Serv/Kit は検証済み身元と信頼できる接続元を関連付け、書込と失敗・拒否を記録し、成功した読取は既定で省略します。身元確定前のプロトコル・入力検証失敗は匿名です。HTTP イベントは操作・メソッド・状態・理由コードのみを記録し、要求本文・トークン・内部エラーは複製しません。業務の完了・失敗境界ではアプリが `audit.log()` を使います。自動記録の無効化は明示的 log に影響しません。audit は IAM より先に初期化し、IAM の後に閉じます。信頼できるジョブ/CLI は実行主体と関連文脈を設定してください。
142
+
143
+ `audit.log(input, tx)` と `audit.capture(input, { tx })` は既存の RelDB トランザクションを使い、変更と一緒にコミット・ロールバックします。独立コミットやロールバック後の成功記録は残しません。失敗・拒否はロールバック完了後に呼出境界で記録してください。自動記録失敗は関連診断を出し、既に反映された業務結果は維持して危険な再試行を防ぎます。記録を必須とする業務は、同じトランザクション内で明示的 log の HaiResult を確認してからコミットします。
144
+
145
+ 新規記録は category(system/business)、outcome(success/failure/denied)、severity(info/warning/error/critical)、要求・追跡・範囲 ID と actorSnapshot を持ちます。イベント時点の身元を複製し、名前・役割の変更や削除後も保持します。userId のみなら設定された名前を取得し、取得失敗時は名前不明を明示して診断を出します。HTTP ホストは検証済み身元で `audit.withContext({ actor, requestId, ipAddress, userAgent }, operation)` を使います。この文脈ツールは操作の戻り値・例外を維持し、業務書込は HaiResult を返します。
146
+
147
+ `audit.list({ category, outcome, severity, scopeId, requestId, traceId, sortBy, sortDirection })` は分類検索と制御された並べ替え、`getStats(7, { category: 'system' })` は分類内の操作集計です。業務個人情報は既定で隠し、`privacy.fields` で項目名を追加できます。無効にしても password/token は常に隠します。自由文に個人情報がある場合は対応項目全体を隠してください。
148
+
149
+ `retention: { enabled: true, minDays: 90 }` は共有DBの最低保持を有効にします。cleanup(0) も最低期間を守り、トリガーは保護対象の UPDATE・DELETE・衝突置換を拒否します。未設定の新規フレームワークは保持保護を強制せず、削除引数の既定90日だけでは不変保証になりません。retention を省略した再初期化はDB設定を継続し、信頼できる起動設定で明示的に無効化できます。通常の行書込を対象とし、DB所有者のDDL・権限や独立アーカイブなどは配備側で管理します。
150
+
151
+ 更新前に旧書込を停止してバックアップし、`migrateAudit({ dryRun: true })` で読み取り専用の事前確認ができます。audit.init は再実行可能な移行を行います。歴史上不明な分類・結果・身元は null のままです。旧記録のユーザー表JOINは表示互換であり、当時の身元の証拠にはしません。書込失敗は失敗結果と関連診断を返し、close は開始済み書込を待つため、その後に RelDB を閉じてください。
152
+
153
+ マスキングは項目全体に適用し、オブジェクト、配列、独自 JSON 変換の機密値も隠します。未初期化または close 後の capture は未初期化エラーを返し、初期化済みモジュールで capture を無効にした場合だけ自動イベントを省略します。
154
+
155
+ ## 非公開バックアップと定時アーカイブ
156
+
157
+ `backup.enabled` は既定で無効です。RelDB を先に初期化し、初期化済み StorageFunctions を `audit.init({ storage, backup: { enabled: true } })` に渡します。batchSize は走査件数(既定200)、maxRecordBytes は UTF-8 一行の上限(既定1 MiB)です。上限超過は失敗し、元の監査証拠は切り詰めません。
158
+
159
+ `audit.backup.run({ id?, from?, to?, signal? })` は非公開 JSONL と ID/key、件数、SHA-256、開始/対象終了日時を返します。名前空間は AUDIT_BACKUP_PREFIX(hai-private/audit-backups/)に固定します。同じ実行は安定 ID を使い、既存の完全なアーカイブを検証して再利用します。条件の衝突・不完全・破損は失敗し、上書きしません。条件書込の競合や結果不明は実ファイルで確認します。`audit.backup.verify(key, { signal? })` は有界ストリームで形式・件数・チェックサムを検証します。チェックサムは完全性を示し、真正性を保証しません。
160
+
161
+ v1 は header、`{type:'entry',record}`、footer の行形式です。元の ID、ISO 日付、保存済み詳細、歴史上不明の身元を維持し、現在のユーザー名とは結合しません。footer の SHA-256 はそれ以前の UTF-8 行と改行を含み、recordCount は entry 件数です。検証後にオフライン参照や独立した検索ストアへ復元でき、保護されたオンライン記録は置換しません。オンライン出力は固定終了時刻と主キーカーソルによる実走査範囲です。同一時点への完全復旧には書込/削除を停止するか DB ネイティブの一貫したスナップショットを使います。
162
+
163
+ backup.schedule は enabled、5項目 cron(既定 `0 2 * * *`)、IANA timezone(既定 UTC)です。audit.schedulerPlan は有効なバックアップとアラートをまとめた tasks/hooks 計画を返し、自動タスクがなければ null です。コールバックは共有 occurrence ID と取消信号を使い、失敗時に例外を投げてタスクを失敗にします。結果不明は Scheduler の手順で確認します。example/fullstack は組み込み済みで、他のホストは既存のタスク/コールバックと合成します。Scheduler、IAM、Audit、Storage、RelDB の順に閉じます。Audit は自分のバックアップを取消・排出し、ホスト Storage は閉じません。
164
+
165
+ ```ts
166
+ const plan = audit.schedulerPlan
167
+ if (plan) {
168
+ const initialized = await scheduler.init({ ...schedulerSettings, ...plan })
169
+ if (!initialized.success)
170
+ throw new Error(initialized.error.message)
171
+ const started = scheduler.start()
172
+ if (!started.success)
173
+ throw new Error(started.error.message)
174
+ }
175
+ ```
176
+
177
+ ホストが audit/scheduler と検証済み schedulerSettings を提供し、Audit/Storage/RelDB を初期化済みとします。非公開ストレージを使い、バックアップ有効時は S3 publicUrl 設定を拒否します。バケット/CDN のアクセス制限も必要です。公開ファイル入口は指定資源のみを許可し、example/fullstack は正規のアバターパスだけを公開します。アーカイブの取得には監査の認可入口を使い、無効化後も既存ファイルは非公開のままです。
178
+
179
+ ## 等級別の異常アラート
180
+
181
+ `alerts.enabled` は既定で false。有効化時は初期化済み Reach を `audit.init({ reach, alerts: { enabled: true, rules: [...] } })` に渡します。ルールは一意な id、完全一致の actions、任意の resource、outcomes、minimumSeverity、threshold/windowSeconds、通知 severity、id/provider/to を持つ targets で指定します。既定の窓は300秒(60–86400秒)、結果は failure/denied、空の outcomes は全結果です。通知 Provider は Reach に登録済みである必要があります。業務ロールはアプリ側で決定します。
182
+
183
+ `audit.checkAlerts({signal?})` はコミット済みイベントの現在と直前の固定窓を確認します。ロールバックは通知せず、二窓を超えた停止の履歴は再送しません。maxNotificationsPerCheck は既定100、report.more は次回の未処理対象を示します。共有 DB でルール・対象・窓ごとに外部試行を一度だけ永続取得し、最終 SQL で DB 実行時刻を検証します。ルールの意味や送信先を変更すると別方針となり、新規通知する場合があります。
184
+
185
+ 通知はルール/対象 ID、等級、件数、期間のみで、イベント詳細や資格情報を複製しません。submitted は Provider の受付、queued は Reach の永続キュー登録であり、到達や既読を保証しません。queueId で照会できます。失敗・中断・クラッシュは unknown/sending/claimed 結果記録を残し、不確定な外部効果を自動再送しません。audit.alert と hai_audit_alert_claims を確認し、通知先と照合して対処します。Reach 登録済みキューは Reach の復旧規則に従います。取得表は宛先/本文を保存せず、確認範囲外の受付/キュー登録済み取得のみ清掃し、未決は運用確認まで保持し、通知イベントは監査保留に従います。
186
+
187
+ alerts.schedule は独立して有効化でき、既定は毎分・UTC。audit.schedulerPlan は有効なバックアップ/アラートをまとめ、ホストが Scheduler に明示的に渡します。自動タスクがなければ null。検査失敗または report.failed>0 はタスク失敗です。Scheduler を先に停止し、次に Audit/Reach を閉じます。Audit は新規取得を取消し、Reach の Provider タイムアウト内で実行中通知を待機し、ホスト Reach は閉じません。DB/監査失敗は HaiResult の失敗、通知失敗は成功レポートの failed に現れるため、呼出側で確認します。
130
188
 
131
189
  ## 開発とテスト
132
190
 
package/README.md CHANGED
@@ -95,7 +95,7 @@ await audit.init({
95
95
  - `audit.list(options)` - 返回统一 `PaginatedResult<AuditLogWithUser>`(items/total/page/pageSize,含用户名 JOIN);页码/条数复用 core 分页规范化,默认 pageSize 为 20,调用方直接使用返回元数据。
96
96
  - `audit.getUserRecent(userId, limit)` - 获取用户最近活动
97
97
  - `audit.cleanup(days)` - 清理旧日志
98
- - `audit.getStats(days)` - 获取操作统计
98
+ - `audit.getStats(days, filters)` - 获取按分类等条件筛选的操作统计
99
99
  - `audit.helper.login/logout/register/...` - 便捷记录器
100
100
  - `audit.close()` - 关闭模块
101
101
 
@@ -104,7 +104,7 @@ await audit.init({
104
104
  #### 输入约束
105
105
 
106
106
  - `audit.log(input)`:`action` / `resource` 必须为非空字符串,且长度不超过 256。
107
- - `audit.log(input)`:`details` 中的凭证类敏感字段(如 `password` / `token` / `secret`)会在持久化前自动脱敏;邮箱、手机号默认保留原值。
107
+ - `audit.log(input)`:`details` 中的凭证类敏感字段(如 `password` / `token` / `secret`)会在持久化前自动脱敏;业务隐私字段(如邮箱、手机号)默认同时隐藏,可通过 privacy 配置调整。
108
108
  - `audit.list(options)`:若同时传 `startDate` 与 `endDate`,必须满足 `startDate <= endDate`。
109
109
  - `audit.getUserRecent(userId, limit)`:`userId` 必须为非空字符串;`limit`(如传入)必须为正整数。
110
110
  - `audit.cleanup(days)`:`days`(如传入)必须为非负整数。
@@ -112,21 +112,79 @@ await audit.init({
112
112
 
113
113
  ### 错误码
114
114
 
115
- 所有操作返回 `HaiResult<T>`,常用错误码如下:
115
+ 业务数据操作返回 `HaiResult<T>`,常用错误码如下:
116
+
117
+ | 错误码 | code | 说明 |
118
+ | -------------------------------- | --------------- | ---------------------- |
119
+ | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | 记录失败 |
120
+ | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | 查询失败 |
121
+ | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | 清理失败 |
122
+ | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | 统计失败 |
123
+ | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | 初始化进行中 |
124
+ | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | 模块未初始化 |
125
+ | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | 配置错误 |
126
+ | `HaiAuditError.BACKUP_DISABLED` | `hai:audit:020` | 备份未启用 |
127
+ | `HaiAuditError.BACKUP_FAILED` | `hai:audit:021` | 备份或校验失败 |
128
+ | `HaiAuditError.BACKUP_INVALID` | `hai:audit:022` | 备份输入无效 |
129
+ | `HaiAuditError.BACKUP_CONFLICT` | `hai:audit:023` | 备份编号与导出条件冲突 |
130
+ | `HaiAuditError.ALERTS_DISABLED` | `hai:audit:024` | 告警未启用 |
131
+ | `HaiAuditError.ALERTS_FAILED` | `hai:audit:025` | 告警检查或通知失败 |
132
+
133
+ > 说明:`audit.close()` 也返回 `HaiResult<void>`;`audit.helper.passwordResetRequest()` 的邮箱详情默认经过业务隐私脱敏。
116
134
 
117
- | 错误码 | code | 说明 |
118
- | -------------------------------- | --------------- | ------------ |
119
- | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | 记录失败 |
120
- | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | 查询失败 |
121
- | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | 清理失败 |
122
- | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | 统计失败 |
123
- | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | 初始化进行中 |
124
- | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | 模块未初始化 |
125
- | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | 配置错误 |
135
+ `audit.init()` 会等待自动建表和仓库就绪才返回成功;DDL 失败保持 `isInitialized=false`,修正数据库后可重试。自定义 JOIN、统计和清理也使用同一就绪屏障;初始化期间关闭会使迟到结果失效。先检查 init 的 HaiResult,再立即记录或查询;audit 不拥有共享 RelDB 的连接,关闭 audit 后再关闭数据库。
126
136
 
127
- > 说明:`audit.close()` 也返回 `HaiResult<void>`;`audit.helper.passwordResetRequest()` 会记录原始邮箱,默认不会额外掩码。
137
+ ## 事件上下文、隐私与保留
128
138
 
129
- `audit.init()` 会等待自动建表和仓库就绪才返回成功;DDL 失败保持 `isInitialized=false`,修正数据库后可重试。自定义 JOIN、统计和清理也使用同一就绪屏障;初始化期间关闭会使迟到结果失效。先检查 init 的 HaiResult,再立即记录或查询;audit 不拥有共享 RelDB 的连接,关闭 audit 后再关闭数据库。
139
+ 审计 createdAt、告警窗口与归档截止共用数据库时钟,不以各节点系统钟作为共享真值。备份与告警调度任务使用已验证的持久去重保障声明幂等;失败不会被错误地当作非幂等未知任务而阻塞后续调度。
140
+
141
+ `capture: { enabled: true, includeReads: false }` 控制框架自动采集。IAM 记录关键账号、鉴别、密码、授权、API Key 和会话事件;Serv/Kit 绑定已验证身份与可信地址,记录写请求及失败/拒绝,成功读取默认跳过。协议/输入校验前尚未确认身份时保留匿名事件。HTTP 入口只记录操作、方法、状态或原因码,不复制请求体、令牌和错误详情。业务含义仍由应用在完成/失败边界调用 `audit.log()` 记录;关闭自动采集不影响显式 log。审计应先于 IAM 初始化,并在 IAM 关闭后关闭。可信任务/CLI 应显式提供操作身份和关联上下文。
142
+
143
+ `audit.log(input, tx)` 与 `audit.capture(input, { tx })` 可使用调用者现有 RelDB 事务;事件与变更一并提交或回滚,不独立提交,也不留下回滚成功的假记录。事务回滚会移除其中的事件,调用入口应在回滚完成后记录失败/拒绝。自动采集写入失败会输出关联诊断,已生效的业务不会被改写成可盲目重试的失败;需要强制审计的业务应在同一事务检查显式 log 的 HaiResult 后再提交。
144
+
145
+ 新记录包含 `category`(system/business)、`outcome`(success/failure/denied)、`severity`(info/warning/error/critical)、请求/链路/范围标识和 `actorSnapshot`。身份快照在事件发生时复制,改名、改角色或删除账号不会改写它。只传 userId 时按配置映射尝试捕获用户名;映射不可读时记录明确缺少名称的身份并输出诊断。可信 HTTP 宿主应通过 `audit.withContext({ actor, requestId, ipAddress, userAgent }, operation)` 提供完整上下文,不能用未验证的请求体充当操作者。该工具保留 operation 的同步/异步结果及异常,业务写入仍返回 HaiResult。
146
+
147
+ `audit.list({ category, outcome, severity, scopeId, requestId, traceId, sortBy, sortDirection })` 支持分类筛选和受控排序,`getStats(7, { category: 'system' })` 统计指定类别的操作。`privacy.enabled` 默认开启,隐藏邮箱、电话等业务字段;`privacy.fields` 可补充项目字段名。凭据始终脱敏,关闭业务隐私不会泄漏 password/token。自由文本包含隐私时应配置整个对应字段隐藏。
148
+
149
+ `retention: { enabled: true, minDays: 90 }` 开启最低保留。共享数据库保存策略,清理使用实际最低期限,`cleanup(0)` 不能绕过;数据库触发器拒绝保护期内的 UPDATE、DELETE 和冲突覆盖写。框架首次未配置时不强制保护,清理参数默认90天本身不构成不可变保证。未传 retention 的重初始化沿用既有策略,只有可信启动配置显式关闭才解除。该保护针对正常行级写入;数据库所有者的 DDL/权限、独立归档和更强存储保护需由部署管理。
150
+
151
+ 升级前先停旧写入并备份,`migrateAudit({ dryRun: true })` 只读预检;audit.init 自动执行可重入升级。历史未知分类、结果和身份快照保持 null,不补造历史身份。列表对无快照的历史记录保留旧的用户表关联展示,不能当作事件时身份。写入失败返回失败并输出关联诊断;close 等待已开始写入后再释放模块,随后才能关闭 RelDB。
152
+
153
+ 脱敏匹配整个字段:对象、数组以及自定义 JSON 投影内的敏感值同样隐藏。关闭或尚未初始化 Audit 时,capture 返回未初始化失败;只有已初始化模块的关闭采集配置才跳过自动事件。
154
+
155
+ ## 私有备份与定时归档
156
+
157
+ `backup.enabled` 默认关闭。开启时向 `audit.init({ storage, backup: { enabled: true } })` 注入已经初始化的 `StorageFunctions`;数据库仍先初始化。`backup.batchSize` 控制扫描批量(默认200),`maxRecordBytes` 限制单条 UTF-8 记录(默认1 MiB),超限返回失败并保留原始审计,不截断证据。
158
+
159
+ `audit.backup.run({ id?, from?, to?, signal? })` 输出私有 JSONL 文件并返回 ID、key、记录数、SHA-256、开始和截止时间。固定命名空间为 `AUDIT_BACKUP_PREFIX`(hai-private/audit-backups/)。同一执行使用稳定 ID;已有完整归档校验后复用,导出条件冲突、文件不完整或损坏均返回失败,不能覆盖。条件写竞争与写结果未知时核对实际文件。`audit.backup.verify(key, { signal? })` 有界流式核对格式、记录数和校验和;校验和用于完整性,不代表来源真实性。
160
+
161
+ 归档 v1 按行保存 header、`{type:'entry',record}` 和 footer。记录保留原始 ID、ISO 日期、已存详情及可空历史身份;不关联当前用户名。footer 的 SHA-256 覆盖之前所有 UTF-8 行及换行,且 recordCount 等于 entry 数量。可核对后离线读取或恢复至独立查询存储;不覆盖在线受保护记录。在线导出按固定截止时间与主键游标扫描,包含范围以实际扫描为准;需要同一时点的全量恢复时先暂停写入和清理,或采用数据库原生一致性快照。
162
+
163
+ 定时配置位于 `backup.schedule`:`enabled`、五字段 cron(默认 `0 2 * * *`)、IANA timezone(默认 UTC)。开启后 `audit.schedulerPlan` 返回可显式合并进 `scheduler.init()` 的 tasks/hooks 计划;没有自动任务时为 null。回调复用 Scheduler 的共享执行编号和取消信号,并在失败时抛错使任务准确失败;未知执行按 Scheduler 规则核对,不能盲目重跑。示例/fullstack 已装配此计划,其它宿主显式合并已有任务与回调。先关闭 Scheduler,再关闭 IAM、Audit、Storage 和 RelDB;Audit 取消并等待自己的备份,不关闭宿主 Storage。
164
+
165
+ ```ts
166
+ const plan = audit.schedulerPlan
167
+ if (plan) {
168
+ const initialized = await scheduler.init({ ...schedulerSettings, ...plan })
169
+ if (!initialized.success)
170
+ throw new Error(initialized.error.message)
171
+ const started = scheduler.start()
172
+ if (!started.success)
173
+ throw new Error(started.error.message)
174
+ }
175
+ ```
176
+
177
+ 上例由宿主导入 `audit`、`scheduler` 并提供已校验的 schedulerSettings,Audit/Storage/RelDB 已初始化。归档使用私有存储,开启备份时拒绝配置了公开 publicUrl 的 S3;部署仍须限制桶/CDN访问。公开静态文件入口应仅开放指定资源;管理示例与 fullstack 仅公开规范头像路径,如提供归档下载,须通过审计授权接口处理,关闭备份也不改变已有文件的私有边界。
178
+
179
+ ## 分级异常告警
180
+
181
+ `alerts.enabled` 默认关闭。开启时注入已初始化的 Reach:`audit.init({ reach, alerts: { enabled: true, rules: [...] } })`。规则通过稳定 id、actions(精确事件名)、可选 resource、outcomes、minimumSeverity 筛选事件,threshold/windowSeconds 指定固定窗口阈值,severity 指定通知等级,targets 中的 id/provider/to 指定人员或目标。默认窗口300秒,范围60–86400秒;默认仅失败/拒绝,空 outcomes 表示所有结果。规则和目标 id 必须各自唯一;provider 须已在 Reach 注册。不将具体业务角色写入框架。
182
+
183
+ `audit.checkAlerts({signal?})` 检查已提交事件的当前及前一个固定窗口,回滚事件不触发;停机超过两窗不补发历史通知。它不是滑动窗口或完整监控系统。默认单轮最多尝试100次,maxNotificationsPerCheck 可配置,report.more 表示仍有目标待后续检查。已达阈值的规则/目标/窗口在共享数据库原子领取,一次外部尝试前持久化;领取同时核验数据库执行时钟,避免节点偏差或挂起恢复重发。规则语义或通知目标变更形成新策略,可能产生新的通知。
184
+
185
+ 通知只包含规则/目标编号、等级、计数与窗口,不复制审计详情或凭据。submitted 表示 Provider 接受,queued 仅表示 Reach 持久入队,可通过 queueId 查询;都不等于人员已收到或阅读。失败/中断/崩溃留下 unknown、sending 或 claimed 回执,不自动重发不确定外部效果。运维先检查 `audit.alert` 事件与 `hai_audit_alert_claims`,核验通知端后处理;Reach 已入队的消息遵循其自身恢复规则。领取表不保存地址/正文,查询范围外已确认提交/入队的领取定期清理;未决回执保留供运维核验,通知审计遵守审计保留策略。
186
+
187
+ `alerts.schedule` 可独立启用,默认每分钟、UTC。`audit.schedulerPlan` 合并已启用的备份和告警任务,由宿主显式交给 Scheduler;计划没有自动任务时为 null。调度回调在检查失败或 failed>0 时准确失败。先关闭 Scheduler,再关闭 Audit 和 Reach;关闭 Audit 取消后续领取并等待在途通知完成(受 Reach Provider 超时约束),不关闭宿主 Reach。数据库查询/审计失败返回失败;通知业务失败在成功检查报告的 failed 字段体现,调用方必须检查。
130
188
 
131
189
  ## 开发测试
132
190