haechi 0.7.0 → 0.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 CHANGED
@@ -167,7 +167,7 @@ haechi auth revoke <id>
167
167
  - **Rate limit**: identity별 분당 요청 수 → `429` (인메모리, 프로세스별).
168
168
  - Audit 이벤트는 **PII-safe** `identity`(keyed-HMAC subject/issuer, 원시 값 아님)와 resolve된 `profile`을 포함하며, `auth_denied` / `model_not_allowed` / `rate_limited` 결정에는 credentials가 포함되지 않는다. `/__haechi/health`는 인증 없이 접근 가능하다.
169
169
 
170
- OIDC/JWT provider와 KMS 기반 key custody는 0.7+ 위성 패키지이다.
170
+ JWT/JWKS 인증과 KMS 기반 key custody는 `haechi-*` 위성 패키지로 제공된다(0.8): [`haechi-auth-jwt`](satellites/auth-jwt/)(헤드리스 JWKS bearer 검증)와 [`haechi-crypto-kms`](satellites/crypto-kms/)(실제 AWS KMS 클라이언트 기반 envelope 암호화). 둘 다 기본 `node:` 전용이며 core를 zero-dependency로 유지한다; 대화형 OIDC와 대시보드는 0.9.
171
171
 
172
172
  ## 설정
173
173
 
@@ -244,7 +244,7 @@ Haechi는 로컬 정책 부트스트래핑을 위한 기본 지역별 Privacy Pr
244
244
  - Privacy profile은 명시적으로 더 엄격한 사용자 action을 강화할 수는 있지만 약화할 수는 없다.
245
245
  - 탐지는 문자열 값, JSON 숫자(예: 카드 번호), 객체 키 이름을 검사한다. Base64/URL 인코딩된 값과 URL 쿼리 스트링은 검사되지 않는다.
246
246
  - Audit tail truncation: `audit.anchor.mode: file`을 설정하면(추가 전용/별도 미디어에서) `haechi audit-verify --anchor`가 마지막 anchor 이후 꼬리 레코드 삭제를 탐지한다. 동일한 쓰기 가능 파일시스템에서는 공격자가 두 파일을 함께 잘라낼 수 있다.
247
- - Key custody: `keys.provider: external`은 주입된 `cryptoProvider`를 허용한다; `assertCryptoProviderConformance`로 adapter를 검증한다. envelope 암호화 KMS adapter는 `examples/crypto-kms-reference/`를 참고한다.
247
+ - Key custody: `keys.provider: external`은 주입된 `cryptoProvider`를 허용한다; `assertCryptoProviderConformance`로 adapter를 검증한다. envelope 암호화 KMS adapter는 `haechi-crypto-kms` satellite(`satellites/crypto-kms/`)가 제공한다.
248
248
  - Release integrity: 배포된 tarball에는 npm provenance attestation이 포함되며, GitHub release asset에는 sigstore attestation과 `SHA256SUMS`가 추가된다(`gh attestation verify`와 `node scripts/release-checksums.mjs --check`로 검증한다).
249
249
  - 이 패키지는 개발자 프리뷰이다. 인터넷에 노출된 운영 LLM 게이트웨이로 사용하지 않는다.
250
250
 
@@ -267,3 +267,5 @@ Haechi는 로컬 정책 부트스트래핑을 위한 기본 지역별 Privacy Pr
267
267
  0.6.0은 인증과 클라이언트별 통제를 추가한다: 해시 기반 token 저장소와 `haechi auth` CLI를 갖춘 내장 bearer auth, identity scope/label로 바인딩되는 named policy profile, model allowlisting, 그리고 identity별 rate limiting — audit 로그에는 PII-safe identity가 기록된다. `docs/current/release-0.6-implementation-scope.md` 참고.
268
268
 
269
269
  0.7.0은 운영 강화(ops-hardening) 릴리스이다: 꼬리 절단을 탐지하는 audit head-hash anchoring(`audit.anchor`), `assertCryptoProviderConformance`와 reference KMS adapter를 포함한 강화된 외부 `cryptoProvider` 계약, 그리고 서명/체크섬된 GitHub release artifact. `docs/current/release-0.7-implementation-scope.md` 참고.
270
+
271
+ 0.8.0은 `haechi-*` 에코시스템을 세운다: npm workspaces 모노레포(core는 unscoped `haechi` 유지, zero runtime dependency, 패킹 매니페스트 CI 게이트로 강제) + 첫 두 위성 — [`haechi-crypto-kms`](satellites/crypto-kms/)(실제 AWS KMS 클라이언트 기반 envelope 암호화; AWS SDK는 optional peer)와 [`haechi-auth-jwt`](satellites/auth-jwt/)(헤드리스 JWKS bearer 검증, `node:` 전용). 각각 자체 provenance + sigstore attest 워크플로로 독립 발행한다. `docs/current/release-0.8-implementation-scope.md` 참고.
package/README.md CHANGED
@@ -167,7 +167,7 @@ haechi auth revoke <id>
167
167
  - **Rate limit**: per-identity requests-per-minute → `429` (in-memory, per-process).
168
168
  - Audit events carry the **PII-safe** `identity` (keyed-HMAC subject/issuer, never raw values) and the resolved `profile`; `auth_denied` / `model_not_allowed` / `rate_limited` decisions never include credentials. `/__haechi/health` stays unauthenticated.
169
169
 
170
- OIDC/JWT providers and KMS-backed key custody are 0.7+ satellite packages.
170
+ JWT/JWKS auth and KMS-backed key custody ship as `haechi-*` satellite packages (0.8): [`haechi-auth-jwt`](satellites/auth-jwt/) (headless JWKS bearer verification) and [`haechi-crypto-kms`](satellites/crypto-kms/) (envelope encryption with a real AWS KMS client). Both are `node:`-only by default and keep core zero-dependency; interactive OIDC and a dashboard are 0.9.
171
171
 
172
172
  ## Configuration
173
173
 
@@ -244,7 +244,7 @@ Set `privacy.profile` in `haechi.config.json` to apply the profile's default act
244
244
  - Privacy profiles can strengthen but never weaken an explicitly stricter user action.
245
245
  - Detection scans string values, JSON numbers (e.g. card numbers), and object key names. Base64/URL-encoded values and URL query strings are NOT inspected.
246
246
  - Audit tail truncation: set `audit.anchor.mode: file` (on append-only/separate media) so `haechi audit-verify --anchor` detects deletion of trailing records back to the last anchor. On the same writable filesystem an attacker can truncate both files together.
247
- - Key custody: `keys.provider: external` accepts an injected `cryptoProvider`; validate adapters with `assertCryptoProviderConformance`. See `examples/crypto-kms-reference/` for an envelope-encryption KMS adapter.
247
+ - Key custody: `keys.provider: external` accepts an injected `cryptoProvider`; validate adapters with `assertCryptoProviderConformance`. The `haechi-crypto-kms` satellite (`satellites/crypto-kms/`) provides an envelope-encryption KMS adapter.
248
248
  - Release integrity: published tarballs carry an npm provenance attestation; GitHub release assets add a sigstore attestation and `SHA256SUMS` (verify with `gh attestation verify` and `node scripts/release-checksums.mjs --check`).
249
249
  - The package is a developer preview. Do not expose it as an internet-facing production LLM gateway.
250
250
 
@@ -267,3 +267,5 @@ Set `privacy.profile` in `haechi.config.json` to apply the profile's default act
267
267
  0.6.0 adds authentication and per-client controls: built-in bearer auth with a hashed token store and `haechi auth` CLI, named policy profiles bound by identity scope/label, model allowlisting, and per-identity rate limiting — with PII-safe identity in the audit log. See `docs/current/release-0.6-implementation-scope.md`.
268
268
 
269
269
  0.7.0 is operational hardening: audit head-hash anchoring (`audit.anchor`) that detects tail truncation, a hardened external `cryptoProvider` contract with `assertCryptoProviderConformance` and a reference KMS adapter, and signed/checksummed GitHub release artifacts. See `docs/current/release-0.7-implementation-scope.md`.
270
+
271
+ 0.8.0 stands up the `haechi-*` ecosystem: an npm workspaces monorepo (core stays the unscoped `haechi`, zero runtime dependency, gated by a packed-manifest CI check) plus the first two satellites — [`haechi-crypto-kms`](satellites/crypto-kms/) (envelope encryption with a real AWS KMS client; the AWS SDK is an optional peer) and [`haechi-auth-jwt`](satellites/auth-jwt/) (headless JWKS bearer verification, `node:`-only). Each publishes independently with its own provenance + sigstore-attested workflow. See `docs/current/release-0.8-implementation-scope.md`.
@@ -42,7 +42,7 @@
42
42
  - `identity` audit 필드와 `authProvider` 계약 (0.4 예약, 0.6 구현 — 그 전까지 형태 변경 가능)
43
43
  - `status` / `audit-verify` CLI 출력 형태
44
44
  - `haechi/stream-filter` (`inspectResponseStream`, path helpers) 및 `createStreamProtector` (스트리밍 검사 내부 구현)
45
- - `haechi/auth` (`createBearerAuthProvider`, token store, `buildIdentity`) 및 `authProvider` 계약
45
+ - `haechi/auth` (`createBearerAuthProvider`, token store, `buildIdentity`, `buildExternalIdentity`) 및 `authProvider` 계약
46
46
  - `policy.profiles`/`policy.profileBinding`/`modelAllowlist`/`rate` 및 `identity`/`profile` audit 필드
47
47
  - `assertCryptoProviderConformance` 및 강화된 cryptoProvider 계약 (envelope base shape + provider 확장)
48
48
  - `audit.anchor` 설정 및 `verifyAuditChain(path, { anchorPath })`
@@ -58,3 +58,12 @@
58
58
  - audit event 필드 변경
59
59
  - token format 변경
60
60
  - plugin manifest schema 변경
61
+
62
+ ## 5. Satellite 패키지 (`haechi-*`)
63
+
64
+ 위성(예: `haechi-crypto-kms`, `haechi-auth-jwt`)은 core와 **독립적으로** 버저닝한다 — 위성 릴리스가 `haechi`를 bump하지 않고, 그 반대도 마찬가지다.
65
+
66
+ - **pre-1.0:** 위성은 npm semver를 따르며 `0.x` **minor** bump가 breaking change를 담을 수 있다; `major.minor`로 핀한다(예: `haechi-crypto-kms@~0.1`). 각자 자체 `1.0.0`까지 pre-stable.
67
+ - **core 호환성**은 `peerDependencies` 범위(`"haechi": ">=0.8.0 <1.0.0"`)로 표현한다 — 위성은 소비자가 설치한 단일 `haechi`를 재사용하므로 crypto/identity 표면이 하나다.
68
+ - **무거운 백엔드는 optional peer다.** `haechi-crypto-kms`는 `@aws-sdk/client-kms`를 `peerDependencies` + `peerDependenciesMeta.optional`로 선언하고 lazy import하므로, AWS 경로를 쓰지 않는 소비자는 설치하지 않고 core는 zero-dependency를 유지한다. 위성의 배포 tarball은 항상 **runtime `dependencies` 0**을 선언한다(CI `check-satellite-packaging`로 강제).
69
+ - 위성 export(`createKmsCryptoProvider`, `createAwsKmsClient`, `createJwtAuthProvider`)는 0.8에서 preview이며 각 위성의 `1.0.0` 전에 변경될 수 있다.
@@ -42,7 +42,7 @@ The following exports are treated as preview in 0.4.0.
42
42
  - `identity` audit field and the `authProvider` contract (reserved in 0.4, implemented in 0.6 — shape may change until then)
43
43
  - `status` / `audit-verify` CLI output shapes
44
44
  - `haechi/stream-filter` (`inspectResponseStream`, path helpers) and `createStreamProtector` (streaming inspection internals)
45
- - `haechi/auth` (`createBearerAuthProvider`, token store, `buildIdentity`) and the `authProvider` contract
45
+ - `haechi/auth` (`createBearerAuthProvider`, token store, `buildIdentity`, `buildExternalIdentity`) and the `authProvider` contract
46
46
  - `assertCryptoProviderConformance` and the hardened cryptoProvider contract (envelope base shape + provider extensions)
47
47
  - `audit.anchor` config and `verifyAuditChain(path, { anchorPath })`
48
48
  - `scripts/release-checksums.mjs` (SHA256SUMS generate/verify)
@@ -58,3 +58,12 @@ A migration note is added to `docs/current/release-*.md` or the README whenever
58
58
  - Changing an audit event field
59
59
  - Changing the token format
60
60
  - Changing the plugin manifest schema
