haechi 1.7.0 → 1.8.0
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.ko.md +5 -1
- package/README.md +7 -1
- package/docs/README.md +5 -0
- package/docs/current/api-stability.ko.md +5 -4
- package/docs/current/api-stability.md +5 -4
- package/docs/current/config-version.ko.md +1 -1
- package/docs/current/config-version.md +1 -1
- package/docs/current/configuration.ko.md +16 -1
- package/docs/current/configuration.md +50 -1
- package/docs/current/release-1.8-implementation-scope.ko.md +200 -0
- package/docs/current/release-1.8-implementation-scope.md +230 -0
- package/docs/current/risk-register-release-gate.ko.md +11 -8
- package/docs/current/risk-register-release-gate.md +11 -8
- package/docs/current/threat-model.ko.md +3 -3
- package/docs/current/threat-model.md +3 -3
- package/haechi.config.example.json +9 -0
- package/package.json +3 -2
- package/packages/cli/runtime.mjs +67 -0
- package/packages/crypto/index.mjs +39 -7
- package/packages/metrics/index.mjs +2 -1
- package/packages/proxy/index.mjs +116 -4
- package/packages/usage/index.mjs +177 -0
package/README.ko.md
CHANGED
|
@@ -35,6 +35,8 @@ Haechi는 LLM·MCP·vLLM·Ollama 및 에이전트 payload가 모델, 도구, 로
|
|
|
35
35
|
- `haechi mcp-wrap -- <command>`: MCP 서버를 양방향 stdio 보호로 감쌉니다
|
|
36
36
|
- `haechi plugin-keygen` / `plugin-sign` / `plugin-verify`: 서명된 `authProvider` 플러그인을 저작하고 검증합니다(Ed25519 신뢰 게이트)
|
|
37
37
|
|
|
38
|
+
**비목표(Non-goals):** Haechi는 로컬/자체 호스팅 LLM 백엔드(vLLM, Ollama, llama.cpp, OpenAI 호환 엔드포인트)를 위한 **local-first** 게이트웨이입니다. 관리형 클라우드 추론 백엔드는 명시적으로 범위 밖입니다 — 특히 **AWS Bedrock**은 SigV4 요청/바디 서명이 Haechi의 실시간 바디 재작성 redaction과 근본적으로 충돌하고(payload를 재작성하면 서명이 무효화됨), **Google Vertex AI**도 같은 부류의 이유로 범위 밖입니다. 이런 제공자로 가는 트래픽을 보호하려면 Haechi 앞에 자체 호스팅 **LiteLLM** 등 OpenAI 호환 proxy를 두어, Haechi가 검사하는 요청이 이미 평범한 OpenAI 호환 JSON payload가 되게 하십시오.
|
|
39
|
+
|
|
38
40
|
## 데모
|
|
39
41
|
|
|
40
42
|
<p align="center">
|
|
@@ -229,7 +231,7 @@ JWT/JWKS 인증과 KMS 기반 key custody(및 기타 선택 기능)는 **`haechi
|
|
|
229
231
|
|
|
230
232
|
## 위성 패키지
|
|
231
233
|
|
|
232
|
-
선택 기능은 **npm에 독립 발행되는 `haechi-*` 패키지**로 제공됩니다 — 각각 core와 별도로 버저닝되고, 기본적으로 `node:` 전용이며(KMS나 Redis 클라이언트 같은 무거운 SDK는 optional peer), core major를 추적하는 `haechi` peer 범위를 선언합니다. 대부분의 위성은 `>=0.8.0 <2.0.0`를 유지하고, `haechi-crypto-kms@0.
|
|
234
|
+
선택 기능은 **npm에 독립 발행되는 `haechi-*` 패키지**로 제공됩니다 — 각각 core와 별도로 버저닝되고, 기본적으로 `node:` 전용이며(KMS나 Redis 클라이언트 같은 무거운 SDK는 optional peer), core major를 추적하는 `haechi` peer 범위를 선언합니다. 대부분의 위성은 `>=0.8.0 <2.0.0`를 유지하고, `haechi-crypto-kms@0.4.0`은 core v3 crypto-AAD helper를 공유하므로 `>=1.8.0 <2.0.0`을 요구합니다.
|
|
233
235
|
|
|
234
236
|
**위성과 함께 core를 반드시 설치하세요** — `haechi`는 **번들되지 않은 peer dependency**이므로, 위성만으로는 동작하지 않습니다:
|
|
235
237
|
|
|
@@ -385,3 +387,5 @@ Haechi는 의도적으로 범위를 좁혔습니다. 아래는 숨기지 않고
|
|
|
385
387
|
1.6.0은 **crypto 봉투**를 강화합니다. 로컬 AES-256-GCM provider가 이제 키별 nonce 예산을 강제합니다 — 랜덤 96-bit IV는 키당 한정된 횟수의 암호화까지만 안전하므로(NIST SP 800-38D §8.3), provider가 키별 암호화 횟수를 세어 키 파일에 영속화하고, 50%에서 경고하며, **한도에서 fail-closed**(`haechi init --force`로 회전)합니다. 읽기 전용 키 파일은 프로세스 단위 한도로 degrade합니다. `haechi status`가 예산을 노출하고(`keys.nonceBudget`, 사용 %), `haechi/crypto`는 `readNonceBudget`를 export합니다. 명명된 `gate:security` CI 잡이 횡단 보안 invariant(audit 평문 없음, hard-block 비억제, AAD/AEAD 바인딩, nonce 예산, privacy-profile strengthen-only)를 독립 required check로 실행합니다. additive(**마이너**); `configVersion`은 `1` 유지; core는 zero runtime dependency 유지.
|
|
386
388
|
|
|
387
389
|
1.7.0은 다음 crypto-envelope hardening 단계를 닫습니다. 새 envelope는 `v:2`, `aadEncoding:"nfkc-json-v2"`를 사용하므로 crypto AAD가 NFKC-normalized string value/object key를 포함한 정렬 canonical JSON(`canonicalizeCryptoAad`)으로 고정됩니다. Legacy v1 envelope는 기존 AAD canonicalization으로 계속 복호화됩니다. NFKC 때문에 같은 object level에서 key collision이 생기면 fail-closed하고, token-vault ciphertext는 token `expiresAt`를 envelope에 담아 stale ciphertext를 crypto 계층에서도 거부합니다. `haechi-crypto-kms@0.3.0`도 같은 core helper를 사용하며 `haechi >=1.7.0`를 요구합니다. additive(**마이너**); `configVersion`은 `1` 유지; core는 zero runtime dependency 유지.
|
|
390
|
+
|
|
391
|
+
1.8.0은 이후 user-management와 quota 모듈이 딛고 설 usage/accounting 계약을 추가합니다: `providers.usageRecorder`, `providers.quotaProvider`, 새 `haechi/usage` export, additive `usage` config 섹션. usage는 기본 비활성이며, 활성화하면 proxy가 완료된 프록시 요청마다 주입된 recorder와(기본적으로) hash-chained audit 로그를 통해 PII-safe `usage_recorded` 이벤트 하나를 발행합니다. 이벤트는 bounded/hash-only 필드(`pathHash`, `modelHash`, status/outcome, byte count, duration, 5-field PII-safe identity projection)만 담으며 raw body content, model name, bearer token, scope, label, subject, provider credential은 절대 포함하지 않습니다. quota provider 계약은 1.8에서 검증·노출되지만, request denial은 upstream 전달 전에 전체 request context를 갖고 집행할 수 있도록 의도적으로 1.9로 이월됩니다.
|
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ The current scope focuses on local adoption:
|
|
|
35
35
|
- `haechi mcp-wrap -- <command>`: wrap an MCP server with bidirectional stdio protection
|
|
36
36
|
- `haechi plugin-keygen` / `plugin-sign` / `plugin-verify`: author and verify a signed `authProvider` plugin (Ed25519 trust gate)
|
|
37
37
|
|
|
38
|
+
**Non-goals:** Haechi is a **local-first** gateway for local/self-hosted LLM backends (vLLM, Ollama, llama.cpp, and OpenAI-compatible endpoints). Managed cloud inference backends are explicitly out of scope — **AWS Bedrock** in particular, because Bedrock's SigV4 request/body signing is fundamentally incompatible with Haechi's in-flight body-rewriting redaction (rewriting the payload invalidates the signature), and **Google Vertex AI** for the same class of reasons. To protect traffic to those providers, put a self-hosted **LiteLLM** or other OpenAI-compatible proxy in front of Haechi so the request Haechi inspects is already a plain OpenAI-compatible JSON payload.
|
|
39
|
+
|
|
38
40
|
## Demo
|
|
39
41
|
|
|
40
42
|
<p align="center">
|
|
@@ -229,7 +231,7 @@ JWT/JWKS auth and KMS-backed key custody (and other optional capabilities) ship
|
|
|
229
231
|
|
|
230
232
|
## Satellite packages
|
|
231
233
|
|
|
232
|
-
Optional capabilities ship as independently-published **`haechi-*` packages on npm** — each versioned separately from core, `node:`-only by default (heavy SDKs like a KMS or Redis client are optional peers), and each declaring a `haechi` peer range that tracks the core major. Most satellites keep `>=0.8.0 <2.0.0`; `haechi-crypto-kms@0.
|
|
234
|
+
Optional capabilities ship as independently-published **`haechi-*` packages on npm** — each versioned separately from core, `node:`-only by default (heavy SDKs like a KMS or Redis client are optional peers), and each declaring a `haechi` peer range that tracks the core major. Most satellites keep `>=0.8.0 <2.0.0`; `haechi-crypto-kms@0.4.0` requires `>=1.8.0 <2.0.0` because it shares the core v3 crypto-AAD helper.
|
|
233
235
|
|
|
234
236
|
**Install the core alongside any satellite** — `haechi` is a **peer dependency, not bundled**, so a satellite does nothing on its own:
|
|
235
237
|
|
|
@@ -278,6 +280,8 @@ Each package's README covers its usage and exact peer requirements. The satellit
|
|
|
278
280
|
| `auth.provider` / `auth.store` | `none` / `.haechi/auth.json` | `none`/`bearer`/`external`. Bearer tokens stored as keyed-HMAC hashes (0600) |
|
|
279
281
|
| `policy.profiles` / `policy.profileBinding` | — | Named per-client policy profiles bound by scope → label → required `default` |
|
|
280
282
|
| `policy.modelAllowlist` / `policy.rate` | — | Allowed model names (403 otherwise); requests-per-minute rate limit (429) — also settable per profile |
|
|
283
|
+
| `usage.enabled` / `usage.recorder` | `false` / `none` | 1.8 usage/accounting contract. When enabled, emits PII-safe `usage_recorded` events through `providers.usageRecorder` and optionally the audit log |
|
|
284
|
+
| `usage.quota.provider` | `none` | Reserved quotaProvider seam (`providers.quotaProvider`) for 1.9 quota enforcement; 1.8 validates/exposes the contract but does not deny requests |
|
|
281
285
|
|
|
282
286
|
The table above is a quick reference. The full per-key reference — types, validation rules, presets, action strength, and common setups — is in [`docs/current/configuration.md`](docs/current/configuration.md), and the CLI prints a condensed version:
|
|
283
287
|
|
|
@@ -385,3 +389,5 @@ Haechi is deliberately scoped. These are real, current limitations — listed op
|
|
|
385
389
|
1.6.0 hardens the **crypto envelope**: the local AES-256-GCM provider now enforces a per-key nonce budget — random 96-bit IVs are only safe for a bounded number of encryptions per key (NIST SP 800-38D §8.3), so the provider counts encryptions per key, persists the count to the key file, warns at 50%, and **fails closed at the limit** (rotate with `haechi init --force`); a read-only key file degrades to a per-process limit. `haechi status` surfaces the budget (`keys.nonceBudget`, used %) and `haechi/crypto` exports `readNonceBudget`. A named `gate:security` CI job runs the cross-cutting security invariants (no plaintext in audit, hard-block non-suppressible, AAD/AEAD binding, the nonce budget, privacy-profile strengthen-only) as an independently required check. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
|
|
386
390
|
|
|
387
391
|
1.7.0 completes the next crypto-envelope hardening step: new envelopes are `v:2` with `aadEncoding:"nfkc-json-v2"`, so crypto AAD uses sorted canonical JSON with NFKC-normalized string values and object keys (`canonicalizeCryptoAad`). Legacy v1 envelopes still decrypt with the old AAD canonicalization. AAD key collisions introduced by NFKC fail closed, and token-vault ciphertext now carries the token `expiresAt` into the envelope so stale ciphertext is rejected by the crypto layer as defense-in-depth. `haechi-crypto-kms@0.3.0` uses the same core helper and requires `haechi >=1.7.0`. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
|
|
392
|
+
|
|
393
|
+
1.8.0 adds the usage/accounting contract that future user-management and quota modules build on: `providers.usageRecorder`, `providers.quotaProvider`, a new `haechi/usage` export, and the additive `usage` config section. Usage is off by default; when enabled, the proxy emits one PII-safe `usage_recorded` event per completed proxied request through the injected recorder and, by default, the hash-chained audit log. Events carry bounded/hash-only fields (`pathHash`, `modelHash`, status/outcome, byte counts, duration, and the five-field PII-safe identity projection) and never raw body content, model names, bearer tokens, scopes, labels, subjects, or provider credentials. The quota provider contract is validated and exposed in 1.8 but request denial is intentionally deferred to 1.9 so enforcement can happen before upstream forwarding with full request context.
|
package/docs/README.md
CHANGED
|
@@ -18,6 +18,11 @@ English is the primary documentation language. Korean translations are maintaine
|
|
|
18
18
|
- `docs/current/release-0.5-implementation-scope.md`: 0.5 SSE/NDJSON streaming response inspection with bounded cross-frame buffer
|
|
19
19
|
- `docs/current/release-0.6-implementation-scope.md`: 0.6 bearer auth, named policy profiles, model allowlist, rate limiting
|
|
20
20
|
- `docs/current/release-0.7-implementation-scope.md`: 0.7 audit anchoring, cryptoProvider contract + reference KMS adapter, signed release artifacts
|
|
21
|
+
- `docs/current/release-0.8-implementation-scope.md`: 0.8 workspaces and first satellite packages
|
|
22
|
+
- `docs/current/release-0.9-implementation-scope.md`: 0.9 read-only dashboard and OIDC satellite scope
|
|
23
|
+
- `docs/current/release-1.0-implementation-scope.md`: 1.0 stable API and signed authProvider plugin sandbox
|
|
24
|
+
- `docs/current/release-1.1-implementation-scope.md`: 1.1 process-isolated authProvider capability enforcement
|
|
25
|
+
- `docs/current/release-1.8-implementation-scope.md`: 1.8 usageRecorder/quotaProvider contracts and PII-safe usage events
|
|
21
26
|
- `docs/current/configuration.md`: full configuration reference (every key, defaults, validation, presets, common setups)
|
|
22
27
|
- `docs/current/risk-register-release-gate.md`: release-blocking risks, security/operational risk status, npm release gates (0.3.2 baseline)
|
|
23
28
|
- `docs/current/threat-model.md`: Haechi trust boundaries, protected assets, key threats and controls
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
| `haechi` / `haechi/core` — `createHaechi().protectJson`, `createHaechi().createStreamProtector`, `collectStringEntries`, `pathToString`, `safePathToString`, `shapeOnly`, `summarize` | **FROZEN** (breaking change = major) |
|
|
29
29
|
| `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
|
|
30
30
|
| `haechi/auth` — `authProvider` 계약, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
|
|
31
|
-
| `haechi/
|
|
31
|
+
| `haechi/usage` — `usageRecorder` / `quotaProvider` 계약, `buildUsageEvent`, `recordUsageEvent`, `projectUsageIdentity`, no-op provider, quota decision 정규화 | **FROZEN** |
|
|
32
|
+
| `haechi/crypto` — `cryptoProvider` 계약, `assertCryptoProviderConformance`, `canonicalize`, `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`, `CRYPTO_AAD_ENCODING_V3`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
32
33
|
| `haechi/audit` — audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, 그리고 1.5.0 주입 가능한 store 시임 `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
|
|
33
34
|
| `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
|
|
34
35
|
| `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, 그리고 **rule/detection shape** | **FROZEN** |
|
|
@@ -82,7 +83,7 @@ audit event(`packages/core/index.mjs`의 `buildAuditEvent`가 생성하고 `pack
|
|
|
82
83
|
|
|
83
84
|
### 2.4 Config schema freeze 단위
|
|
84
85
|
|
|
85
|
-
**config key 존재 + shape**가 frozen입니다(top-level key `mode`, `target`, `proxy`, `responseProtection`, `streaming`, `limits`, `policy`, `filters`, `keys`, `audit`, `tokenVault`, `privacy`, `auth`, `mcp` 및 그 중첩 shape). **default *값*은 여전히 하드닝될 수 있습니다** — 더 안전한 default(예: 더 엄격한 `failureMode`)는 breaking change가 **아닙니다**. **알 수 없는 key는 여전히 throw합니다**(fail-closed). `normalizeConfig`는 엄격한 enumerated 검증을 수행하며, 그 fail-closed 태세가 계약의 일부입니다.
|
|
86
|
+
**config key 존재 + shape**가 frozen입니다(top-level key `mode`, `target`, `proxy`, `responseProtection`, `streaming`, `limits`, `policy`, `filters`, `keys`, `audit`, `tokenVault`, `privacy`, `auth`, `usage`, `mcp` 및 그 중첩 shape). **default *값*은 여전히 하드닝될 수 있습니다** — 더 안전한 default(예: 더 엄격한 `failureMode`)는 breaking change가 **아닙니다**. **알 수 없는 key는 여전히 throw합니다**(fail-closed). `normalizeConfig`는 엄격한 enumerated 검증을 수행하며, 그 fail-closed 태세가 계약의 일부입니다.
|
|
86
87
|
|
|
87
88
|
## 3. Graduated / 잔존 preview exports
|
|
88
89
|
|
|
@@ -110,10 +111,10 @@ audit event(`packages/core/index.mjs`의 `buildAuditEvent`가 생성하고 `pack
|
|
|
110
111
|
위성(예: `haechi-crypto-kms`, `haechi-auth-jwt`, `haechi-dashboard`, `haechi-auth-oidc`)은 core와 **독립적으로** 버저닝합니다 — 위성 릴리스가 `haechi`를 bump하지 않고, 그 반대도 마찬가지입니다.
|
|
111
112
|
|
|
112
113
|
- **pre-1.0:** 위성은 npm semver를 따르며 `0.x` **minor** bump가 breaking change를 담을 수 있습니다. `major.minor`로 핀합니다(예: `haechi-crypto-kms@~0.2`). 각자 자체 `1.0.0`까지 pre-stable입니다.
|
|
113
|
-
- **core 호환성**은 `peerDependencies` 범위로 표현합니다 — 위성은 소비자가 설치한 단일 `haechi`를 재사용하므로 crypto/identity 표면이 하나입니다. 대부분의 1.x 위성은 `"haechi": ">=0.8.0 <2.0.0"`를 유지하지만, `haechi-crypto-kms@0.
|
|
114
|
+
- **core 호환성**은 `peerDependencies` 범위로 표현합니다 — 위성은 소비자가 설치한 단일 `haechi`를 재사용하므로 crypto/identity 표면이 하나입니다. 대부분의 1.x 위성은 `"haechi": ">=0.8.0 <2.0.0"`를 유지하지만, `haechi-crypto-kms@0.4.0`은 core의 `CRYPTO_AAD_ENCODING_V3` export를 사용하므로 `"haechi": ">=1.8.0 <2.0.0"`를 요구합니다. `haechi-auth-oidc`는 추가로 `haechi-auth-jwt`(`">=0.3.0 <2.0.0"`)에 peer-depend하여 둘이 audit되는 단일 JWS/JWKS 검증 경로를 공유합니다. 위성의 `haechi` peer-dependency **상한은 반드시 core MAJOR를 추적해야 하며**(`<2.0.0`), 다음 minor 미만으로 고정해서는 안 됩니다 — core의 minor/major 호환 범프가 위성 설치를 깨뜨리지 않도록 하기 위함입니다. `release:preflight` 게이트(`scripts/check-satellite-peer-ranges.mjs`)가 이를 자동으로 강제합니다.
|
|
114
115
|
- **무거운 백엔드는 optional peer입니다.** `haechi-crypto-kms`는 SDK 백엔드(`@aws-sdk/client-kms`, 그리고 0.2.0의 `@google-cloud/kms`, `@azure/keyvault-keys`, `@azure/identity`)를 `peerDependencies` + `peerDependenciesMeta.optional`로 선언하고 lazy import하므로, 해당 경로를 쓰지 않는 소비자는 설치하지 않고 core는 zero-dependency를 유지합니다. `./vault` 백엔드는 `node:` `fetch`만 사용합니다(optional peer 없음). 위성의 배포 tarball은 항상 **runtime `dependencies` 0**을 선언합니다(CI `check-satellite-packaging`로 강제).
|
|
115
116
|
- **pre-1.0 위성 export**는 preview이며 각 위성의 자체 `1.0.0` 전에 변경될 수 있습니다.
|
|
116
|
-
- `haechi-crypto-kms` (0.8 → 0.
|
|
117
|
+
- `haechi-crypto-kms` (0.8 → 0.4.0): `createKmsCryptoProvider`, `createInMemoryKms`, `./aws`의 `createAwsKmsClient`, 0.2.0 subpath `./gcp`(`createGcpKmsClient`), `./azure`(`createAzureKmsClient`), `./vault`(`createVaultKmsClient`), 그리고 0.4.0의 core v3 crypto-AAD parity(`haechi >=1.8.0` peer floor).
|
|
117
118
|
- `haechi-auth-jwt` (0.2.0): `createJwtAuthProvider`(0.8, behavior-preserving)와 추가된 `createJwtVerifier`(재사용 가능한 JWS 검증 primitive), `isBlockedAddress`(SSRF 범위 술어, `haechi-auth-oidc`가 재사용).
|
|
118
119
|
- `haechi-dashboard` (0.1.0, 신규): `createDashboardServer`, `normalizeDashboardConfig`.
|
|
119
120
|
- `haechi-auth-oidc` (0.1.0, 신규): `createOidcSessionBroker`, `normalizeOidcConfig`.
|
|
@@ -28,7 +28,8 @@ Every `package.json` `exports` subpath and the CLI is classed. There is no silen
|
|
|
28
28
|
| `haechi` / `haechi/core` — `createHaechi().protectJson`, `createHaechi().createStreamProtector`, `collectStringEntries`, `pathToString`, `safePathToString`, `shapeOnly`, `summarize` | **FROZEN** (breaking change = major) |
|
|
29
29
|
| `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
|
|
30
30
|
| `haechi/auth` — the `authProvider` contract, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, the token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
|
|
31
|
-
| `haechi/
|
|
31
|
+
| `haechi/usage` — `usageRecorder` / `quotaProvider` contracts, `buildUsageEvent`, `recordUsageEvent`, `projectUsageIdentity`, no-op providers, quota decision normalization | **FROZEN** |
|
|
32
|
+
| `haechi/crypto` — the `cryptoProvider` contract, `assertCryptoProviderConformance`, `canonicalize`, `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`, `CRYPTO_AAD_ENCODING_V3`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
32
33
|
| `haechi/audit` — the audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, plus the 1.5.0 injectable store seam `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
|
|
33
34
|
| `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
|
|
34
35
|
| `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, and the **rule/detection shape** | **FROZEN** |
|
|
@@ -82,7 +83,7 @@ Rules:
|
|
|
82
83
|
|
|
83
84
|
### 2.4 Config schema freeze unit
|
|
84
85
|
|
|
85
|
-
The **config key presence + shape** is frozen (the top-level keys `mode`, `target`, `proxy`, `responseProtection`, `streaming`, `limits`, `policy`, `filters`, `keys`, `audit`, `tokenVault`, `privacy`, `auth`, `mcp`, and their nested shapes). **Default *values* may still be hardened** — a safer default (e.g. a stricter `failureMode`) is **not** a breaking change. **Unknown keys still throw** (fail-closed): `normalizeConfig` performs strict, enumerated validation, and that fail-closed posture is part of the contract.
|
|
86
|
+
The **config key presence + shape** is frozen (the top-level keys `mode`, `target`, `proxy`, `responseProtection`, `streaming`, `limits`, `policy`, `filters`, `keys`, `audit`, `tokenVault`, `privacy`, `auth`, `usage`, `mcp`, and their nested shapes). **Default *values* may still be hardened** — a safer default (e.g. a stricter `failureMode`) is **not** a breaking change. **Unknown keys still throw** (fail-closed): `normalizeConfig` performs strict, enumerated validation, and that fail-closed posture is part of the contract.
|
|
86
87
|
|
|
87
88
|
## 3. Graduated / remaining-preview exports
|
|
88
89
|
|
|
@@ -110,10 +111,10 @@ A migration note is added to `docs/current/release-*.md` or the README whenever
|
|
|
110
111
|
Satellites (e.g. `haechi-crypto-kms`, `haechi-auth-jwt`, `haechi-dashboard`, `haechi-auth-oidc`) version **independently** of core — a satellite release never bumps `haechi`, and vice versa.
|
|
111
112
|
|
|
112
113
|
- **Pre-1.0:** satellites follow npm semver where a `0.x` **minor** bump may carry breaking changes; pin `major.minor` (e.g. `haechi-crypto-kms@~0.2`). Each is pre-stable until its own `1.0.0`.
|
|
113
|
-
- **Core compatibility** is expressed as a `peerDependencies` range — a satellite reuses the consumer's single installed `haechi`, so there is one crypto/identity surface. Most 1.x satellites keep `"haechi": ">=0.8.0 <2.0.0"`, but `haechi-crypto-kms@0.
|
|
114
|
+
- **Core compatibility** is expressed as a `peerDependencies` range — a satellite reuses the consumer's single installed `haechi`, so there is one crypto/identity surface. Most 1.x satellites keep `"haechi": ">=0.8.0 <2.0.0"`, but `haechi-crypto-kms@0.4.0` requires `"haechi": ">=1.8.0 <2.0.0"` because it imports core's `CRYPTO_AAD_ENCODING_V3` export. `haechi-auth-oidc` additionally peer-depends on `haechi-auth-jwt` (`">=0.3.0 <2.0.0"`) so the two share one audited JWS/JWKS verification path. The satellite `haechi` peer-dependency **upper bound must track the core MAJOR** (`<2.0.0`), never be pinned below the next minor, so a core minor- or major-compatible bump never breaks satellite installs; the `release:preflight` gate (`scripts/check-satellite-peer-ranges.mjs`) enforces this automatically.
|
|
114
115
|
- **Heavy backends are optional peers.** `haechi-crypto-kms` declares its SDK backends (`@aws-sdk/client-kms`, and in 0.2.0 `@google-cloud/kms`, `@azure/keyvault-keys`, `@azure/identity`) under `peerDependencies` + `peerDependenciesMeta.optional` and imports them lazily, so consumers who do not use a given path never install it and core stays zero-dependency. The `./vault` backend uses `node:` `fetch` only (no optional peer). A satellite's published tarball always declares **zero runtime `dependencies`** (CI-gated by `check-satellite-packaging`).
|
|
115
116
|
- **Pre-1.0 satellite exports** are preview and may change before each satellite's own `1.0.0`:
|
|
116
|
-
- `haechi-crypto-kms` (0.8 → 0.
|
|
117
|
+
- `haechi-crypto-kms` (0.8 → 0.4.0): `createKmsCryptoProvider`, `createInMemoryKms`, the `./aws` `createAwsKmsClient`, the 0.2.0 subpaths `./gcp` (`createGcpKmsClient`), `./azure` (`createAzureKmsClient`), `./vault` (`createVaultKmsClient`), and 0.4.0 v3 crypto-AAD parity with core (`haechi >=1.8.0` peer floor).
|
|
117
118
|
- `haechi-auth-jwt` (0.2.0): `createJwtAuthProvider` (0.8, behavior-preserving) plus the additive `createJwtVerifier` (reusable JWS verifier primitive) and `isBlockedAddress` (SSRF range predicate, reused by `haechi-auth-oidc`).
|
|
118
119
|
- `haechi-dashboard` (0.1.0, new): `createDashboardServer`, `normalizeDashboardConfig`.
|
|
119
120
|
- `haechi-auth-oidc` (0.1.0, new): `createOidcSessionBroker`, `normalizeOidcConfig`.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| `configVersion` | 코어 라인 | 노트 |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| `1` | 1.0 – 1.
|
|
24
|
+
| `1` | 1.0 – 1.8.x | 최초 스탬프. 모든 키는 1.0 frozen 설정 표면(`api-stability.md` §2.4)에 대해 additive입니다. 1.1.x의 additive 키(`logging`, `metrics`, WS4-B의 `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, 그리고 `configVersion` 자체)와 1.2.0 신뢰성 강화 키(`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`)는 모두 이전 동작을 기본값으로 합니다. 1.3.0의 추가는 새 키가 아니라 새 *값*입니다 — `target.type`의 `anthropic`/`gemini`, 추가 탐지 타입, `asia-pdpa`/`jp-appi` `privacy.profile` 값입니다. 1.4.0 plugin-signing CLI, 1.5.0 store 시임, 1.6.0 nonce-budget 가시성, 1.7.0 v2 crypto-AAD/freshness 변경은 모두 CLI/API/envelope 동작입니다. 1.8.0은 기본값이 꺼진 additive `usage` 섹션을 추가해 usageRecorder/quotaProvider 계약을 예약합니다. 따라서 설정 스키마 스탬프는 `1` 유지이며 마이그레이션은 필요 없습니다. |
|
|
25
25
|
|
|
26
26
|
## 업그레이드
|
|
27
27
|
|
|
@@ -34,7 +34,7 @@ the "policies only get stronger / fail closed" invariant intact.
|
|
|
34
34
|
|
|
35
35
|
| `configVersion` | Core line | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `1` | 1.0 – 1.
|
|
37
|
+
| `1` | 1.0 – 1.8.x | Initial stamp. All keys are additive over the 1.0 frozen config surface (`api-stability.md` §2.4). The 1.1.x additive keys (`logging`, `metrics`, the WS4-B `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, `configVersion` itself) and the 1.2.0 Reliability-Hardening keys (`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`) all default to prior behavior. The 1.3.0 additions are new *values*, not new keys — `target.type` `anthropic`/`gemini`, additional detection types, and the `asia-pdpa`/`jp-appi` `privacy.profile` values. The 1.4.0 plugin-signing CLI (`plugin-keygen`/`plugin-sign`/`plugin-verify`) is **CLI surface, not config**. The 1.5.0 store seams (`createAuditSink`/`createTokenVault` + the file-store defaults) are an **injected-provider** surface. The 1.6.0 nonce-budget visibility and 1.7.0 v2 crypto-AAD/freshness changes are crypto envelope/API behavior. 1.8.0 adds the additive, default-off `usage` section for usageRecorder/quotaProvider contracts. So the config schema stamp remains `1`; no migration needed. |
|
|
38
38
|
|
|
39
39
|
## Upgrading
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 설정 레퍼런스
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.8.x 추적)
|
|
4
4
|
|
|
5
5
|
`haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있습니다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽습니다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**됩니다. 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킵니다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력합니다.
|
|
6
6
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"privacy": { "profile": null },
|
|
24
24
|
"logging": { "format": "text" },
|
|
25
25
|
"metrics": { "enabled": true },
|
|
26
|
+
"usage": { "enabled": false, "audit": true, "recorder": "none", "quota": { "provider": "none", "failClosed": true } },
|
|
26
27
|
"mcp": { "allowedMethods": ["initialize", "tools/call", "resources/read", "prompts/get"], "protectParams": true, "protectResults": true, "requireJsonRpc": true }
|
|
27
28
|
}
|
|
28
29
|
```
|
|
@@ -167,6 +168,20 @@ npm run scan:detection # CI 회귀 게이트: 어떤 type이라도 baseline
|
|
|
167
168
|
|
|
168
169
|
메트릭 수집기는 **주입 가능한 협력 객체**이기도 합니다(`createRuntime(config, { metrics })`). 계약과 no-PII 보장은 [운영 엔드포인트](#운영-엔드포인트)를 참고하십시오.
|
|
169
170
|
|
|
171
|
+
## `usage`
|
|
172
|
+
|
|
173
|
+
1.8은 코어를 사용자 DB나 IdP로 만들지 않고, 후속 관리/쿼터 기능이 사용할 usage/accounting 계약만 추가합니다. 기본값은 **비활성화**이므로 기존 1.7 설정은 동일하게 동작합니다. 활성화하면 proxy가 완료된 프록시 요청마다 PII-safe `usage_recorded` 이벤트를 `usageRecorder`로 내보내고, `usage.audit`가 `true`이면 같은 이벤트를 hash-chained audit log에도 추가합니다.
|
|
174
|
+
|
|
175
|
+
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `usage.enabled` | boolean | `false` | 마스터 스위치입니다. `false`이면 usage 이벤트를 만들지 않습니다. |
|
|
178
|
+
| `usage.audit` | boolean | `true` | usage가 활성화되었을 때 `usage_recorded` 이벤트를 auditSink에도 기록합니다. 이벤트에는 원문 요청/응답 본문, 토큰, subject, label/scope, path, model name이 들어가지 않습니다. |
|
|
179
|
+
| `usage.recorder` | `none` \| `external` | `none` | `none`은 no-op recorder를 사용합니다. `external`은 `createRuntime(config, { usageRecorder })` 주입이 필요합니다. 기본 설정에서도 `providers.usageRecorder`를 직접 주입할 수 있습니다. |
|
|
180
|
+
| `usage.quota.provider` | `none` \| `external` | `none` | 예약된 quota contract입니다. `external`은 `createRuntime(config, { quotaProvider })` 주입이 필요합니다. 1.8은 provider를 검증하고 노출하지만 proxy에서 요청을 차단하지는 않습니다. upstream 전 차단은 1.9 범위입니다. |
|
|
181
|
+
| `usage.quota.failClosed` | boolean | `true` | quota-critical 배포를 위한 계약 플래그입니다. 1.9에서 provider 오류 처리를 추가 config shape 변경 없이 적용할 수 있도록 지금 검증합니다. |
|
|
182
|
+
|
|
183
|
+
`usageRecorder`는 함수이거나 `{ record(event) }` 객체일 수 있습니다. 이벤트는 `haechi/usage`의 `buildUsageEvent()` / `recordUsageEvent()`와 같은 모듈 계약을 따르며, identity는 `id`, `type`, `subjectHash`, `issuerHash`, `provider` 다섯 필드로만 투영됩니다. URL path는 `pathHash`, model은 `modelHash`와 `modelPresent`로만 기록됩니다.
|
|
184
|
+
|
|
170
185
|
## 운영 엔드포인트
|
|
171
186
|
|
|
172
187
|
프록시는 예약된 `/__haechi/*` 접두어 아래에 네 개의 인증 없는 엔드포인트를 제공하며, 이들은 인증과 본문 읽기 **이전**에 처리되고 업스트림으로 프록시되지 않습니다.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Configuration Reference
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.8.x)
|
|
4
4
|
|
|
5
5
|
`haechi init` writes `haechi.config.json`; a non-secret template is at `haechi.config.example.json`. Every command reads it with `--config <path>` (default `haechi.config.json`). Configuration is **validated fail-closed**: unknown providers, out-of-range numbers, and malformed values throw at load time rather than degrading silently. `haechi config` prints this reference; `haechi status` prints the *effective* state of a given config.
|
|
6
6
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"privacy": { "profile": null },
|
|
24
24
|
"logging": { "format": "text" },
|
|
25
25
|
"metrics": { "enabled": true },
|
|
26
|
+
"usage": { "enabled": false, "audit": true, "recorder": "none", "quota": { "provider": "none", "failClosed": true } },
|
|
26
27
|
"mcp": { "allowedMethods": ["initialize", "tools/call", "resources/read", "prompts/get"], "protectParams": true, "protectResults": true, "requireJsonRpc": true }
|
|
27
28
|
}
|
|
28
29
|
```
|
|
@@ -167,6 +168,54 @@ In `json` mode the proxy's internal-error log is a single line `{ "level": "erro
|
|
|
167
168
|
|
|
168
169
|
The metrics collector is also an **injectable collaborator** (`createRuntime(config, { metrics })`); see [Operability endpoints](#operability-endpoints) for the contract and the no-PII guarantee.
|
|
169
170
|
|
|
171
|
+
## `usage`
|
|
172
|
+
|
|
173
|
+
1.8 adds the core usage/accounting contract without turning core into a user database or an IdP. It is **off by default**, so existing 1.7 configs behave the same. When enabled, the proxy emits one PII-safe `usage_recorded` event per completed proxied request through an injected `usageRecorder`; optionally the same event is also appended to the hash-chained audit log.
|
|
174
|
+
|
|
175
|
+
| Key | Type / values | Default | Notes |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `usage.enabled` | boolean | `false` | Master switch. When `false`, no usage events are emitted. |
|
|
178
|
+
| `usage.audit` | boolean | `true` | When usage is enabled, also write `usage_recorded` events to `auditSink`. The event contains no raw request/response body, token, subject, labels/scopes, path, or model name. |
|
|
179
|
+
| `usage.recorder` | `none` \| `external` | `none` | `none` uses a no-op recorder. `external` requires `createRuntime(config, { usageRecorder })`. Supplying `providers.usageRecorder` also works with the default config. |
|
|
180
|
+
| `usage.quota.provider` | `none` \| `external` | `none` | Reserved quota contract. `external` requires `createRuntime(config, { quotaProvider })`; 1.8 validates and exposes the provider but does not enforce denials in the proxy yet. Enforcement before upstream forwarding is the 1.9 scope. |
|
|
181
|
+
| `usage.quota.failClosed` | boolean | `true` | Contract flag for quota-critical deployments. It is validated now so 1.9 can enforce provider-error behavior without another config shape change. |
|
|
182
|
+
|
|
183
|
+
### `providers.usageRecorder` injection seam
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const runtime = createRuntime(config, {
|
|
187
|
+
usageRecorder: {
|
|
188
|
+
async record(event) {
|
|
189
|
+
// event is already PII-safe and contains bounded/hash-only fields.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
An injected recorder may be a function or an object with `record(event)`. The event shape is exported from `haechi/usage` via `buildUsageEvent()` / `recordUsageEvent()` and includes:
|
|
196
|
+
|
|
197
|
+
- PII-safe identity projection: `id`, `type`, `subjectHash`, `issuerHash`, `provider` only.
|
|
198
|
+
- Bounded route/method/status/outcome fields.
|
|
199
|
+
- `pathHash` instead of raw URL path.
|
|
200
|
+
- `modelHash` + `modelPresent`, never the raw model string.
|
|
201
|
+
- request/response byte counts and duration, never body content.
|
|
202
|
+
|
|
203
|
+
Usage recorder failures are swallowed after the client response is completed and counted in `haechi_usage_record_failed_total`; they do not place raw error messages in audit.
|
|
204
|
+
|
|
205
|
+
### `providers.quotaProvider` reservation
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const runtime = createRuntime(config, {
|
|
209
|
+
quotaProvider: {
|
|
210
|
+
async check(context) {
|
|
211
|
+
return { allowed: true, reason: "within_budget" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The 1.8 quota provider contract accepts `check(context) -> boolean | { allowed, reason?, retryAfterSeconds? }`; `normalizeQuotaDecision()` enforces a bounded result shape. Proxy denial is intentionally deferred to 1.9 so the enforcement point can account for request body/model information before the upstream call.
|
|
218
|
+
|
|
170
219
|
## Operability endpoints
|
|
171
220
|
|
|
172
221
|
The proxy serves four unauthenticated endpoints under the reserved `/__haechi/*` prefix, checked **before** auth and body-read. They never proxy upstream.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Haechi 1.8.0 구현 범위
|
|
2
|
+
|
|
3
|
+
상태: 구현 초안
|
|
4
|
+
테마: 후속 management 및 quota 모듈을 위한 usage/accounting 계약
|
|
5
|
+
|
|
6
|
+
## 1. 목표
|
|
7
|
+
|
|
8
|
+
1.8.0은 Haechi core를 사용자 데이터베이스, IdP, dashboard write plane으로 만들지 않으면서 사용자/사용량 관리 기능이 안정적으로 기대할 수 있는 최소 core 표면을 추가합니다.
|
|
9
|
+
|
|
10
|
+
이번 릴리스는 다음을 도입합니다.
|
|
11
|
+
|
|
12
|
+
- `providers.usageRecorder`
|
|
13
|
+
- `providers.quotaProvider`
|
|
14
|
+
- 새 public `haechi/usage` subpath
|
|
15
|
+
- 기본 비활성 top-level `usage` config section
|
|
16
|
+
- 완료된 proxy 요청에 대한 PII-safe `usage_recorded` 이벤트
|
|
17
|
+
|
|
18
|
+
설계는 의도적으로 contract-first입니다. 사용량 기록은 지금 사용할 수 있지만, quota denial은 1.9로 미룹니다. quota enforcement는 전체 요청 context를 확보한 뒤 upstream forwarding 전에 실행되어야 하기 때문입니다.
|
|
19
|
+
|
|
20
|
+
## 2. 제외 범위
|
|
21
|
+
|
|
22
|
+
- core 안의 password/MFA/account-recovery lifecycle 없음
|
|
23
|
+
- core 안의 local user registry 없음
|
|
24
|
+
- dashboard write/admin route 없음
|
|
25
|
+
- Prometheus label에 per-user 값 없음
|
|
26
|
+
- audit 또는 metric에 raw email/name/JWT subject/issuer/scopes/labels/model name 없음
|
|
27
|
+
- 1.8에서 quota denial 없음. provider는 검증되고 노출되지만 proxy enforcement는 1.9 범위입니다.
|
|
28
|
+
|
|
29
|
+
## 3. Public API
|
|
30
|
+
|
|
31
|
+
새 export:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"exports": {
|
|
36
|
+
"./usage": "./packages/usage/index.mjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Exported names:
|
|
42
|
+
|
|
43
|
+
- `USAGE_EVENT_SCHEMA_VERSION`
|
|
44
|
+
- `buildUsageEvent`
|
|
45
|
+
- `recordUsageEvent`
|
|
46
|
+
- `projectUsageIdentity`
|
|
47
|
+
- `createNoopUsageRecorder`
|
|
48
|
+
- `createNoopQuotaProvider`
|
|
49
|
+
- `coerceUsageRecorder`
|
|
50
|
+
- `coerceQuotaProvider`
|
|
51
|
+
- `normalizeQuotaDecision`
|
|
52
|
+
|
|
53
|
+
API는 1.x semver 계약 아래 additive 변경입니다.
|
|
54
|
+
|
|
55
|
+
## 4. Config
|
|
56
|
+
|
|
57
|
+
기본값:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"usage": {
|
|
62
|
+
"enabled": false,
|
|
63
|
+
"audit": true,
|
|
64
|
+
"recorder": "none",
|
|
65
|
+
"quota": {
|
|
66
|
+
"provider": "none",
|
|
67
|
+
"failClosed": true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
검증:
|
|
74
|
+
|
|
75
|
+
- `usage.enabled`: boolean
|
|
76
|
+
- `usage.audit`: boolean
|
|
77
|
+
- `usage.recorder`: `none | external`
|
|
78
|
+
- `usage.quota.provider`: `none | external`
|
|
79
|
+
- `usage.quota.failClosed`: boolean
|
|
80
|
+
|
|
81
|
+
`usage.recorder: "external"`은 `createRuntime(config, { usageRecorder })`가 필요합니다.
|
|
82
|
+
`usage.quota.provider: "external"`은 `createRuntime(config, { quotaProvider })`가 필요합니다.
|
|
83
|
+
|
|
84
|
+
`configVersion`은 `1`을 유지합니다. 이 section은 additive이며 기본 비활성입니다.
|
|
85
|
+
|
|
86
|
+
## 5. Runtime Contracts
|
|
87
|
+
|
|
88
|
+
### `usageRecorder`
|
|
89
|
+
|
|
90
|
+
Shape:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
{
|
|
94
|
+
async record(event) {}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
bare function도 허용하며 `{ record }`로 감쌉니다.
|
|
99
|
+
|
|
100
|
+
`usage.enabled`가 `true`이면 proxy는 observability route가 아닌 요청이 완료된 뒤 이벤트를 하나 내보냅니다. 이벤트는 recorder로 전달되고, `usage.audit`가 `true`이면 기존 hash-chained audit sink에도 기록됩니다.
|
|
101
|
+
|
|
102
|
+
Recorder 오류는 client response가 완료된 뒤 catch되며 `haechi_usage_record_failed_total`만 증가시킵니다. raw error message를 audit에 남기지 않고, PII를 포함할 수 있는 metric label도 만들지 않습니다.
|
|
103
|
+
|
|
104
|
+
### `quotaProvider`
|
|
105
|
+
|
|
106
|
+
Shape:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
{
|
|
110
|
+
async check(context) {
|
|
111
|
+
return { allowed: true, reason: "within_budget" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
허용 return value:
|
|
117
|
+
|
|
118
|
+
- `true`
|
|
119
|
+
- `false`
|
|
120
|
+
- `{ allowed: boolean, reason?: boundedString, retryAfterSeconds?: positiveInt }`
|
|
121
|
+
|
|
122
|
+
`normalizeQuotaDecision()`은 bounded result shape을 검증합니다. 1.8은 proxy enforcement path에서 provider를 호출하지 않습니다. 요청 body/model accounting이 확정되기 전에 반쪽짜리 quota 기능을 만들지 않기 위한 결정입니다.
|
|
123
|
+
|
|
124
|
+
## 6. Usage Event Shape
|
|
125
|
+
|
|
126
|
+
core event에는 bounded 또는 hash-only 필드만 포함됩니다.
|
|
127
|
+
|
|
128
|
+
- `schemaVersion`
|
|
129
|
+
- `eventType: "usage_recorded"`
|
|
130
|
+
- `correlationId`
|
|
131
|
+
- `timestamp`
|
|
132
|
+
- `protocol`
|
|
133
|
+
- `operation`
|
|
134
|
+
- `identity`
|
|
135
|
+
- `profile`
|
|
136
|
+
- `decision: "usage_recorded"`
|
|
137
|
+
- `reason: "request_completed"`
|
|
138
|
+
- `routeId`
|
|
139
|
+
- `method`
|
|
140
|
+
- `pathHash`
|
|
141
|
+
- `outcome`
|
|
142
|
+
- `upstream`
|
|
143
|
+
- `request`
|
|
144
|
+
- `response`
|
|
145
|
+
- `durationMs`
|
|
146
|
+
- `summary`
|
|
147
|
+
|
|
148
|
+
Identity projection은 정확히 다음 다섯 필드입니다.
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"id": "bounded id",
|
|
153
|
+
"type": "bounded type",
|
|
154
|
+
"subjectHash": "hash",
|
|
155
|
+
"issuerHash": "hash",
|
|
156
|
+
"provider": "bounded provider"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
이벤트에는 다음이 들어가지 않습니다.
|
|
161
|
+
|
|
162
|
+
- raw URL path
|
|
163
|
+
- raw request body
|
|
164
|
+
- raw response body
|
|
165
|
+
- raw model name
|
|
166
|
+
- bearer token
|
|
167
|
+
- JWT subject 또는 issuer
|
|
168
|
+
- scopes
|
|
169
|
+
- labels
|
|
170
|
+
- provider credentials
|
|
171
|
+
|
|
172
|
+
Model 정보는 `request.modelPresent`와 `request.modelHash`로만 표현됩니다.
|
|
173
|
+
|
|
174
|
+
## 7. 보안 속성
|
|
175
|
+
|
|
176
|
+
1. **usage audit에 raw PII 없음.** Usage event는 audit sink로 들어가기 전에 projected field로 생성됩니다. sink의 `FORBIDDEN_KEYS`도 defense in depth로 `scopes`와 `labels`를 제거합니다.
|
|
177
|
+
2. **high-cardinality metric 없음.** Usage recorder failure는 identity label 없이 고정 counter 하나만 증가시킵니다.
|
|
178
|
+
3. **core IdP drift 없음.** Core는 identity projection과 accounting contract만 소유합니다. 사용자 label, group, quota, display name은 후속 satellite 책임입니다.
|
|
179
|
+
4. **quota fail-closed contract 예약.** `usage.quota.failClosed`는 지금 검증되므로 1.9에서 config shape 변경 없이 provider-error behavior를 적용할 수 있습니다.
|
|
180
|
+
5. **기본 비활성 호환성.** 기존 config는 `usage.enabled`를 명시하지 않는 한 usage event를 만들지 않습니다.
|
|
181
|
+
|
|
182
|
+
## 8. 테스트
|
|
183
|
+
|
|
184
|
+
추가:
|
|
185
|
+
|
|
186
|
+
- `tests/usage-contract.test.mjs`
|
|
187
|
+
|
|
188
|
+
Coverage:
|
|
189
|
+
|
|
190
|
+
- default config 및 fail-closed provider validation
|
|
191
|
+
- no-op usage/quota provider contract
|
|
192
|
+
- quota decision normalization
|
|
193
|
+
- PII-safe identity projection
|
|
194
|
+
- end-to-end proxy usage recording
|
|
195
|
+
- `usage_recorded` event가 포함된 hash-chained audit verification
|
|
196
|
+
- usage event와 audit log에 raw token, email, model name이 없는지 검증
|
|
197
|
+
|
|
198
|
+
기존 guard 갱신:
|
|
199
|
+
|
|
200
|
+
- `tests/api-contract.test.mjs`가 additive `haechi/usage` export와 additive top-level `usage` config key를 포함합니다.
|