61
+
62
+ ## 5. Satellite packages (`haechi-*`)
63
+
64
+ Satellites (e.g. `haechi-crypto-kms`, `haechi-auth-jwt`) version **independently** of core — a satellite release never bumps `haechi`, and vice versa.
65
+
66
+ - **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.1`). Each is pre-stable until its own `1.0.0`.
67
+ - **Core compatibility** is expressed as a `peerDependencies` range (`"haechi": ">=0.8.0 <1.0.0"`) — a satellite reuses the consumer's single installed `haechi`, so there is one crypto/identity surface.
68
+ - **Heavy backends are optional peers.** `haechi-crypto-kms` declares `@aws-sdk/client-kms` under `peerDependencies` + `peerDependenciesMeta.optional` and imports it lazily, so consumers who do not use the AWS path never install it and core stays zero-dependency. A satellite's published tarball always declares **zero runtime `dependencies`** (CI-gated by `check-satellite-packaging`).
69
+ - Satellite exports (`createKmsCryptoProvider`, `createAwsKmsClient`, `createJwtAuthProvider`) are preview in 0.8 and may change before each satellite's `1.0.0`.
@@ -10,7 +10,7 @@
10
10
 
11
11
  0.4에서 예약해 둔 `authProvider`/`identity` 계약을 구현하고, identity를 실질적인 per-client 제어로 전환한다: 내장 bearer 인증, 명명된 per-client policy profile, model allowlist, request rate limiting. 이로써 Haechi를 단일 호스트에서 복수의 클라이언트/에이전트 앞에 안전하게 배치할 수 있게 된다.
12
12
 
13
- **범위 결정 (2026-06-10):** 0.6은 auth 핵심에 집중한다. 원래 0.6으로 묶였던 무거운 운영 항목들 — Vault/AWS KMS 레퍼런스 어댑터, 외부 append-only audit 싱크, 서명된 릴리스 아티팩트, npm org(`@haechi/*`) 취득 — 은 각각 별도의 보안 설계를 받을 수 있도록 **0.7**로 이월한다.
13
+ **범위 결정 (2026-06-10):** 0.6은 auth 핵심에 집중한다. 원래 0.6으로 묶였던 무거운 운영 항목들 — Vault/AWS KMS 레퍼런스 어댑터, 외부 append-only audit 싱크, 서명된 릴리스 아티팩트, `haechi-*` 패키지 패밀리 — 은 각각 별도의 보안 설계를 받을 수 있도록 **0.7**로 이월한다.
14
14
 
15
15
  ## 2. 범위
16
16
 
@@ -21,7 +21,7 @@
21
21
  - `auth.provider`로 선택한다:
22
22
  - `none` (기본값) — 인증 없음; `identity`는 `null`로 유지 (0.5와 byte 단위로 동일한 audit 형태). Per-client policy는 default profile / base policy로 결정된다.
23
23
  - `bearer` — 내장 token auth (§2.2).
24
- - `external` — 주입된 `authProvider` 필요; 없으면 fail-closed (`keys.provider: external`과 동일한 방식). OIDC/JWT provider는 **0.7+ 위성 패키지** (`@haechi/auth-oidc`)로 남긴다; 0.6은 네트워크 IdP 코드를 포함하지 않는다.
24
+ - `external` — 주입된 `authProvider` 필요; 없으면 fail-closed (`keys.provider: external`과 동일한 방식). OIDC/JWT provider는 **0.7+ 위성 패키지** (`haechi-auth-oidc`)로 남긴다; 0.6은 네트워크 IdP 코드를 포함하지 않는다.
25
25
 
26
26
  ### 2.2 내장 bearer auth + token store
27
27
 
@@ -123,8 +123,8 @@ forward
123
123
 
124
124
  ## 4. 명시적 비범위 (0.7+로 이월)
125
125
 
126
- - OIDC/JWT provider (`@haechi/auth-oidc`, `@haechi/auth-jwt`) — 0.6은 bearer + external 주입만 포함.
127
- - Vault/AWS KMS 레퍼런스 어댑터; 외부 append-only audit 싱크; 서명된 릴리스 아티팩트; npm org `@haechi/*`.
126
+ - OIDC/JWT provider (`haechi-auth-oidc`, `haechi-auth-jwt`) — 0.6은 bearer + external 주입만 포함.
127
+ - Vault/AWS KMS 레퍼런스 어댑터; 외부 append-only audit 싱크; 서명된 릴리스 아티팩트; the `haechi-*` package family.
128
128
  - LLM token-budget limiting; 분산/공유 rate 상태.
129
129
  - auth provider의 동적 npm 로딩 (1.0 plugin sandbox).
130
130
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  Implement the `authProvider`/`identity` contracts reserved in 0.4 and turn identity into real per-client controls: built-in bearer authentication, named per-client policy profiles, model allowlisting, and request rate limiting. This makes Haechi safe(r) to put in front of multiple clients/agents on one host.
12
12
 
13
- **Scope decision (2026-06-10):** 0.6 is focused on the auth core. The heavier operational items originally grouped under 0.6 — Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, npm org (`@haechi/*`) acquisition — move to **0.7** so each gets its own security design instead of bloating one release.
13
+ **Scope decision (2026-06-10):** 0.6 is focused on the auth core. The heavier operational items originally grouped under 0.6 — Vault/AWS KMS reference adapter, external append-only audit sink, signed release artifacts, the `haechi-*` package family — move to **0.7** so each gets its own security design instead of bloating one release.
14
14
 
15
15
  ## 2. Scope
16
16
 
@@ -21,7 +21,7 @@ Implement the `authProvider`/`identity` contracts reserved in 0.4 and turn ident
21
21
  - Selected by `auth.provider`:
22
22
  - `none` (default) — no authentication; `identity` stays `null` (byte-identical audit shape to 0.5). Per-client policy resolves to the default profile / base policy.
23
23
  - `bearer` — built-in token auth (§2.2).
24
- - `external` — requires an injected `authProvider`; fail-closed if absent (mirrors `keys.provider: external`). The OIDC/JWT providers remain **0.7+ satellite packages** (`@haechi/auth-oidc`); 0.6 ships no network IdP code.
24
+ - `external` — requires an injected `authProvider`; fail-closed if absent (mirrors `keys.provider: external`). The OIDC/JWT providers remain **0.7+ satellite packages** (`haechi-auth-oidc`); 0.6 ships no network IdP code.
25
25
 
26
26
  ### 2.2 Built-in bearer auth + token store
27
27
 
@@ -123,8 +123,8 @@ Plus `policy.profiles`, `policy.profileBinding`, `policy.modelAllowlist`, `polic
123
123
 
124
124
  ## 4. Explicit non-scope (deferred to 0.7+)
125
125
 
126
- - OIDC/JWT providers (`@haechi/auth-oidc`, `@haechi/auth-jwt`) — 0.6 ships bearer + external-injection only.
127
- - Vault/AWS KMS reference adapter; external append-only audit sink; signed release artifacts; npm org `@haechi/*`.
126
+ - OIDC/JWT providers (`haechi-auth-oidc`, `haechi-auth-jwt`) — 0.6 ships bearer + external-injection only.
127
+ - Vault/AWS KMS reference adapter; external append-only audit sink; signed release artifacts; the `haechi-*` package family.
128
128
  - LLM token-budget limiting; distributed/shared rate state.
129
129
  - Dynamic npm loading of auth providers (1.0 plugin sandbox).
130
130
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  1.0("stable", developer-preview 레이블 제거)이 차단하는 운영 스토리를 강화한다: 단일 로컬 파일을 넘어선 audit 무결성, 외부 key custody, 검증 가능한 릴리스 아티팩트. 두 번의 1.0 차단 릴리스 중 첫 번째다.
12
12
 
13
- **범위 결정 (2026-06-10):** 0.7은 **ops hardening** — audit 무결성, key custody 계약, 서명된 아티팩트에 집중한다. 이전에 여기에 묶였던 **생태계** 항목들(npm org `@haechi/*`, `@haechi/crypto-kms` / `@haechi/auth-oidc` 게시, `@haechi/dashboard`, npm workspaces)은 **0.8**로 이월하며, 0.8에서 중복된 0.7 로드맵 행도 제거한다.
13
+ **범위 결정 (2026-06-10):** 0.7은 **ops hardening** — audit 무결성, key custody 계약, 서명된 아티팩트에 집중한다. 이전에 여기에 묶였던 **생태계** 항목들(the `haechi-*` package family, `haechi-crypto-kms` / `haechi-auth-oidc` 게시, `haechi-dashboard`, npm workspaces)은 **0.8**로 이월하며, 0.8에서 중복된 0.7 로드맵 행도 제거한다.
14
14
 
15
15
  Core의 **zero runtime dependency** 기조는 협상 불가: 0.7의 모든 것은 `node:` 빌트인만으로 제공된다. 무거운 어댑터(AWS KMS, Vault)는 satellite/example이며, 절대 core에 포함되지 않는다.
16
16
 
@@ -38,7 +38,7 @@ audit hash chain은 변조와 재정렬은 탐지하지만, 마지막 N개 레
38
38
 
39
39
  - `keys.provider: external`에 대한 `cryptoProvider` 계약을 강화하고 문서화한다: 외부 provider는 `encrypt`, `decrypt`, **그리고 `hmac`** (토큰/identity를 위해 0.4에서 추가됨)을 구현해야 하며, envelope 형태(`{ v, alg, kid, iv, ct, tag, aadHash }`)를 보존하고, canonical AAD를 바인딩하며, `kid`로 키를 선택해야 한다.
40
40
  - `assertCryptoProviderConformance(provider)` (익스포트된 테스트 헬퍼)를 제공한다: encrypt→decrypt 왕복, AAD 불일치 거부, `hmac` 결정론 + domain separation. Satellite 어댑터는 이를 통해 자체 테스트한다.
41
- - `examples/crypto-kms-reference/` 아래에 **레퍼런스 어댑터**를 제공한다 (자체 `package.json`, AWS/Vault SDK는 *optional/peer* 의존성으로, core의 `files`에 포함하지 않음): 주입 방법을 시연한다. 이것이 0.8에서(npm org 취득 후) 게시되는 **`@haechi/crypto-kms`** satellite의 소스가 된다.
41
+ - `examples/crypto-kms-reference/` 아래에 **레퍼런스 어댑터**를 제공한다 (자체 `package.json`, AWS/Vault SDK는 *optional/peer* 의존성으로, core의 `files`에 포함하지 않음): 주입 방법을 시연한다. 이것이 0.8에서(npm org 취득 후) 게시되는 **`haechi-crypto-kms`** satellite의 소스가 된다.
42
42
 
43
43
  ### 2.4 서명된 릴리스 아티팩트
44
44
 
@@ -47,8 +47,8 @@ audit hash chain은 변조와 재정렬은 탐지하지만, 마지막 N개 레
47
47
 
48
48
  ## 3. 명시적 비범위 (0.8로 이월)
49
49
 
50
- - npm org `@haechi/*` 생성; `@haechi/crypto-kms`, `@haechi/auth-oidc`, `@haechi/auth-jwt` 게시.
51
- - `@haechi/dashboard` (읽기 전용 audit 뷰어) 및 npm workspaces 전환.
50
+ - the `haechi-*` package family 생성; `haechi-crypto-kms`, `haechi-auth-oidc`, `haechi-auth-jwt` 게시.
51
+ - `haechi-dashboard` (읽기 전용 audit 뷰어) 및 npm workspaces 전환.
52
52
  - 게시된 패키지로서의 실제 AWS KMS / HashiCorp Vault SDK 연동 (0.7은 계약 + 레퍼런스 example만 제공).
53
53
  - 분산/공유 audit 또는 rate 상태.
54
54
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  Harden the operational story that 1.0 ("stable", developer-preview label removed) blocks on: audit integrity beyond a single local file, external key custody, and verifiable release artifacts. This is the first of the two 1.0-blocker releases.
12
12
 
13
- **Scope decision (2026-06-10):** 0.7 is focused on **ops hardening** — audit integrity, key custody contract, signed artifacts. The **ecosystem** items previously grouped here (npm org `@haechi/*`, publishing `@haechi/crypto-kms` / `@haechi/auth-oidc`, `@haechi/dashboard`, npm workspaces) move to **0.8**, which also removes the duplicate 0.7 roadmap row.
13
+ **Scope decision (2026-06-10):** 0.7 is focused on **ops hardening** — audit integrity, key custody contract, signed artifacts. The **ecosystem** items previously grouped here (the `haechi-*` package family, publishing `haechi-crypto-kms` / `haechi-auth-oidc`, `haechi-dashboard`, npm workspaces) move to **0.8**, which also removes the duplicate 0.7 roadmap row.
14
14
 
15
15
  Core's **zero runtime dependency** posture is non-negotiable: everything in 0.7 ships with `node:` builtins only. Heavy adapters (AWS KMS, Vault) are satellites/examples, never core.
16
16
 
@@ -39,7 +39,7 @@ The audit hash chain detects tampering and reordering but **not** deletion of th
39
39
 
40
40
  - Tighten and document the `cryptoProvider` contract for `keys.provider: external`: a provider always implements `encrypt`/`decrypt`, binds canonical AAD, and selects keys by `kid`; the envelope **base shape** is `{ v, alg, kid, iv, ct, tag, aadHash }` and adapters **may add provider-specific fields** (e.g. a KMS adapter's `wrappedKey`). `hmac` is required **only by features that use it** — bearer auth and deterministic tokenization — and `createRuntime` fails closed at construction when one of those is configured without `hmac` (an encrypt-only provider is otherwise valid). Policy-bundle signing uses the local key file directly via the CLI, not the injected provider.
41
41
  - Ship `assertCryptoProviderConformance(provider, { requireHmac = true })` (an exported test helper): encrypt→decrypt round-trip (distinct plaintexts), AAD-mismatch rejection, **tampered-ciphertext rejection (real AEAD authentication)**, and `hmac` determinism + data-dependency + domain separation + invalid-domain rejection. Satellite adapters self-test against it; pass `requireHmac: false` for an encrypt-only provider.
42
- - Ship a **reference adapter** under `examples/crypto-kms-reference/` (its own `package.json`, AWS/Vault SDK as an *optional* dependency; the in-process `createInMemoryKms` is explicitly non-production) demonstrating envelope-encryption injection. It is the source that becomes the published **`@haechi/crypto-kms`** satellite in 0.8 (gated on the npm org).
42
+ - Ship a **reference adapter** under `examples/crypto-kms-reference/` (its own `package.json`, AWS/Vault SDK as an *optional* dependency; the in-process `createInMemoryKms` is explicitly non-production) demonstrating envelope-encryption injection. It is the source that becomes the published **`haechi-crypto-kms`** satellite in 0.8 (gated on the npm org).
43
43
 
44
44
  ### 2.4 Signed release artifacts
45
45
 
@@ -48,8 +48,8 @@ The audit hash chain detects tampering and reordering but **not** deletion of th
48
48
 
49
49
  ## 3. Explicit non-scope (deferred to 0.8)
50
50
 
51
- - Create npm org `@haechi/*`; publish `@haechi/crypto-kms`, `@haechi/auth-oidc`, `@haechi/auth-jwt`.
52
- - `@haechi/dashboard` (read-only audit viewer) and npm workspaces conversion.
51
+ - Publish the `haechi-*` family: publish `haechi-crypto-kms`, `haechi-auth-oidc`, `haechi-auth-jwt`.
52
+ - `haechi-dashboard` (read-only audit viewer) and npm workspaces conversion.
53
53
  - Real AWS KMS / HashiCorp Vault SDK integration as a published package (0.7 ships the contract + reference example only).
54
54
  - Distributed/shared audit or rate state.
55
55
 
@@ -0,0 +1,145 @@
1
+ # Haechi 0.8 구현 범위
2
+
3
+ - 상태: Draft 0.3 (설계 — 아직 미구현)
4
+ - 작성일: 2026-06-10
5
+ - 대상 버전: 0.8.0 (0.7.0 이후)
6
+ - 유형: 에코시스템 기반
7
+
8
+ ## 1. 릴리스 목표
9
+
10
+ `haechi-*` 패키지 에코시스템을 띄운다: 저장소를 npm workspaces 모노레포로 전환하고 첫 두 satellite를 배포한다 — `haechi-crypto-kms`(0.7 reference 승격)와 `haechi-auth-jwt`(JWKS bearer 검증), 둘 다 unscoped(`@haechi` scope는 점유됨; org 불필요). 이로써 운영 키 custody를 설치 가능한 패키지로 실현하고, core의 zero-dependency 자세를 건드리지 않으면서 auth 에코시스템을 키운다.
11
+
12
+ **범위 결정(2026-06-10):** 0.8은 **패키징 기반 + satellite**다. `haechi-dashboard` 읽기 전용 audit 뷰어(UI 빌드)와 완전한 대화형 `haechi-auth-oidc`는 **0.9**로 이동하여, 0.8은 코드가 가볍고 모노레포 + 두 개의 헤드리스 친화적 어댑터에 집중한다.
13
+
14
+ Core(`haechi`, unscoped)는 **zero runtime dependency**를 유지한다. satellite 의존성(예: AWS SDK)은 해당 satellite의 `package.json`에만 존재하며 core의 tarball이나 SBOM에 절대 들어가지 않는다.
15
+
16
+ ## 2. 범위
17
+
18
+ ### 2.1 npm workspaces 모노레포 (검증된 resolution 메커니즘)
19
+
20
+ 순진한 레이아웃(`"workspaces": ["satellites/*"]` + satellite peer 범위 `"haechi": ">=0.8.0"`)은 **동작하지 않으며** 경험적 테스트 후 기각했다: npm은 충족되지 않은 peer 범위를 레지스트리 조회로 취급하고(`ETARGET: no matching version for haechi@>=0.8.0`), 루트 프로젝트를 `node_modules/haechi`로 심링크하지 않으며, satellite의 `import "haechi/crypto"`가 `ERR_MODULE_NOT_FOUND`로 던진다. 루트 프로젝트는 기본적으로 workspace 멤버가 **아니므로** npm이 절대 링크하지 않는다.
21
+
22
+ 검증된 동작 레이아웃:
23
+
24
+ - **루트가 자기 자신을 workspace 멤버로 등록한다:** `"workspaces": [".", "satellites/*"]`. `"."` 항목이 npm으로 하여금 `node_modules/haechi → ..` 심링크를 만들게 해 satellite가 core를 resolve한다. 이게 없으면 satellite는 레지스트리로 fallback한다.
25
+ - **저장소 루트는 배포되는 `haechi` 패키지로 그대로 남는다** — `exports`, `bin`, `files` 허용목록은 변하지 않는다. `package.json` 변경은 추가된 `workspaces` 필드와 버전 bump뿐이다. satellite는 core의 `files`에 없으므로 `haechi` tarball 안으로 절대 실리지 않는다(검증됨: 루트 `npm pack --dry-run`은 `files` 허용목록만 나열하고 `satellites/`는 제외된다).
26
+ - **satellite는 core에 대한 이중 의존성을 선언한다:**
27
+ - `"peerDependencies": { "haechi": ">=0.8.0 <1.0.0" }` — *소비자*가 설치할 때의 계약. satellite는 소비자의 단일 `haechi` 인스턴스를 재사용한다(crypto/identity 표면 하나, 중복 사본 없음).
28
+ - `"devDependencies": { "haechi": "*" }` — 모노레포 개발/CI 중에 npm이 peer 범위를 레지스트리에서 resolve하지 않고 **로컬 workspace**를 링크하게 만드는 메커니즘. `npm pack`은 배포 tarball에서 devDependencies를 제거하므로 소비자 매니페스트에는 peer 범위만 남고 `*` devDep은 다운스트림에서 보이지 않는다. (`satellites/*/package.json`을 읽는 저장소/소스 스캐너는 여전히 그것을 본다. 이 source-vs-artifact 차이는 예상된 것이고 무해하다.)
29
+ - **소비자 peer-mismatch 동작:** 배포된 satellite를 *비호환* `haechi`(예: 이미 설치된 `haechi@0.7.0`)에 설치하면 hard failure가 아니라 npm `ERESOLVE` **경고**가 난다; 소비자는 `haechi`를 올리거나(자기 책임으로 `--legacy-peer-deps`) 해야 한다. satellite는 범위를 벗어난 core에서 올바르게 동작하지 않는다.
30
+ - satellite는 core를 subpath로 import한다(`haechi/crypto`, `haechi/auth`, `haechi/runtime`). 개발 중에는 workspace 심링크로, 프로덕션에서는 소비자가 설치한 `haechi`로 resolve된다.
31
+ - `examples/crypto-kms-reference/`는 `satellites/crypto-kms/`로 **승격**한다. reference 예제는 workspaces 이전에 중첩 `package.json`이 `haechi/crypto`를 self-resolve하지 못해 `canonicalize`를 인라인했었다; **workspaces 하에서는 그 import가 resolve되므로** satellite는 사본을 들고 다니지 않고 `haechi/crypto`에서 `canonicalize`를 import한다(core와 satellite 간 AAD 정규화 drift 방지). 기존 `examples/` 디렉터리는 배포된 패키지를 가리키는 짧은 README만 남긴다. conformance 테스트가 satellite의 AAD 정규화가 `haechi/crypto`와 **byte-for-byte 동일**함을 검증한다(의미적 동등이 아니라).
32
+ - **lock 파일:** workspaces 전환은 `package-lock.json`을 workspace-resolve된 항목(루트 자기 멤버 포함)으로 재생성한다. 재생성된 lock 파일을 커밋한다; CI는 `npm ci`(stale/누락 lock이면 실패)를 쓰므로, 전환 PR이 새 lock을 커밋해야 한다.
33
+
34
+ **CI 전략(중복 실행 방지):** 루트 CI는 루트에서 `node --test`를 직접 돌리며, 이는 `satellites/**/*.test.mjs`를 자동 발견한다(workspace 심링크 덕에 그들의 `haechi/*` import가 resolve됨). CI는 `npm test --workspaces`를 **쓰지 않는다**(루트 자기 멤버로 재귀해 스위트를 다시 돌릴 것이다). 각 satellite는 로컬 단독 실행용(`npm test -w haechi-crypto-kms`) 자체 `test` 스크립트만 유지한다. 검증됨: 루트 `node --test`가 core + satellite 테스트를 한 번 실행하고, `node_modules/haechi → ..` 심링크 순환에도 runner가 hang하지 않는다(node는 `node_modules`를 건너뜀).
35
+
36
+ **정직한 패키징 노트(기존 "byte-stable" 주장 대체):** 루트 tarball은 0.7.0과 byte-identical이 **아니다** — `package.json`이 `workspaces` 필드를 얻고 버전이 bump되며, 이는 어떤 릴리스에서도 예상되는 일이다. 방어 가능하고 테스트된 더 좁은 주장이며 CI 게이트(§6.1)로 강제한다: **(a) satellite 파일이 `haechi` tarball에 나타나지 않고, (b) `haechi` tarball 자체의 `package.json`이 runtime `dependencies`를 0으로 선언한다.** 게이트는 **패킹된 매니페스트**를 검사한다(`npm pack` 출력에서 `package.json`을 추출해 `dependencies`가 비어있음/undefined임을 단언) — 오늘은 공허하게 통과하고 미래의 runtime-dep 누수를 놓칠 설치된 `node_modules` SBOM이 아니라.
37
+
38
+ ### 2.2 unscoped `haechi-*` 이름 + 패키지별 trusted publishing
39
+
40
+ - **네이밍(2026-06-10 결정):** `@haechi` npm org/scope는 제3자가 이미 점유했으므로, satellite는 **unscoped `haechi-*`** 이름으로 발행한다 — `haechi-crypto-kms`, `haechi-auth-jwt`(둘 다 npm에 비어있음 확인). **npm org 불필요**, unscoped core `haechi`와 일관되며, 각 이름을 개별 예약 + Trusted Publisher 바인딩한다. (scope 대비 트레이드오프: 네임스페이스 그룹핑/방어 없음; `haechi-` 접두사가 관례.)
41
+ - 각 satellite는 0.7에서 증명한 **동일한 OIDC trusted-publishing + sigstore + SHA256SUMS** 경로로 배포한다 — 자체 npmjs.com Trusted Publisher 링크와 태그 트리거 배포 워크플로.
42
+ - **satellite `package.json` 요구사항(루트에서 상속 안 됨):** 각 satellite는 여전히 자체 `"publishConfig": { "access": "public", "provenance": true }`를 설정한다. unscoped 패키지는 기본이 public이라 `access: public`은 보조적이고, `provenance: true`와 루트 `publishConfig` 비상속이 핵심이다. 배포 후 런북은 `npm view haechi-<pkg> access`가 `public`을 보고하는지 검증한다.
43
+ - **태그 네임스페이싱 + 워크플로 가드(오트리거·충돌 방지):**
44
+ - core 릴리스 태그: `v<semver>`(예: `v0.8.0`). 루트 배포 워크플로는 `push: tags: ['v[0-9]*.[0-9]*.[0-9]*']`에서 트리거된다. GitHub 태그 glob은 `.`을 리터럴로, `[0-9]*`를 느슨하게 취급하므로(`v1.2.3.4`나 `v1a.2.3`도 매칭됨), 워크플로는 pre-publish 단계에서 엄격한 `^v[0-9]+\.[0-9]+\.[0-9]+$` 정규식으로 **재검증**하고 불일치 시 fail-closed한다.
45
+ - satellite 태그는 **접두사**가 붙는다: `crypto-kms-v<semver>`, `auth-jwt-v<semver>`. 각 satellite 워크플로는 자기 접두사 glob에서만 트리거되고 마찬가지로 `^<prefix>-v[0-9]+\.[0-9]+\.[0-9]+$`로 재검증한다.
46
+ - 각 워크플로는 배포할 패키지 디렉터리를 재확인한다(`npm publish -w <dir>`)므로 잘못 태깅된 push가 엉뚱한 패키지를 배포할 수 없다. npmjs.com의 Trusted Publisher는 **특정 워크플로 파일명**에 바인딩된다 — npm 설정 갱신 없이 워크플로를 rename하면 OIDC auth가 깨진다(런북에 패키지→워크플로-파일명→태그-glob 매핑 표와 함께 실패 모드로 문서화).
47
+ - satellite별 **독립 semver**(satellite patch가 core를 bump하지 않음). satellite는 `0.1.0`에서 시작한다. **pre-1.0 계약:** satellite는 표준 npm semver를 따르며 `0.x` **minor** bump가 breaking change를 담을 수 있다; 소비자는 `major.minor`로 핀해야 한다(예: `haechi-crypto-kms@~0.1`). satellite는 자체 `1.0.0`까지 pre-stable이다.
48
+ - **첫 배포 부트스트랩(org 불필요):** satellite별 순서 — (1) npmjs.com에서 (아직 미발행) unscoped 이름에 대해 repo + 정확한 워크플로 파일명을 연결하는 **Trusted Publisher 설정**; (2) satellite 첫 태그 push → 워크플로의 OIDC 배포가 provenance와 함께 `0.1.0`을 생성하고 첫 발행 시 **이름을 확보**. 노트북에서의 수동 `npm publish`는 필요 없다(0.7 trusted-publishing 자세와 동일). 이름이 unscoped이고 현재 비어있으므로 org-membership 선행 요건이 없다.
49
+
50
+ ### 2.3 `haechi-crypto-kms` (배포 + 실제 KMS 클라이언트)
51
+
52
+ - 0.7 reference(`createKmsCryptoProvider` envelope 암호화 + `createInMemoryKms`)를 배포 패키지로 승격하며, 인라인 `canonicalize`를 `import { canonicalize } from "haechi/crypto"`(§2.1)로 전환한다. 기존 `kms` 클라이언트 인터페이스(`keyId`/`wrap`/`unwrap`/`deriveHmacKey`)는 **변경하지 않으므로** 승격된 provider와 in-memory 클라이언트는 byte-for-byte 동일하고 0.7 테스트가 그대로 넘어온다.
53
+ - **실제 AWS KMS 클라이언트**를 `haechi-crypto-kms/aws`에 추가한다: `createAwsKmsClient({ keyId, region, client, hmacRootCiphertext })`. 동일한 `kms` 인터페이스를 구현한다: `wrap` = CSPRNG로 생성한 32바이트 data key를 KMS `Encrypt`, `unwrap` = KMS `Decrypt`(envelope 암호화 — master key는 KMS를 떠나지 않음); `deriveHmacKey(domain)` = 단일 KMS-`Decrypt`된 32바이트 root(`hmacRootCiphertext`, 캐시)에 대한 **HKDF-SHA256**, domain-separated — 결정적이고 토큰당 네트워크 호출 없음. `hmacRootCiphertext`가 없으면 `deriveHmacKey`는 throw하고 provider는 encrypt-only가 된다(`requireHmac:false`로 유효).
54
+ - **`@aws-sdk/client-kms`는 hard dependency가 아니라 OPTIONAL peer dependency다**(2026-06-10 결정, 기존 "satellite 자체 의존성" 표현을 수정). `client` 미주입 시에만 **lazy import**되므로: 모노레포 `npm ci`/CI는 (대용량) AWS SDK를 절대 받지 않고; in-memory 또는 주입형 클라이언트를 쓰는 소비자는 설치하지 않으며; core는 자명하게 영향받지 않는다. 배포 satellite는 `peerDependencies` + `peerDependenciesMeta.optional`로 선언한다. 이로써 실제 백엔드를 제공하면서도 satellite를 의존성 가볍게 유지한다.
55
+ - satellite CI는 in-memory 클라이언트 **그리고** KMS `encrypt`/`decrypt` ops의 **주입된 mock**(SDK·네트워크 없음)으로 구동되는 AWS 클라이언트에 대해 `assertCryptoProviderConformance`(workspace 심링크로 `haechi/crypto`에서 import)를 실행한다. mock은 충실한 envelope(per-mock master key의 AES-256-GCM)여야 한다: `Decrypt`는 이 키가 wrap한 blob에 대해서만 plaintext를 반환하고, 다른 키가 wrap한 blob(cross-key 격리)과 손상된 blob은 **거부**한다. 항상 성공하는 trivial stub은 불충분하며 스위트가 이 거부 경로와 HMAC 결정성/domain-separation을 실행한다. sandbox KMS 키에 대한 실제 `createAwsKmsClient` 검증은 **CI 밖 통합 테스트**다(문서화, 게이팅 아님).
56
+
57
+ ### 2.4 `haechi-auth-jwt` (JWKS bearer 검증, 의존성 최소)
58
+
59
+ `createJwtAuthProvider({ issuer, audience, jwksUri, cryptoProvider, algorithms, clockSkewSeconds, claimMappings })`는 **헤드리스** 게이트웨이를 위한 `authProvider` 계약을 구현한다. `node:` 빌트인만으로 구현 가능하다(`jose` 없음): JWKS는 전역 `fetch`로, JWK→키는 `crypto.createPublicKey({ key: jwk, format: "jwk" })`, 서명은 `crypto.verify`로 검증한다.
60
+
61
+ **구현 노트 — ES256 서명 인코딩(검증됨):** JWS ES256 서명은 raw `R‖S`(IEEE-P1363, P-256은 64바이트)이지만, `node:crypto.verify`는 EC 키에 대해 기본이 **DER**이고 raw 서명에 `false`를 반환한다 — 유효한 ES256 토큰을 전부 조용히 거부한다. 검증기는 EC 알고리즘에 대해 반드시 `dsaEncoding: "ieee-p1363"`을 넘겨야 한다. (경험적 확인: 기본 DER ⇒ `false`; `ieee-p1363` ⇒ `true`.) 이는 옵션이 아니라 수용 기준이다.
62
+
63
+ **보안 명세(필수 — 옵션이 아니라 수용 기준).** 아래 구체 상수는 구현 재량이 아니라 *결정*이다.
64
+
65
+ - **알고리즘 선택은 서버 측이며, 토큰에서 가져오지 않는다.** 검증기는 설정된 `algorithms` 허용목록(기본 `["RS256","ES256"]`)과 JWK의 `kty`/`crv`에서 알고리즘을 고른다. 토큰의 `alg` 헤더는 키 선택 **전에** 허용목록 *멤버십*만 확인하고, 검증 루틴을 선택하지 않는다.
66
+ - **`alg: "none"`을 무조건 거부**한다.
67
+ - **alg-confusion 차단:** RSA 공개키를 HMAC verify에 절대 넣지 않는다. HMAC 계열(`HS*`)은 기본적으로 **허용하지 않는다**; JWKS에서 온 공개키는 오직 그에 맞는 비대칭 알고리즘과만 쓰인다.
68
+ - **`kid`는 필수**다; 서명 키는 JWKS에서 `kid`로 선택하며, 모든 키를 시도하지 않는다.
69
+ - **RSA 키 강도 하한:** modulus `< 2048` 비트인 RSA JWK는 invalid로 거부한다.
70
+ - **JWK 사용 의도:** JWK에 `use`가 있으면 `sig`여야 하고, `key_ops`가 있으면 `verify`/`sign`을 포함하고 `encrypt`/`decrypt`를 포함하지 않아야 한다. 아니면 거부.
71
+ - **헤더 `typ` / JWE 금지:** `typ`가 있으면 `JWT`여야 한다; 암호화된(JWE) 토큰은 무조건 거부 — JWS만 수용.
72
+ - **클레임은 필수이며 완전 검증한다:**
73
+ - `iss`는 설정된 `issuer`와 같아야 한다.
74
+ - `aud`(토큰)는 문자열 또는 문자열 배열일 수 있다(RFC 7519); 설정된 `audience`는 그 문자열과 같거나 배열의 멤버여야 한다 — 정확, 대소문자 구분 일치.
75
+ - `sub`는 필수이며 비어있지 않은 문자열이어야 한다(`subjectHash`의 입력).
76
+ - `exp`와 `nbf`는 **필수**다. `exp`: `now > exp + clockSkewSeconds`이면 거부. `nbf`: `now < nbf - clockSkewSeconds`이면 거부. `exp`가 없는 토큰은 거부. `iat`가 있으면 sanity-check.
77
+ - **`clockSkewSeconds`** 기본 `60`, **최대 `300`** — `> 300` 값은 생성 시 거부(더 큰 skew는 만료 검증을 무력화).
78
+ - **JWKS fetch는 SSRF-하드닝:**
79
+ - `issuer`는 유효한 **HTTPS URL**이어야 하고; `jwksUri`는 HTTPS이며 그 **hostname이 `issuer` hostname과 정확히 일치**해야 한다(포트 제외). 0.8은 **단일 origin issuer만** 지원한다 — issuer 식별자와 다른 host에서 JWKS를 서빙하는 IdP(일부 CDN-fronted 구성)는 0.8 범위 밖이며 생성 시 거부한다. 비-URL issuer(URN 형태)는 명확한 오류로 생성 시 거부한다.
80
+ - private/loopback/link-local 대역과 클라우드 메타데이터 엔드포인트로의 요청은 거부한다: `127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`(`169.254.169.254` 포함), `fe80::/10`. 거부는 DNS resolve 후 fetch 시점에 한다(rebinding 방어), fetch 타임아웃 포함.
81
+ - **JWKS 응답 경계:** 응답 본문 `> 1 MiB`는 거부; `JSON.parse`는 병적 중첩(depth 한계)에 대해 가드한다. JWT 세그먼트는 `JSON.parse` 전에 **엄격한 base64url**(`[A-Za-z0-9_-]`, 패딩 없음)로 디코드한다.
82
+ - **JWKS 캐시는 경계가 있고 DoS 저항적:** 키는 TTL로 캐시한다; 알 수 없는 `kid`가 **무제한 refetch를 트리거하지 않는다** — **60초 cooldown**당 최대 1회 전체 JWKS 갱신이므로, 위조된 `kid` 폭주가 IdP에 대한 fetch storm으로 바뀌지 않는다.
83
+ - **identity는 PII-safe(fail-closed):** `cryptoProvider`는 **필수**이며 `hmac`을 노출해야 한다; 없으면 provider 생성자가 throw한다(PII-safe identity를 만들 수 없음). `subjectHash`/`issuerHash`는 keyed **HMAC-SHA-256**(`haechi:identity:hash:v1`), hex 인코딩(64자) — raw `sub`/`iss`는 절대 저장·로깅하지 않는다. `scopes`는 설정된 scope 클레임(`scp`/`scope`)에서, `labels`는 허용목록 클레임 매핑에서만 온다.
84
+ - **모든 곳에서 fail-closed:** 검증 오류 → `authenticate`는 `null`(거부)을 반환하고 요청 경로로 throw하지 않으며, 토큰 세부정보를 클라이언트로 echo하지 않는다.
85
+
86
+ **주입**으로 연결한다(`createRuntime(config, { authProvider: createJwtAuthProvider(...) })`); `auth.provider: external`. 동적 로딩은 1.0 plugin sandbox까지 금지를 유지한다.
87
+
88
+ ### 2.5 모노레포 릴리스 프로세스
89
+
90
+ - 기존 루트 워크플로가 `haechi`를 계속 배포한다(`v*` 태그 glob + 엄격한 정규식 재검증으로 가드).
91
+ - 각 satellite는 0.7의 서명 아티팩트 단계(pack → checksum → attest → publish → upload)를 자기 디렉터리로 스코프하여 재사용하는 자체 접두사 태그 배포 워크플로를 가지며, 각각 자체 Trusted Publisher(특정 워크플로 파일명)에 바인딩된다.
92
+ - 패키지별 릴리스 런북(`release-process.md`)은 다음을 문서화한다: 태그 규칙 + 패키지→워크플로-파일명→태그-glob 매핑 표, Trusted Publisher 부트스트랩 순서(예약 → 설정 → 태그), 워크플로-rename 실패 모드, 배포 후 검증(provenance, `npm view ... access`).
93
+
94
+ ## 3. 명시적 비범위 (0.9+로 이월)
95
+
96
+ - `haechi-dashboard` 읽기 전용 audit 뷰어(UI 빌드, 자체 기술스택 결정).
97
+ - `haechi-auth-oidc` 완전 대화형 OIDC(authorization-code flow) — `haechi-auth-jwt`가 헤드리스 케이스를 먼저 다룬다.
98
+ - `haechi-auth-jwt` multi-origin/CDN-fronted JWKS(issuer host ≠ JWKS host).
99
+ - `haechi-classifier-*` ML/휴리스틱 classifier 플러그인.
100
+ - `haechi-crypto-kms` Vault/GCP/Azure 백엔드(0.8은 AWS만).
101
+ - satellite의 동적 로딩(1.0 plugin sandbox).
102
+
103
+ risk-register와 로드맵은 `haechi-auth-oidc`와 `haechi-dashboard`를 0.8 행에서 빼 새 **0.9** 행으로 옮겨, 공개 문서가 이 범위와 일치하도록 갱신한다.
104
+
105
+ ## 4. 하위 호환성
106
+
107
+ Core 동작은 불변이다: 루트 패키지의 `exports`, `bin`, `files`, zero-dep runtime 자세는 동일하다. 루트 `package.json`에 `workspaces`(`"."` 자기 항목 포함)를 추가하는 것은 `haechi`를 단일 의존성으로 설치하는 누구에게도 무해(inert)하다. 기존 config와 API는 손대지 않으며, satellite는 순수하게 부가적이고 opt-in이다.
108
+
109
+ ## 5. 1.0 관계
110
+
111
+ 0.8 자체는 1.0 blocker를 닫지 않지만, **운영 키 custody를 설치 가능하고 attest된 패키지로 실현**하고(`haechi-crypto-kms`) satellite 모델을 end-to-end로 증명한다. 남은 1.0 게이트는 유지된다: API 안정성 freeze와 plugin sandbox + 실환경 검증.
112
+
113
+ ## 6. 테스트 기준 (PR 분해에 매핑)
114
+
115
+ ### 6.1 PR1 — workspaces 전환 (새 배포 패키지 없음)
116
+
117
+ - 루트 `npm install`이 **ERESOLVE/ETARGET 없이** exit 0; `node_modules/haechi`가 workspace 심링크; 커밋된 `package-lock.json`으로 fresh checkout에서 `npm ci` 성공.
118
+ - `import { ... } from "haechi/crypto"`를 하는 satellite 테스트가 루트 `node --test`에서 green.
119
+ - **no-leak + zero-dep 게이트:** core `npm pack --dry-run`에 **`satellites/` 경로 없음**; **패킹된** `haechi` `package.json`(tarball에서 추출)의 `dependencies`가 비어있음/undefined. 게이트는 **음성 테스트**된다: core의 `files`에 `satellites/`를 임시로 추가하거나 core `package.json`에 runtime dep를 추가하면 게이트가 명확한 오류로 실패한다(공허한 통과 방지).
120
+ - in-memory crypto provider(승격된 0.7 코드)가 workspace 심링크로 `assertCryptoProviderConformance`를 통과하며, `haechi/crypto` 대비 byte-for-byte `canonicalize` parity 검사를 포함한다.
121
+
122
+ ### 6.2 PR2 — `haechi-crypto-kms` (실제 AWS 클라이언트)
123
+
124
+ - in-memory **및 AWS** 클라이언트(AWS는 KMS `encrypt`/`decrypt` ops의 **주입된 mock**으로 구동 — SDK·네트워크 없음)가 cross-key/손상-blob **거부** 경로와 HMAC 결정성/domain-separation을 포함해 `assertCryptoProviderConformance`를 통과; `createRuntime`을 통한 end-to-end(암호화 + 토큰화 round-trip).
125
+ - `createAwsKmsClient`는 `keyId` 없으면 throw; `hmacRootCiphertext` 없으면 `deriveHmacKey`가 throw하고 provider는 encrypt-only로 conformance 통과(`requireHmac:false`).
126
+ - 배포 매니페스트가 `publishConfig.access: public`을 설정하고 `@aws-sdk/client-kms`를 `peerDependencies` + `peerDependenciesMeta.optional`로 선언(runtime `dependency` 아님); 배포 satellite tarball은 `dependencies: {}`이고 core tarball은 zero-dep 유지(§6.1 게이트 계속 통과).
127
+ - satellite publish 워크플로(`crypto-kms-v<semver>`)가 0.7 서명 아티팩트 경로로 존재; core 워크플로는 satellite 릴리스 태그가 `haechi`를 발행하지 않도록 가드.
128
+
129
+ ### 6.3 PR3 — `haechi-auth-jwt` (보안 게이트)
130
+
131
+ - 유효한 RS256/ES256 JWT(테스트 키 서명, stub JWKS)가 **audit에 raw `sub` 없이** PII-safe identity로 인증; `subjectHash`/`issuerHash`는 64-hex자 HMAC-SHA-256.
132
+ - 다음이 각각 **거부**됨: `alg:"none"`; RSA 공개키로 위조한 `HS256` 토큰(alg-confusion); JWE/`typ` 불일치; 만료(`exp`); 아직 유효하지 않음(`nbf`); `exp` 누락; `sub` 누락/빈 값; 잘못된 `aud`(문자열·배열 형태); 잘못된 `iss`; 알 수 없는 `kid`; 잘못된 서명; `< 2048` 비트 RSA JWK; `use:"enc"`/`key_ops:["encrypt"]` JWK.
133
+ - 생성이 거부함: non-HTTPS 또는 cross-origin `jwksUri`; 비-URL `issuer`; `clockSkewSeconds > 300`; `cryptoProvider.hmac` 누락. `127.0.0.1`, `169.254.169.254`, `::1`, 또는 RFC1918 CIDR로 resolve되는 `jwksUri`는 거부.
134
+ - 알 수 없는 `kid` 폭주가 60초 cooldown 내 **정확히 1회** JWKS refetch를 트리거; `> 1 MiB` JWKS 응답은 거부.
135
+
136
+ ### 6.4 모든 satellite
137
+
138
+ - 각각 provenance + sigstore attestation으로 배포(0.7처럼 배포 후 검증).
139
+
140
+ ## 7. 제안 PR 분해 (스택)
141
+
142
+ 1. **Workspaces 전환**(새 배포 패키지 없음): 루트 `workspaces: [".", "satellites/*"]`, 루트를 **0.8.0**으로 bump, `crypto-kms`를 `satellites/crypto-kms/`로 이동(core `peer + dev` 의존성), 인라인 `canonicalize`를 `haechi/crypto`로 전환(+ parity 테스트), 테스트 재지정, 재생성된 `package-lock.json` 커밋, **no-leak + zero-dep CI 게이트** 추가(음성 테스트 포함), 루트 CI가 루트 `node --test`로 모든 workspace 테스트 실행. → §6.1
143
+ 2. **`haechi-crypto-kms`:** 실제 AWS KMS 클라이언트(satellite만의 `@aws-sdk/client-kms` 의존성) + 충실한 mocked-AWS conformance CI + `publishConfig` + 접두사 태그 배포 워크플로(엄격한 정규식 가드) + Trusted Publisher 부트스트랩. → §6.2
144
+ 3. **`haechi-auth-jwt`:** §2.4 전체 보안 명세를 구현하는 JWKS 검증 provider + identity 매핑 + §6.3 보안 게이트 테스트 + `publishConfig` + 접두사 태그 배포 워크플로. → §6.3
145
+ 4. **0.8.0 릴리스 컷:** EN/KO 문서, packaging/roadmap/risk-register(OIDC+dashboard를 0.9로 이동)/api-stability, wiki, npm org / Trusted Publisher 런북(매핑 표 + 부트스트랩 순서 + 실패 모드).
@@ -0,0 +1,145 @@
1
+ # Haechi 0.8 Implementation Scope
2
+
3
+ - Status: Draft 0.3 (design — not yet implemented)
4
+ - Date: 2026-06-10
5
+ - Target version: 0.8.0 (after 0.7.0)
6
+ - Type: ecosystem foundation
7
+
8
+ ## 1. Release Goal
9
+
10
+ Stand up the `haechi-*` package ecosystem: convert the repo to an npm workspaces monorepo and publish the first two satellites — `haechi-crypto-kms` (promoting the 0.7 reference) and `haechi-auth-jwt` (JWKS bearer verification), both unscoped (the `@haechi` scope is taken; no org needed). This realizes operational key custody as an installable package and grows the auth ecosystem without touching core's zero-dependency posture.
11
+
12
+ **Scope decision (2026-06-10):** 0.8 is the **packaging foundation + satellites**. The `haechi-dashboard` read-only audit viewer (a UI build) and full interactive `haechi-auth-oidc` move to **0.9** so 0.8 stays code-light and focused on the monorepo + two headless-friendly adapters.
13
+
14
+ Core (`haechi`, unscoped) stays **zero runtime dependency**. Satellite dependencies (e.g. an AWS SDK) live in the satellite's own `package.json` only and never enter core's tarball or SBOM.
15
+
16
+ ## 2. Scope
17
+
18
+ ### 2.1 npm workspaces monorepo (verified resolution mechanic)
19
+
20
+ The naive layout (`"workspaces": ["satellites/*"]` + a satellite peer range `"haechi": ">=0.8.0"`) **does not work** and was rejected after empirical testing: npm treats the unmet peer range as a registry lookup (`ETARGET: no matching version for haechi@>=0.8.0`), never symlinks the root project into `node_modules/haechi`, and the satellite's `import "haechi/crypto"` throws `ERR_MODULE_NOT_FOUND`. The root project is **not** a workspace member by default, so npm never links it.
21
+
22
+ The verified working layout:
23
+
24
+ - **Root lists itself as a workspace member:** `"workspaces": [".", "satellites/*"]`. The `"."` entry is what makes npm create the `node_modules/haechi → ..` symlink so satellites resolve core. Without it, satellites fall back to the registry.
25
+ - **The repo root remains the published `haechi` package** — its `exports`, `bin`, and `files` allowlist are unchanged. The only `package.json` delta is the added `workspaces` field and the version bump. Satellites are **not** in core's `files`, so they never ship inside the `haechi` tarball (verified: a root `npm pack --dry-run` lists only the `files` allowlist; `satellites/` is excluded).
26
+ - **Satellites declare a dual dependency on core:**
27
+ - `"peerDependencies": { "haechi": ">=0.8.0 <1.0.0" }` — the contract a *consumer* installs against, so the satellite reuses the consumer's single `haechi` instance (one crypto/identity surface, no duplicate copies).
28
+ - `"devDependencies": { "haechi": "*" }` — the mechanism that makes npm link the **local workspace** during monorepo development/CI instead of resolving the peer range from the registry. `npm pack` strips devDependencies from the published tarball, so the consumer-facing manifest carries only the peer range — the `*` devDep is invisible downstream. (A repo/source scanner reading `satellites/*/package.json` will still see it; that source-vs-artifact difference is expected and harmless.)
29
+ - **Consumer peer-mismatch behavior:** installing a published satellite against an *incompatible* `haechi` (e.g. `haechi@0.7.0` already present) yields an npm `ERESOLVE` **warning**, not a hard failure; the consumer must upgrade `haechi` (or `--legacy-peer-deps` at their own risk). The satellite does not function correctly against an out-of-range core.
30
+ - Satellites import core by subpath (`haechi/crypto`, `haechi/auth`, `haechi/runtime`); these resolve through the workspace symlink in dev and through the consumer's installed `haechi` in production.
31
+ - `examples/crypto-kms-reference/` is **promoted** to `satellites/crypto-kms/`. The reference example inlined `canonicalize` because its nested `package.json` could not self-resolve `haechi/crypto` pre-workspaces; **under workspaces that import resolves**, so the satellite imports `canonicalize` from `haechi/crypto` rather than carrying a copy (avoiding AAD-canonicalization drift between core and satellite). The old `examples/` directory keeps a short README pointing at the published package. A conformance test asserts the satellite's AAD canonicalization is **byte-for-byte identical** to `haechi/crypto` (not merely semantically equivalent).
32
+ - **Lock file:** converting to workspaces regenerates `package-lock.json` with workspace-resolved entries (including the root self-member). The regenerated lock file is committed; CI uses `npm ci` (which fails on a stale/missing lock), so the conversion PR must commit the fresh lock.
33
+
34
+ **CI strategy (avoid double-runs):** root CI runs `node --test` directly from the root, which discovers `satellites/**/*.test.mjs` automatically (workspace symlinks make their `haechi/*` imports resolve). CI does **not** use `npm test --workspaces` (which would recurse into the root self-member and re-run the suite). Each satellite keeps its own `test` script for isolated local runs (`npm test -w haechi-crypto-kms`) only. Verified: root `node --test` runs core + satellite tests once, and the `node_modules/haechi → ..` symlink cycle does not hang the runner (node skips `node_modules`).
35
+
36
+ **Honest packaging note (was a "byte-stable" claim):** the root tarball is **not** byte-identical to 0.7.0 — `package.json` gains the `workspaces` field and the version bumps, which is expected for any release. The defensible, tested claim is narrower and is enforced by a CI gate (see §6.1): **(a) no satellite files appear in the `haechi` tarball, and (b) the `haechi` tarball's own `package.json` declares zero runtime `dependencies`.** The gate inspects the **packed manifest** (extract `package.json` from `npm pack` output and assert `dependencies` is empty/undefined) — not the installed `node_modules` SBOM, which would pass vacuously today and miss a future runtime-dep leak.
37
+
38
+ ### 2.2 Unscoped `haechi-*` names + per-package trusted publishing
39
+
40
+ - **Naming (decision 2026-06-10):** the `@haechi` npm org/scope is already taken by a third party, so satellites are published as **unscoped `haechi-*`** names — `haechi-crypto-kms`, `haechi-auth-jwt` (both verified free on npm). This needs **no npm org**, matches the unscoped core `haechi`, and each name is reserved + Trusted-Publisher-bound individually. (Trade-off vs a scope: no namespace grouping/defence; the `haechi-` prefix is the convention.)
41
+ - Each satellite is published with the **same OIDC trusted-publishing + sigstore + SHA256SUMS** path proven in 0.7 — its own npmjs.com Trusted Publisher link and a tag-triggered publish workflow.
42
+ - **Satellite `package.json` requirements (do not inherit from root):** each satellite still sets its own `"publishConfig": { "access": "public", "provenance": true }`. Unscoped packages are public by default, so `access: public` is belt-and-suspenders; `provenance: true` and not inheriting the root's `publishConfig` are the load-bearing parts. Post-publish, the runbook verifies `npm view haechi-<pkg> access` reports `public`.
43
+ - **Tag namespacing + workflow guards (avoid mis-triggers and collisions):**
44
+ - Core release tags: `v<semver>` (e.g. `v0.8.0`). The root publish workflow triggers on `push: tags: ['v[0-9]*.[0-9]*.[0-9]*']`. Because GitHub's tag glob treats `.` literally and `[0-9]*` loosely (it would also match `v1.2.3.4` or `v1a.2.3`), the workflow **re-validates** the tag against a strict `^v[0-9]+\.[0-9]+\.[0-9]+$` regex in a pre-publish step and fails closed on a non-match.
45
+ - Satellite tags are **prefixed**: `crypto-kms-v<semver>`, `auth-jwt-v<semver>`. Each satellite workflow triggers only on its own prefix glob and likewise re-validates against `^<prefix>-v[0-9]+\.[0-9]+\.[0-9]+$`.
46
+ - Each workflow re-asserts the package directory it publishes (`npm publish -w <dir>`) so a mistagged push can't publish the wrong package. The Trusted Publisher on npmjs.com is bound to a **specific workflow filename** — renaming the workflow without updating the npm config breaks OIDC auth (documented as a failure mode in the runbook, with a package→workflow-filename→tag-glob mapping table).
47
+ - **Independent semver** per satellite (a satellite patch never bumps core). Satellites start at `0.1.0`. **Pre-1.0 contract:** satellites follow standard npm semver where a `0.x` **minor** bump may carry breaking changes; consumers should pin `major.minor` (e.g. `haechi-crypto-kms@~0.1`). Satellites are pre-stable until their own `1.0.0`.
48
+ - **Bootstrapping the first publish (no org needed):** order per satellite — (1) on npmjs.com, **configure the Trusted Publisher** for the (not-yet-published) unscoped name, linking the repo + exact workflow filename; (2) push the satellite's first tag → the workflow's OIDC publish creates `0.1.0` with provenance and **claims the name** on first publish. No manual `npm publish` from a laptop is required (matches the 0.7 trusted-publishing posture). Because the names are unscoped and currently free, there is no org-membership prerequisite.
49
+
50
+ ### 2.3 `haechi-crypto-kms` (publish + real KMS client)
51
+
52
+ - Promote the 0.7 reference (`createKmsCryptoProvider` envelope encryption + `createInMemoryKms`) into the published package, switching its inlined `canonicalize` to `import { canonicalize } from "haechi/crypto"` (§2.1). The existing `kms` client interface (`keyId` / `wrap` / `unwrap` / `deriveHmacKey`) is **unchanged**, so the promoted provider and in-memory client stay byte-for-byte and their 0.7 tests carry over.
53
+ - Add a **real AWS KMS client** at `haechi-crypto-kms/aws`: `createAwsKmsClient({ keyId, region, client, hmacRootCiphertext })`. It implements the same `kms` interface: `wrap` = KMS `Encrypt` of a CSPRNG-generated 32-byte data key, `unwrap` = KMS `Decrypt` (envelope encryption — the master key never leaves KMS); `deriveHmacKey(domain)` = **HKDF-SHA256** over a single KMS-`Decrypt`ed 32-byte root (`hmacRootCiphertext`, cached), domain-separated — deterministic with no per-token network call. With no `hmacRootCiphertext`, `deriveHmacKey` throws and the provider is encrypt-only (valid via `requireHmac:false`).
54
+ - **`@aws-sdk/client-kms` is an OPTIONAL peer dependency, not a hard dependency** (decision 2026-06-10, revising the earlier "satellite's own dependency" wording). It is imported **lazily** only when no `client` is injected, so: the monorepo `npm ci`/CI never pulls the (large) AWS SDK; consumers on the in-memory or an injected client never install it; and `core` is trivially unaffected. The published satellite declares it under `peerDependencies` + `peerDependenciesMeta.optional`. This keeps the satellite dependency-light while still offering a real backend.
55
+ - The satellite's CI runs `assertCryptoProviderConformance` (imported from `haechi/crypto` via the workspace symlink) against the in-memory client **and** the AWS client driven by an **injected mock** of the two KMS ops (`encrypt`/`decrypt`) — **no SDK, no network**. The mock is a faithful envelope (AES-256-GCM under a per-mock master key): `Decrypt` returns the plaintext only for a blob this key wrapped and **rejects** a blob wrapped by a different key (cross-key isolation) or a corrupted blob. A trivial always-succeeds stub is insufficient; the suite exercises these rejection paths plus HMAC determinism/domain-separation. Live `createAwsKmsClient` validation against a sandbox KMS key is an **out-of-CI integration test** (documented, not gating).
56
+
57
+ ### 2.4 `haechi-auth-jwt` (JWKS bearer verification, dependency-light)
58
+
59
+ `createJwtAuthProvider({ issuer, audience, jwksUri, cryptoProvider, algorithms, clockSkewSeconds, claimMappings })` implements the `authProvider` contract for a **headless** gateway. It is implementable with `node:` builtins only (no `jose`): JWKS fetched via global `fetch`, JWK→key via `crypto.createPublicKey({ key: jwk, format: "jwk" })`, signatures verified via `crypto.verify`.
60
+
61
+ **Implementation note — ES256 signature encoding (verified):** a JWS ES256 signature is raw `R‖S` (IEEE-P1363, 64 bytes for P-256), but `node:crypto.verify` defaults to **DER** for EC keys and returns `false` for a raw signature — silently rejecting every valid ES256 token. The verifier MUST pass `dsaEncoding: "ieee-p1363"` for the EC algorithms. (Empirically confirmed: default DER ⇒ `false`; `ieee-p1363` ⇒ `true`.) This is an acceptance criterion, not an option.
62
+
63
+ **Security spec (mandatory — these are acceptance criteria, not options).** Concrete constants below are *decisions*, not implementation discretion.
64
+
65
+ - **Algorithm selection is server-side, never from the token.** The verifier picks the algorithm from the configured `algorithms` allowlist (default `["RS256","ES256"]`) and the JWK's `kty`/`crv`. The token's `alg` header is checked for *membership* in the allowlist **before** key selection; it never selects the verification routine.
66
+ - **Reject `alg: "none"`** unconditionally.
67
+ - **Block alg-confusion:** never feed an RSA public key into an HMAC verify. HMAC family (`HS*`) is **not allowed** by default; a JWKS-sourced public key is only ever used with its matching asymmetric algorithm.
68
+ - **`kid` is required**; the signing key is selected by `kid` from the JWKS, not by trying every key.
69
+ - **RSA key-strength floor:** an RSA JWK with modulus `< 2048` bits is rejected as invalid.
70
+ - **JWK usage intent:** if a JWK carries `use`, it must be `sig`; if it carries `key_ops`, it must include `verify`/`sign` and must not include `encrypt`/`decrypt`. Otherwise reject.
71
+ - **Header `typ` / no JWE:** if `typ` is present it must be `JWT`; encrypted (JWE) tokens are rejected unconditionally — only JWS is accepted.
72
+ - **Claims are mandatory and fully validated:**
73
+ - `iss` must equal the configured `issuer`.
74
+ - `aud` (token) may be a string or an array of strings (RFC 7519); the configured `audience` must equal the string or be a member of the array — exact, case-sensitive match.
75
+ - `sub` is required and must be a non-empty string (it is the input to `subjectHash`).
76
+ - `exp` and `nbf` are **required**. `exp`: reject when `now > exp + clockSkewSeconds`. `nbf`: reject when `now < nbf - clockSkewSeconds`. A token missing `exp` is rejected. `iat` is sanity-checked if present.
77
+ - **`clockSkewSeconds`** default `60`, **maximum `300`** — construction rejects a value `> 300` (a larger skew would gut expiry validation).
78
+ - **JWKS fetching is SSRF-hardened:**
79
+ - `issuer` must be a valid **HTTPS URL**; `jwksUri` must be HTTPS and its **hostname must exactly equal the `issuer` hostname** (port excluded). 0.8 supports **single-origin issuers only** — IdPs that serve JWKS from a different host than the issuer identifier (some CDN-fronted setups) are explicitly out of scope for 0.8 and rejected at construction. A non-URL issuer (URN-style) is rejected at construction with a clear error.
80
+ - Requests to private/loopback/link-local ranges and cloud-metadata endpoints are refused: `127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16` (incl. `169.254.169.254`), `fe80::/10`. Refusal is at fetch time after DNS resolution (guard against rebinding), with a fetch timeout.
81
+ - **JWKS response bounds:** reject a response body `> 1 MiB`; `JSON.parse` is guarded against pathological nesting (depth bound). JWT segments are decoded as **strict base64url** (`[A-Za-z0-9_-]`, no padding) before `JSON.parse`.
82
+ - **JWKS cache is bounded and DoS-resistant:** keys are cached with a TTL; an **unknown `kid` does not trigger an unbounded refetch** — at most one full JWKS refresh per **60 s cooldown**, so a flood of forged `kid`s can't become a fetch storm against the IdP.
83
+ - **Identity is PII-safe (fail-closed):** `cryptoProvider` is **required** and must expose `hmac`; if absent, the provider constructor throws (it cannot produce a PII-safe identity). `subjectHash` / `issuerHash` are keyed **HMAC-SHA-256** (`haechi:identity:hash:v1`), hex-encoded (64 chars) — raw `sub` / `iss` are never stored or logged. `scopes` come from a configured scope claim (`scp` / `scope`); `labels` from an allowlisted claim mapping only.
84
+ - **Fail-closed everywhere:** any verification error → `authenticate` returns `null` (deny), never throws into the request path; no token detail is echoed to the client.
85
+
86
+ Wired via **injection** (`createRuntime(config, { authProvider: createJwtAuthProvider(...) })`); `auth.provider: external`. Dynamic loading stays banned until the 1.0 plugin sandbox.
87
+
88
+ ### 2.5 Release process for the monorepo
89
+
90
+ - The existing root workflow keeps publishing `haechi` (guarded to the `v*` tag glob + strict regex re-validation).
91
+ - Each satellite has its own prefixed-tag publish workflow reusing the 0.7 signed-artifacts steps (pack → checksum → attest → publish → upload), scoped to its own directory, each bound to its own Trusted Publisher (specific workflow filename).
92
+ - The per-package release runbook (`release-process.md`) documents: tag conventions + the package→workflow-filename→tag-glob mapping table, the Trusted Publisher bootstrap order (reserve → configure → tag), the workflow-rename failure mode, and post-publish verification (provenance, `npm view ... access`).
93
+
94
+ ## 3. Explicit non-scope (deferred to 0.9+)
95
+
96
+ - `haechi-dashboard` read-only audit viewer (UI build, its own tech-stack decision).
97
+ - `haechi-auth-oidc` full interactive OIDC (authorization-code flow) — `haechi-auth-jwt` covers the headless case first.
98
+ - `haechi-auth-jwt` multi-origin/CDN-fronted JWKS (issuer host ≠ JWKS host).
99
+ - `haechi-classifier-*` ML/heuristic classifier plugins.
100
+ - `haechi-crypto-kms` Vault/GCP/Azure backends (AWS only in 0.8).
101
+ - Dynamic loading of satellites (1.0 plugin sandbox).
102
+
103
+ The risk-register and roadmap are updated to move `haechi-auth-oidc` and `haechi-dashboard` out of the 0.8 row and into a new **0.9** row, so the public docs match this scope.
104
+
105
+ ## 4. Backward compatibility
106
+
107
+ Core behavior is unchanged: the root package's `exports`, `bin`, `files`, and zero-dep runtime posture are identical. Adding `workspaces` (including the `"."` self-entry) to the root `package.json` is inert for anyone installing `haechi` as a single dependency. Existing config and APIs are untouched; satellites are purely additive and opt-in.
108
+
109
+ ## 5. 1.0 relationship
110
+
111
+ 0.8 does not itself close a 1.0 blocker, but it **realizes operational key custody as an installable, attested package** (`haechi-crypto-kms`) and proves the satellite model end-to-end. The remaining 1.0 gates stay: API-stability freeze and plugin sandbox + real-environment validation.
112
+
113
+ ## 6. Test criteria (mapped to the PR breakdown)
114
+
115
+ ### 6.1 PR1 — workspaces conversion (no new published package)
116
+
117
+ - Root `npm install` exits 0 with **no ERESOLVE/ETARGET**; `node_modules/haechi` is the workspace symlink; the committed `package-lock.json` makes `npm ci` succeed on a fresh checkout.
118
+ - A satellite test that does `import { ... } from "haechi/crypto"` runs green under root `node --test`.
119
+ - **No-leak + zero-dep gate:** core `npm pack --dry-run` contains **no `satellites/` paths**; the **packed** `haechi` `package.json` (extracted from the tarball) has empty/undefined `dependencies`. The gate is **negatively tested**: temporarily adding `satellites/` to core's `files`, or a runtime dep to core's `package.json`, makes the gate fail with a clear error (so the gate isn't a vacuous pass).
120
+ - In-memory crypto provider (promoted 0.7 code) passes `assertCryptoProviderConformance` through the workspace symlink, including the byte-for-byte `canonicalize` parity check vs `haechi/crypto`.
121
+
122
+ ### 6.2 PR2 — `haechi-crypto-kms` (real AWS client)
123
+
124
+ - In-memory **and AWS** clients (the AWS one driven by an **injected mock** of the KMS `encrypt`/`decrypt` ops — no SDK, no network) pass `assertCryptoProviderConformance`, including the cross-key/corrupted-blob **rejection** paths and HMAC determinism/domain-separation; end-to-end through `createRuntime` (encrypt + tokenization round-trip).
125
+ - `createAwsKmsClient` without a `keyId` throws; with no `hmacRootCiphertext`, `deriveHmacKey` throws and the provider passes conformance as encrypt-only (`requireHmac:false`).
126
+ - The published manifest sets `publishConfig.access: public` and declares `@aws-sdk/client-kms` under `peerDependencies` + `peerDependenciesMeta.optional` (NOT a runtime `dependency`); the published satellite tarball has `dependencies: {}`, and core's tarball stays zero-dep (the §6.1 gate still passes).
127
+ - A satellite publish workflow (`crypto-kms-v<semver>`) exists with the 0.7 signed-artifacts path; the core workflow is guarded so a satellite release tag never publishes `haechi`.
128
+
129
+ ### 6.3 PR3 — `haechi-auth-jwt` (security gates)
130
+
131
+ - A valid RS256/ES256 JWT (test-key-signed, stub JWKS) authenticates into a PII-safe identity with **no raw `sub` in the audit**; `subjectHash`/`issuerHash` are 64-hex-char HMAC-SHA-256.
132
+ - Each of the following is **denied**: `alg:"none"`; an `HS256` token forged with the RSA public key (alg-confusion); a JWE/`typ` mismatch; expired (`exp`); not-yet-valid (`nbf`); missing `exp`; missing/empty `sub`; wrong-`aud` (string and array forms); wrong-`iss`; unknown-`kid`; bad-signature; an RSA JWK `< 2048` bits; a JWK with `use:"enc"`/`key_ops:["encrypt"]`.
133
+ - Construction rejects: a non-HTTPS or cross-origin `jwksUri`; a non-URL `issuer`; `clockSkewSeconds > 300`; a missing `cryptoProvider.hmac`. A `jwksUri` resolving to `127.0.0.1`, `169.254.169.254`, `::1`, or any RFC1918 CIDR is rejected.
134
+ - An unknown-`kid` flood triggers **exactly one** JWKS refetch within the 60 s cooldown; a JWKS response `> 1 MiB` is rejected.
135
+
136
+ ### 6.4 All satellites
137
+
138
+ - Each publishes with provenance + sigstore attestation, verified post-release like 0.7.
139
+
140
+ ## 7. Suggested PR breakdown (stacked)
141
+
142
+ 1. **Workspaces conversion** (no new published package): root `workspaces: [".", "satellites/*"]`, bump root to **0.8.0**, move `crypto-kms` to `satellites/crypto-kms/` with `peer + dev` core deps, switch the inlined `canonicalize` to `haechi/crypto` (+ parity test), repoint tests, commit the regenerated `package-lock.json`, add the **no-leak + zero-dep CI gate** (with negative tests), root CI runs all workspace tests via root `node --test`. → §6.1
143
+ 2. **`haechi-crypto-kms`:** real AWS KMS client (satellite-only `@aws-sdk/client-kms` dep) + faithful mocked-AWS conformance CI + `publishConfig` + prefixed-tag publish workflow (strict regex guard) + Trusted Publisher bootstrap. → §6.2
144
+ 3. **`haechi-auth-jwt`:** JWKS verification provider implementing the full §2.4 security spec + identity mapping + the §6.3 security-gate tests + `publishConfig` + prefixed-tag publish workflow. → §6.3
145
+ 4. **0.8.0 release cut:** docs EN/KO, packaging/roadmap/risk-register (move OIDC+dashboard to 0.9)/api-stability, wiki, npm org / Trusted Publisher runbook (mapping table + bootstrap order + failure modes).
@@ -63,12 +63,43 @@ npm audit signatures
63
63
 
64
64
  ## 4. GitHub Actions
65
65
 
66
- | Workflow | 목적 |
67
- |---|---|
68
- | `.github/workflows/ci.yml` | test, release preflight, SBOM artifact |
69
- | `.github/workflows/npm-publish.yml` | GitHub release published 이벤트에서 npm provenance publish + 체크섬/증명 release 자산 |
66
+ | Workflow | 배포 대상 | 트리거 태그 | 목적 |
67
+ |---|---|---|---|
68
+ | `.github/workflows/ci.yml` | — | 모든 push/PR | test, release preflight, SBOM artifact |
69
+ | `.github/workflows/npm-publish.yml` | `haechi` | `v<semver>` | npm provenance publish + 체크섬/증명 release 자산 |
70
+ | `.github/workflows/crypto-kms-publish.yml` | `haechi-crypto-kms` | `crypto-kms-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
71
+ | `.github/workflows/auth-jwt-publish.yml` | `haechi-auth-jwt` | `auth-jwt-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
70
72
 
71
- ## 5. 배포 차단 조건
73
+ publish 워크플로는 `release: published`에서 트리거되지만 **가드**되어 둘이 교차 발화하지 않는다: core job은 `v`로 시작하는 태그에서만 실행되고(그리고 `^v[0-9]+\.[0-9]+\.[0-9]+$` 재검증), satellite job은 `crypto-kms-v…`에서만 실행된다(그리고 `^crypto-kms-v[0-9]+\.[0-9]+\.[0-9]+$` 재검증 **및** 태그 버전이 satellite `package.json` 버전과 일치하는지 검증). npmjs.com Trusted Publisher는 각 패키지의 **특정 워크플로 파일명**에 바인딩된다 — 워크플로 파일 rename은 npm 설정을 갱신할 때까지 OIDC publish를 깨뜨린다.
74
+
75
+ ## 5. Satellite 패키지 (unscoped `haechi-*`)
76
+
77
+ Satellite는 npm workspaces 모노레포의 `satellites/*`에 살며 core와 **독립적으로** 발행된다(자체 semver; satellite patch가 `haechi`를 bump하지 않음). core와 동일한 서명 아티팩트 경로를 재사용한다(pack → checksum → sigstore attest → OIDC publish → upload). **unscoped** `haechi-*` 이름으로 발행하므로(`@haechi` org/scope는 제3자가 점유) **npm org가 필요 없다**.
78
+
79
+ **satellite별 부트스트랩 순서(첫 발행, org 불필요):**
80
+
81
+ 1. npmjs.com에서 (아직 미발행) unscoped 이름(예: `haechi-crypto-kms`)에 **Trusted Publisher 설정**: `raeseoklee/haechi` 저장소와 satellite의 **정확한 워크플로 파일명**(예: `crypto-kms-publish.yml`) 연결. npm은 아직 발행 전인 이름에도 Trusted Publisher 설정을 허용한다.
82
+ 2. 접두사 태그를 push하고 GitHub Release 발행(예: `crypto-kms-v0.1.0`) → 워크플로의 OIDC publish가 provenance와 함께 `0.1.0`을 생성하고 첫 발행 시 이름을 확보.
83
+
84
+ 노트북에서의 수동 `npm publish`는 필요 없다. 이름이 unscoped이고 비어있으므로 org-membership 선행 요건이 없다.
85
+
86
+ **태그 → 워크플로 → 패키지 매핑:**
87
+
88
+ | 패키지 | 태그 패턴 | 워크플로 파일 | npm 버전 소스 |
89
+ |---|---|---|---|
90
+ | `haechi-crypto-kms` | `crypto-kms-v<semver>` | `crypto-kms-publish.yml` | `satellites/crypto-kms/package.json` |
91
+ | `haechi-auth-jwt` | `auth-jwt-v<semver>` | `auth-jwt-publish.yml` | `satellites/auth-jwt/package.json` |
92
+
93
+ **satellite 릴리스 검증** (core와 동일한 신뢰 앵커):
94
+
95
+ ```bash
96
+ gh attestation verify haechi-crypto-kms-<version>.tgz --repo raeseoklee/haechi
97
+ npm view haechi-crypto-kms --json # dist.attestations 존재 확인; access "public"
98
+ ```
99
+
100
+ **의존성 노트:** `haechi-crypto-kms`는 core를 zero-dependency로 유지한다 — `@aws-sdk/client-kms`는 **optional peer dependency**이며, 실제 AWS 클라이언트를 쓰고 주입하지 않을 때만 lazy import된다. in-memory 또는 주입형 클라이언트를 쓰는 소비자는 SDK를 설치하지 않는다.
101
+
102
+ ## 6. 배포 차단 조건
72
103
 
73
104
  다음 중 하나라도 실패하면 npm publish를 하지 않는다.
74
105
 
@@ -63,12 +63,43 @@ npm audit signatures
63
63
 
64
64
  ## 4. GitHub Actions
65
65
 
66
- | Workflow | Purpose |
67
- |---|---|
68
- | `.github/workflows/ci.yml` | Tests, release preflight, SBOM artifact |
69
- | `.github/workflows/npm-publish.yml` | npm provenance publish + checksummed/attested release assets on GitHub release published |
66
+ | Workflow | Publishes | Fires on tag | Purpose |
67
+ |---|---|---|---|
68
+ | `.github/workflows/ci.yml` | — | any push/PR | Tests, release preflight, SBOM artifact |
69
+ | `.github/workflows/npm-publish.yml` | `haechi` | `v<semver>` | npm provenance publish + checksummed/attested release assets |
70
+ | `.github/workflows/crypto-kms-publish.yml` | `haechi-crypto-kms` | `crypto-kms-v<semver>` | satellite publish, same signed-artifacts path |
71
+ | `.github/workflows/auth-jwt-publish.yml` | `haechi-auth-jwt` | `auth-jwt-v<semver>` | satellite publish, same signed-artifacts path |
70
72
 
71
- ## 5. Deployment block conditions
73
+ Each publish workflow triggers on `release: published` but is **guarded** so the two never cross-fire: the core job runs only for tags starting `v` (and re-validates `^v[0-9]+\.[0-9]+\.[0-9]+$`); the satellite job runs only for `crypto-kms-v…` (and re-validates `^crypto-kms-v[0-9]+\.[0-9]+\.[0-9]+$` **and** that the tag version equals the satellite's `package.json` version). The npmjs.com Trusted Publisher for each package is bound to its **specific workflow filename** — renaming a workflow file breaks its OIDC publish until the npm config is updated.
74
+
75
+ ## 5. Satellite packages (unscoped `haechi-*`)
76
+
77
+ Satellites live under `satellites/*` in the npm workspaces monorepo and publish **independently** of core (their own semver; a satellite patch never bumps `haechi`). They reuse the exact signed-artifacts path as core (pack → checksum → sigstore attest → OIDC publish → upload). They are published as **unscoped** `haechi-*` names (the `@haechi` org/scope is taken by a third party), so **no npm org is required**.
78
+
79
+ **Per-satellite bootstrap order (first publish, no org needed):**
80
+
81
+ 1. On npmjs.com, **configure a Trusted Publisher** for the (not-yet-published) unscoped name (e.g. `haechi-crypto-kms`): link the `raeseoklee/haechi` repository and the satellite's **exact workflow filename** (e.g. `crypto-kms-publish.yml`). npm allows configuring a Trusted Publisher for a name you have not published yet.
82
+ 2. Push the prefixed tag and publish a GitHub Release (e.g. `crypto-kms-v0.1.0`) → the workflow's OIDC publish creates `0.1.0` with provenance and claims the name on first publish.
83
+
84
+ No manual `npm publish` from a laptop is needed. Because the names are unscoped and free, there is no org-membership prerequisite.
85
+
86
+ **Tag → workflow → package mapping:**
87
+
88
+ | Package | Tag pattern | Workflow file | npm version source |
89
+ |---|---|---|---|
90
+ | `haechi-crypto-kms` | `crypto-kms-v<semver>` | `crypto-kms-publish.yml` | `satellites/crypto-kms/package.json` |
91
+ | `haechi-auth-jwt` | `auth-jwt-v<semver>` | `auth-jwt-publish.yml` | `satellites/auth-jwt/package.json` |
92
+
93
+ **Verify a satellite release** (same anchors as core):
94
+
95
+ ```bash
96
+ gh attestation verify haechi-crypto-kms-<version>.tgz --repo raeseoklee/haechi
97
+ npm view haechi-crypto-kms --json # dist.attestations present; access "public"
98
+ ```
99
+
100
+ **Dependency note:** `haechi-crypto-kms` keeps core zero-dependency — `@aws-sdk/client-kms` is an **optional peer dependency**, imported lazily only when a real AWS client is used and not injected. Consumers who use the in-memory or an injected client never install the SDK.
101
+
102
+ ## 6. Deployment block conditions
72
103
 
73
104
  npm publish is not performed if any of the following fail.
74
105
 
@@ -130,6 +130,8 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
130
130
  | 0.5.0 ✅ | streaming hardening | 2026-06-10 출시: bounded cross-frame 버퍼를 사용한 SSE/NDJSON 스트리밍 응답 검사(`streaming.requestMode: inspect`). stream sequence AAD, replay cache, 강화된 원격 배포 가이드는 0.6+으로 이월. `docs/current/release-0.5-implementation-scope.md` 참조 |
131
131
  | 0.6.0 ✅ | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. `docs/current/release-0.6-implementation-scope.md` 참조 |
132
132
  | 0.7.0 ✅ | Shipped 2026-06-10 (PRs #22–#24): audit head-hash anchoring + external sink contract, cryptoProvider contract hardening + `assertCryptoProviderConformance` + reference KMS adapter, 서명/체크섬된 release artifact. `docs/current/release-0.7-implementation-scope.md` 참조 |
133
+ | 0.8.0 (구현 완료; 발행 대기) | ecosystem foundation + satellites | 2026-06-10 구현(PR #27–#29): npm workspaces 모노레포(루트 자기참조 `["."]` + `satellites/*`), `haechi-crypto-kms`(실제 AWS KMS 클라이언트, AWS SDK는 optional peer)와 `haechi-auth-jwt`(헤드리스 JWKS bearer 검증), 각각 provenance-attest 발행 워크플로 보유. core는 zero runtime dependency 유지(CI no-leak + zero-dep + satellite-packaging 게이트). **발행은 운영자가 `@haechi` npm org + 위성별 Trusted Publisher를 생성**해야 진행된다(`docs/current/release-process.md` §5); core `haechi@0.8.0`은 기존 TP로 발행 가능. `docs/current/release-0.8-implementation-scope.md` 참조 |
134
+ | 0.9.0 | observability + interactive auth | `haechi-auth-oidc` 전체 authorization-code flow, `haechi-dashboard` 읽기 전용 audit 뷰어(hash-chain 무결성 표시, 요약/검색/타임라인), `haechi-crypto-kms` 추가 백엔드(Vault/GCP/Azure) |
133
135
  | 1.0.0 | stable API contract | migration policy, long-term audit schema, plugin sandbox/runtime conformance 및 allowlist/manifest 통과 외부 auth/classifier package 동적 로딩 |
134
136
 
135
137
  동적 npm package 로딩은 1.0 plugin sandbox 이전까지 금지한다. 0.4~0.7의 외부 provider는 `createRuntime(config, providers)` 프로그래매틱 주입만 지원한다.
@@ -130,7 +130,8 @@ All checklist items below were completed for 0.3.2 on 2026-06-10 except the prov
130
130
  | 0.5.0 ✅ | Streaming hardening | Shipped 2026-06-10: SSE/NDJSON streaming response inspection with bounded cross-frame buffer (`streaming.requestMode: inspect`). Stream sequence AAD, replay cache, stronger remote deployment guide deferred to 0.6+. See `docs/current/release-0.5-implementation-scope.md` |
131
131
  | 0.6.0 ✅ | Auth and per-client controls | Shipped 2026-06-10 (PRs #17–#19): built-in bearer auth, named policy profiles, model allowlist, request rate limit, PII-safe identity in audit. See `docs/current/release-0.6-implementation-scope.md` |
132
132
  | 0.7.0 ✅ | Ops hardening | Shipped 2026-06-10 (PRs #22–#24): audit head-hash anchoring + external sink contract, cryptoProvider contract hardening + `assertCryptoProviderConformance` + reference KMS adapter, signed/checksummed release artifacts. See `docs/current/release-0.7-implementation-scope.md` |
133
- | 0.8.0 | Ecosystem and observability | npm org (`@haechi/*`), publish `@haechi/crypto-kms` and `@haechi/auth-oidc`, npm workspaces, `@haechi/dashboard` read-only audit viewer (hash-chain integrity display, summary/search/timeline) |
133
+ | 0.8.0 (implemented; publish pending) | Ecosystem foundation + satellites | Implemented 2026-06-10 (PRs #27–#29): npm workspaces monorepo (root self-member `["."]` + `satellites/*`), `haechi-crypto-kms` (real AWS KMS client, AWS SDK as optional peer) and `haechi-auth-jwt` (headless JWKS bearer verification), each with its own provenance-attested publish workflow. Core stays zero runtime dependency (CI no-leak + zero-dep + satellite-packaging gates). **Publish is gated on the operator creating the `@haechi` npm org + per-satellite Trusted Publishers** (`docs/current/release-process.md` §5); core `haechi@0.8.0` can publish on the existing TP. See `docs/current/release-0.8-implementation-scope.md` |
134
+ | 0.9.0 | Observability + interactive auth | `haechi-auth-oidc` full authorization-code flow, `haechi-dashboard` read-only audit viewer (hash-chain integrity display, summary/search/timeline), additional `haechi-crypto-kms` backends (Vault/GCP/Azure) |
134
135
  | 1.0.0 | Stable API contract | Migration policy, long-term audit schema, plugin sandbox/runtime conformance, and dynamic loading of external auth/classifier packages that pass allowlist/manifest |
135
136
 
136
137
  Dynamic npm package loading is prohibited until the 1.0 plugin sandbox. External providers in 0.4–0.7 are supported only via `createRuntime(config, providers)` programmatic injection.
@@ -1,47 +1,13 @@
1
- # `@haechi/crypto-kms` (reference)
1
+ # `haechi-crypto-kms` moved
2
2
 
3
- A reference KMS-backed `cryptoProvider` for Haechi's `keys.provider: external` path. This is the **shape** of the satellite published as `@haechi/crypto-kms` in 0.8 it lives here as a dependency-free reference so core stays zero-runtime-dependency.
3
+ The KMS-backed `cryptoProvider` reference that used to live here has been **promoted to a published satellite**. It now lives in the monorepo at [`satellites/crypto-kms/`](../../satellites/crypto-kms/) and is published as **`haechi-crypto-kms`**.
4
4
 
5
- ## How it works
6
-
7
- Envelope encryption: each `encrypt` generates a fresh AES-256-GCM **data key**, encrypts the plaintext locally, and **wraps the data key with the KMS**. The KMS master key never leaves the KMS. `decrypt` unwraps the data key via the KMS and decrypts locally. `hmac` derives a per-domain key from the KMS, preserving Haechi's domain-separation discipline (tokens, identity, policy bundles).
8
-
9
- The envelope matches Haechi's contract (`v, alg, kid, iv, ct, tag, aadHash`) plus a `wrappedKey`. AAD is canonicalized and bound exactly as the local provider does, so it passes `assertCryptoProviderConformance`.
10
-
11
- ## The KMS client interface
12
-
13
- Inject any client implementing:
14
-
15
- ```js
16
- {
17
- keyId: string,
18
- async wrap(dataKey: Buffer): string, // KMS-encrypt a data key
19
- async unwrap(wrapped: string): Buffer, // KMS-decrypt it
20
- async deriveHmacKey(domain: string): Buffer // KMS-derived per-domain key
21
- }
22
- ```
23
-
24
- `createInMemoryKms()` is a process-local stand-in for examples/tests. A real deployment swaps in an AWS KMS / HashiCorp Vault client (e.g. `@aws-sdk/client-kms` `GenerateDataKey`/`Decrypt`, plus an HKDF for `deriveHmacKey`).
25
-
26
- ## Usage
27
-
28
- In 0.7 this is a repo reference example — import it by relative path
29
- (`./examples/crypto-kms-reference/index.mjs`). From 0.8 it is published as
30
- `@haechi/crypto-kms` and imported by name, as shown below.
31
-
32
- ```js
33
- import { createRuntime } from "haechi/runtime";
34
- import { createKmsCryptoProvider, createInMemoryKms } from "@haechi/crypto-kms";
35
-
36
- const cryptoProvider = createKmsCryptoProvider({ kms: createInMemoryKms() });
37
- const runtime = createRuntime({ keys: { provider: "external" }, /* ... */ }, { cryptoProvider });
5
+ ```sh
6
+ npm install haechi-crypto-kms
38
7
  ```
39
8
 
40
- ## Self-test
41
-
42
9
  ```js
43
- import { assertCryptoProviderConformance } from "haechi/crypto";
44
- await assertCryptoProviderConformance(cryptoProvider); // throws on any contract violation
10
+ import { createKmsCryptoProvider, createInMemoryKms } from "haechi-crypto-kms";
45
11
  ```
46
12
 
47
- This reference is **not a production key provider**; `createInMemoryKms` holds a process-local master key. Use a real KMS client for custody.
13
+ See [`satellites/crypto-kms/README.md`](../../satellites/crypto-kms/README.md) for usage. Core (`haechi`) stays zero-runtime-dependency; the satellite carries any KMS-client dependency itself.
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "haechi",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Experimental developer preview for self-hosted AI context enforcement across LLM, MCP, vLLM, Ollama, and agent traffic.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
+ "workspaces": [
8
+ ".",
9
+ "satellites/*"
10
+ ],
7
11
  "repository": {
8
12
  "type": "git",
9
13
  "url": "git+https://github.com/raeseoklee/haechi.git"
@@ -63,6 +67,8 @@
63
67
  "check:types": "tsc -p jsconfig.json --noEmit",
64
68
  "pack:dry": "npm pack --dry-run",
65
69
  "scan:stale-names": "node scripts/stale-name-scan.mjs",
70
+ "check:packaging": "node scripts/check-core-packaging.mjs",
71
+ "check:satellite-packaging": "node scripts/check-satellite-packaging.mjs",
66
72
  "sbom": "node scripts/generate-sbom.mjs",
67
73
  "checksums": "node scripts/release-checksums.mjs",
68
74
  "bench:payload": "node scripts/bench-payload.mjs",
@@ -117,6 +117,51 @@ export async function buildIdentity(record, cryptoProvider) {
117
117
  };
118
118
  }
119
119
 
120
+ // PII-safe identity builder for EXTERNAL auth providers (e.g. the haechi-auth-jwt
121
+ // satellite). Core owns identity construction so the keyed-HMAC domain and the
122
+ // identity shape stay authoritative here — a satellite supplies raw claims and
123
+ // never sees or stores the IDENTITY_DOMAIN. subject/issuer become keyed HMACs;
124
+ // the raw values are never returned. Throws (fail-closed) on a missing
125
+ // cryptoProvider.hmac, an empty subject/issuer, an invalid type, bad scopes, or
126
+ // a disallowed label.
127
+ export async function buildExternalIdentity(
128
+ { provider, subject, issuer, type = "user", scopes = [], labels = {}, allowedLabelKeys = DEFAULT_ALLOWED_LABEL_KEYS },
129
+ cryptoProvider
130
+ ) {
131
+ if (typeof cryptoProvider?.hmac !== "function") {
132
+ throw new Error("buildExternalIdentity requires a cryptoProvider with hmac()");
133
+ }
134
+ if (!provider || typeof provider !== "string") {
135
+ throw new Error("identity requires a non-empty provider string");
136
+ }
137
+ if (!subject || typeof subject !== "string") {
138
+ throw new Error("identity requires a non-empty subject");
139
+ }
140
+ if (!issuer || typeof issuer !== "string") {
141
+ throw new Error("identity requires a non-empty issuer");
142
+ }
143
+ if (!VALID_IDENTITY_TYPES.has(type)) {
144
+ throw new Error(`Invalid identity type: ${type} (expected user | service | agent)`);
145
+ }
146
+ if (!Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string" && scope.trim())) {
147
+ throw new Error("scopes must be an array of non-empty strings");
148
+ }
149
+ validateLabels(labels, allowedLabelKeys);
150
+
151
+ const subjectHash = await cryptoProvider.hmac({ data: subject, domain: IDENTITY_DOMAIN });
152
+ const issuerHash = await cryptoProvider.hmac({ data: issuer, domain: IDENTITY_DOMAIN });
153
+ return {
154
+ // Non-PII, stable per subject: derived from the keyed subject hash.
155
+ id: `${provider}:${subjectHash.slice(0, 16)}`,
156
+ type,
157
+ subjectHash,
158
+ issuerHash,
159
+ provider,
160
+ scopes,
161
+ labels
162
+ };
163
+ }
164
+
120
165
  function bearerTokenFromRequest(request) {
121
166
  const header = request?.headers?.authorization ?? request?.headers?.Authorization;
122
167
  if (typeof header !== "string") {
@@ -1,133 +0,0 @@
1
- // Reference KMS-backed cryptoProvider for Haechi (keys.provider: external).
2
- //
3
- // This is the *shape* a published @haechi/crypto-kms satellite (0.8) takes. It
4
- // uses envelope encryption: a fresh data key per record encrypts the plaintext
5
- // locally with AES-256-GCM, and the data key is wrapped by the KMS. The master
6
- // key never leaves the KMS. The `kms` client is injected, so this file has zero
7
- // real dependencies — a real adapter swaps createInMemoryKms() for an AWS KMS /
8
- // HashiCorp Vault client implementing the same small interface.
9
- //
10
- // Inject it: createRuntime(config, { cryptoProvider: createKmsCryptoProvider({ kms }) })
11
- // and set keys.provider: "external".
12
-
13
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
14
-
15
- // The published @haechi/crypto-kms satellite imports this from `haechi/crypto`;
16
- // it is inlined here so the reference example is fully self-contained (no
17
- // cross-package import) and matches Haechi's canonical AAD exactly.
18
- function canonicalize(value) {
19
- if (Array.isArray(value)) {
20
- return `[${value.map((item) => canonicalize(item)).join(",")}]`;
21
- }
22
- if (value && typeof value === "object") {
23
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
24
- }
25
- return JSON.stringify(value);
26
- }
27
-
28
- const ALG = "AES-256-GCM";
29
- const HMAC_KEY_DOMAIN = "haechi:crypto-kms:hmac-root:v1";
30
-
31
- // The injected KMS client must implement:
32
- // keyId: string
33
- // async wrap(dataKey: Buffer) -> string (KMS-encrypt a data key)
34
- // async unwrap(wrapped: string) -> Buffer (KMS-decrypt it back)
35
- // async deriveHmacKey(domain: string) -> Buffer (KMS-derived per-domain key)
36
- export function createKmsCryptoProvider({ kms }) {
37
- if (!kms || typeof kms.wrap !== "function" || typeof kms.unwrap !== "function" || typeof kms.deriveHmacKey !== "function") {
38
- throw new Error("createKmsCryptoProvider requires a kms client with wrap/unwrap/deriveHmacKey");
39
- }
40
-
41
- function sha256(value) {
42
- // Plain SHA-256, matching Haechi's core aadHash (defence-in-depth; GCM
43
- // already authenticates the AAD via the tag).
44
- return createHash("sha256").update(value).digest("base64url");
45
- }
46
-
47
- return {
48
- id: "haechi.crypto.kms-reference",
49
- version: "0.1.0",
50
- capabilities: {
51
- readsPlaintext: true,
52
- networkEgress: true, // a real KMS adapter calls out to the KMS
53
- keyCustody: "external-kms"
54
- },
55
- async encrypt({ plaintext, aad }) {
56
- const dataKey = randomBytes(32);
57
- const iv = randomBytes(12);
58
- const cipher = createCipheriv("aes-256-gcm", dataKey, iv);
59
- const aadBytes = Buffer.from(canonicalize(aad), "utf8");
60
- cipher.setAAD(aadBytes);
61
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
62
- const tag = cipher.getAuthTag();
63
- return {
64
- v: 1,
65
- alg: ALG,
66
- kid: kms.keyId,
67
- iv: iv.toString("base64url"),
68
- ct: ciphertext.toString("base64url"),
69
- tag: tag.toString("base64url"),
70
- wrappedKey: await kms.wrap(dataKey),
71
- aadHash: sha256(aadBytes)
72
- };
73
- },
74
- async decrypt({ envelope, aad }) {
75
- if (envelope.alg && envelope.alg !== ALG) {
76
- throw new Error(`Unsupported algorithm: ${envelope.alg}`);
77
- }
78
- const aadBytes = Buffer.from(canonicalize(aad), "utf8");
79
- if (envelope.aadHash && envelope.aadHash !== sha256(aadBytes)) {
80
- throw new Error("AAD hash mismatch");
81
- }
82
- const dataKey = await kms.unwrap(envelope.wrappedKey);
83
- const decipher = createDecipheriv("aes-256-gcm", dataKey, Buffer.from(envelope.iv, "base64url"));
84
- decipher.setAAD(aadBytes);
85
- decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
86
- return Buffer.concat([
87
- decipher.update(Buffer.from(envelope.ct, "base64url")),
88
- decipher.final()
89
- ]).toString("utf8");
90
- },
91
- async hmac({ data, domain }) {
92
- if (!domain || typeof domain !== "string") {
93
- throw new Error("hmac requires a non-empty domain string");
94
- }
95
- // Domain-separated: derive a per-domain key from the KMS, then HMAC.
96
- const derived = await kms.deriveHmacKey(domain);
97
- return createHmac("sha256", derived).update(data).digest("hex");
98
- }
99
- };
100
- }
101
-
102
- // In-memory stand-in for AWS KMS / Vault — for examples and tests only. A real
103
- // deployment injects a client backed by the cloud KMS.
104
- //
105
- // WARNING: the default masterKey is a fresh random key PER PROCESS. Anything
106
- // encrypted in one run cannot be decrypted in the next — exactly the silent
107
- // data-loss footgun that key rotation must avoid. For any persistence across
108
- // restarts, supply a stable `masterKey` (or, in production, use a real KMS that
109
- // holds the master key). This fake is NOT a production key provider.
110
- export function createInMemoryKms({ keyId = "kms-ref-local", masterKey = randomBytes(32) } = {}) {
111
- return {
112
- keyId,
113
- async wrap(dataKey) {
114
- const iv = randomBytes(12);
115
- const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
116
- const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
117
- const tag = cipher.getAuthTag();
118
- return Buffer.concat([iv, tag, ct]).toString("base64url");
119
- },
120
- async unwrap(wrapped) {
121
- const buffer = Buffer.from(wrapped, "base64url");
122
- const iv = buffer.subarray(0, 12);
123
- const tag = buffer.subarray(12, 28);
124
- const ct = buffer.subarray(28);
125
- const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
126
- decipher.setAuthTag(tag);
127
- return Buffer.concat([decipher.update(ct), decipher.final()]);
128
- },
129
- async deriveHmacKey(domain) {
130
- return createHmac("sha256", masterKey).update(`${HMAC_KEY_DOMAIN}:${domain}`).digest();
131
- }
132
- };
133
- }
@@ -1,19 +0,0 @@
1
- {
2
- "name": "@haechi/crypto-kms",
3
- "version": "0.0.0-reference",
4
- "private": true,
5
- "description": "Reference KMS-backed cryptoProvider for Haechi (keys.provider: external). Promoted to a published @haechi/* satellite in 0.8.",
6
- "type": "module",
7
- "exports": {
8
- ".": "./index.mjs"
9
- },
10
- "peerDependencies": {
11
- "haechi": ">=0.7.0"
12
- },
13
- "optionalDependencies": {
14
- "@aws-sdk/client-kms": "^3"
15
- },
16
- "engines": {
17
- "node": ">=22"
18
- }
19
- }