diff --git a/.ai/rules/bun-first.md b/.ai/rules/bun-first.md deleted file mode 100644 index 4e2e0f8..0000000 --- a/.ai/rules/bun-first.md +++ /dev/null @@ -1,46 +0,0 @@ -# Bun-first Policy - -## Runtime Priority - -1. **Bun built-in / Bun runtime API** — highest -2. Node.js standard API — only when Bun lacks support -3. npm packages — only when Bun and Node cannot solve it -4. Custom implementation — last resort - -## Scope - -Applies to **every** case where Node.js API, npm package, or custom implementation is considered. -No exception for size or perceived importance. A one-liner utility still requires Bun-alternative verification. - -## Verification Flow - -1. About to use Node.js / npm / custom implementation → **STOP.** -2. Search Bun official documentation for an equivalent Bun API. Follow `.ai/rules/search-policy.md` lookup priority. -3. Bun alternative **exists** → use it. -4. Bun alternative **confirmed absent** (with search evidence) → present the evidence + proposed alternative → obtain `ㅇㅇ`. - -**Selecting Node/npm without search verification is a policy violation.** - -## Required Output Block (hard gate) - -When this rule triggers, the response MUST contain the following block **before any code that uses the chosen API**. -Absence of this block = decision not made = code using Node/npm/custom is **prohibited**. - -``` -[Bun-first Check] -- API/module considered: (e.g., node:url fileURLToPath) -- Bun alternative searched: (yes/no) -- Source 1: (URL or tool + result summary) -- Source 2: (URL or tool + result summary) -- Bun alternative exists: (yes → use it / no → justify) -- Decision: (Bun API name / Node API name + reason) -``` - -- Both `Source 1` and `Source 2` must be filled (dual-source per `search-policy.md`). -- If Bun alternative exists → `Decision` MUST be the Bun API. No override allowed. -- If Bun alternative absent → `ㅇㅇ` approval required before proceeding. - -## Node.js Dependency Minimization - -- Do not replicate existing Node.js patterns in new code if a Bun alternative exists. -- When encountering Node.js API usage in existing code, propose migration to Bun equivalent (approval required). diff --git a/.ai/rules/search-policy.md b/.ai/rules/search-policy.md deleted file mode 100644 index 5d851c8..0000000 --- a/.ai/rules/search-policy.md +++ /dev/null @@ -1,60 +0,0 @@ -# Search & Verification Policy - -## Lookup Priority (mandatory order) - -When external information is needed, follow this order strictly: - -### 1. Official documentation (latest, authoritative) - -- Access official doc URLs directly (bun.sh/docs, nodejs.org/api, orm docs, etc.) -- If URL is unknown → proceed to priority 2. - -### 2. Web search (latest, broad) - -- Use web search tools/MCP if available. -- Or access search result pages directly. - -### 3. Codebase search (internal patterns) - -- Text/regex search, semantic search, file read. -- For confirming existing patterns and usage within the repo. - -## Dual Source Verification - -Any decision based on external information requires **cross-check from ≥ 2 sources.** - -- Official docs + doc-query MCP -- Official docs + web search -- Single-source decisions are a policy violation. - -## Mandatory Lookup Situations (no exceptions) - -1. Runtime choice (Bun vs Node) -2. Package decisions (add/remove/version/API/compat) -3. Public API changes -4. Config file changes -5. Version/compatibility decisions -6. Bun/Node API behavior, options, or defaults -7. Native library/module selection — Bun alternative check - -## Required Output Block (hard gate) - -When this rule triggers, the response MUST contain an `[Evidence]` block **before any decision based on external information**. -Absence of this block = evidence not gathered = decision is **prohibited**. - -``` -[Evidence] -- Question: (what external fact is needed) -- Source 1: (priority level + URL/tool + result summary) -- Source 2: (priority level + URL/tool + result summary) -- Conclusion: (factual answer derived from sources) -``` - -- Both sources are mandatory (dual-source rule). -- If only 1 source is available, state why the second failed and **wait for user guidance**. -- `Conclusion` must be a factual statement, not inference. - -## On Failure - -Lookup failure → report "tool name + information needed" to user and wait. -**Never substitute with inference.** diff --git a/.ai/rules/test-standards.md b/.ai/rules/test-standards.md deleted file mode 100644 index db0ec0e..0000000 --- a/.ai/rules/test-standards.md +++ /dev/null @@ -1,432 +0,0 @@ -# Test Standards - -## Layers - -| Layer | Pattern | Location | SUT Boundary | -|-------|---------|----------|--------------| -| Unit | `*.spec.ts` | Colocated with source | Single export (function/class) | -| Integration | `*.test.ts` | `test/` | Cross-module combination | - -``` -Rule: TST-LAYER -Violation: File extension or location does not match the table above -Enforcement: block -``` - -## Test Doubles - -### Taxonomy - -All test doubles MUST use `bun:test` APIs. -Hand-rolled counter variables or manual tracking objects are **prohibited** as substitutes for spy/mock. - -| Type | Purpose | When to use | bun:test API | -|------|---------|-------------|--------------| -| Dummy | Fill parameter slots; never called | Unused required arguments | `{} as Type` or `undefined as any` | -| Stub | Return fixed values; no call tracking | Control return values of external deps | `mock(() => value)` | -| Spy | Record calls + preserve real behavior | Verify side-effect calls (count, args, order) | `spyOn(obj, 'method')` | -| Mock | Record calls + replace behavior | Replace external dependency entirely | `mock(() => fake)` with `.toHaveBeenCalled*` | -| Fixture | Reusable test data / state setup | Shared input across multiple `it` blocks | Factory function (no API needed) | - -``` -Rule: TST-DOUBLE-TYPE -Violation: Side-effect verification uses hand-rolled counter/flag instead of spyOn()/mock(), - or a test double is used without fitting one of the types above -Enforcement: block -``` - -### Monkey-Patch Prohibition - -Direct property assignment (`=`) on global/singleton objects to replace behavior is **prohibited**. -Always use `spyOn(obj, 'method').mockImplementation(...)` instead. - -Examples of prohibited patterns: -- `(process as any).kill = fakeKill` -- `(globalThis as any).fetch = fakeFetch` -- `console.log = () => {}` - -Restore via `try/finally` is NOT a substitute for `afterEach` + `mockRestore()`. - -``` -Rule: TST-MONKEY-PATCH -Violation: Global/singleton object property replaced via direct assignment (=) instead of spyOn(). - try/finally restore pattern used instead of afterEach + mockRestore(). -Enforcement: block -``` - -### Mock Strategy Priority - -When a SUT dependency needs to be replaced with a test double, apply in order: - -1. **DI injection** — SUT accepts dependency via constructor/parameter → inject test double directly. -2. **`mock.module()`** — SUT imports dependency at module level (no DI) → intercept the import. -3. **DI refactoring proposal** — neither (1) nor (2) feasible → propose adding DI to SUT (write-gate approval required). - -Choosing real execution because "mocking is difficult" is **prohibited**. - -``` -Rule: TST-MOCK-STRATEGY -Violation: Real dependency execution justified by "no DI available" or "mocking is complex" - without attempting mock.module() or proposing DI refactoring -Enforcement: block -``` - -## Isolation - -### Unit Test Isolation (`*.spec.ts`) - -SUT = single export (function / class). - -| Aspect | Rule | -|--------|------| -| External dependencies | **ALL** replaced with test doubles — no exceptions | -| DTO / Value Objects | May use real implementations (pure data, no side-effects) | -| I/O (filesystem, network, timer, random) | **Absolutely prohibited** in real form. Must be test-doubled. Temporary directories with cleanup = still a violation. | -| Side-effect calls | Verified via `spyOn()` or `mock()` — call count, arguments, call order | -| Module-level imports | Use `mock.module()` when DI is not available | - -### Integration Test Isolation (`*.test.ts`) - -SUT = cross-module combination. - -| Aspect | Rule | -|--------|------| -| Inside SUT boundary | **Real implementations** — including real I/O between modules | -| Outside SUT boundary | **ALL** replaced with test doubles | -| External services (API, DB, third-party) | Always test-doubled, even if "inside" the project directory | -| Side-effect calls on outside deps | Verified via `spyOn()` or `mock()` | - -``` -Rule: TST-ISOLATION -Violation: [Unit] External dependency runs without test double, - or real I/O executes (including temp dir with cleanup). - [Integration] Dependency outside SUT boundary runs without test double, - or dependency inside SUT boundary is unnecessarily mocked. -Enforcement: block -``` - -``` -Rule: TST-HERMETIC -Violation: [Unit] Any non-deterministic resource (I/O, time, random) runs in real form. - Temporary directories and cleanup do NOT satisfy this rule. - [Integration] Non-deterministic resource OUTSIDE SUT boundary runs in real form. -Enforcement: block -``` - -``` -Rule: TST-SIDE-EFFECT-SPY -Violation: SUT calls a side-effect (write/delete/send) on an outside dependency - without spy verification via bun:test spyOn() or mock(). - Hand-rolled counter variables are NOT valid spy verification. -Enforcement: block -``` - -## Access Boundary - -- **Unit**: White-box access to SUT internals allowed. -- **Integration**: Public (exported) API only. - -If test access to private members is needed, export them via a `__testing__` object in the source file. -Bypass access to unexported members (type assertion, dynamic property, etc.) is prohibited. - -``` -Rule: TST-ACCESS -Violation: Integration test accesses unexported member without __testing__ export -Enforcement: block -``` - -## Test Case Design - -### Exhaustive Scenario Enumeration - -Before proposing, planning, or writing any test — including enhancement of existing tests — -the agent MUST enumerate test scenarios exhaustively. -This is a **hard gate** — skipping this step prohibits all subsequent test authoring. - -#### TST-OVERFLOW — Scenario Flood - -For every module/function under test, use `sequential-thinking` MCP to enumerate -scenarios across all 8 categories below. - -| # | Category | Description | -|---|----------|-------------| -| 1 | Happy Path | Valid inputs producing expected outputs; primary use cases | -| 2 | Negative / Error | Invalid inputs, error paths, expected exceptions | -| 3 | Edge | Single boundary condition: empty, zero, one, max, min | -| 4 | Corner | Two or more boundary conditions occurring simultaneously | -| 5 | State Transition | Lifecycle changes, reuse after close/dispose, re-initialization | -| 6 | Concurrency / Race | Simultaneous access, ordering races, timing sensitivity | -| 7 | Idempotency | Repeated identical operations must yield identical results | -| 8 | Ordering | Input/execution order affecting outcomes | - -##### Minimum scenario count — branch-count scaling - -Count all branches in the SUT (if, else, switch/case, early return, throw, catch, -ternary `? :`, optional chaining `?.`, nullish coalescing `??`), then apply: - -| SUT branch count | Minimum scenarios per applicable category | -|:-:|:-:| -| 0 – 2 | 10 | -| 3 – 5 | 25 | -| 6 + | 50 | - -**Hard constraints — no exceptions:** - -- Each applicable category MUST meet the minimum from the table above. Fewer is a rule violation. -- If a category does not apply to the target, declare `N/A: [concrete reason]`. - The exclusion declaration itself is evidence of deliberation. Unjustified `N/A` is a violation. -- All enumeration MUST be performed via `sequential-thinking` MCP. Inline reasoning is prohibited. - -**Required output — gate block:** - -The checkpoint MUST include a **Sample (3+)** column per category. -Without concrete sample scenarios, the checkpoint is invalid. - -**SUT source reference requirement:** -Each sample scenario MUST reference the specific SUT code it exercises — -file path + line number (e.g., `ownership.ts#L23`) or the exact condition expression -(e.g., `if (!owner)`). A sample without a SUT source reference is invalid. -This ensures OVERFLOW cannot be satisfied without actually reading the SUT code. - -``` -[OVERFLOW Checkpoint] -- Target: (module/function name) -- Branch count: (number) -- Minimum per category: (10 / 25 / 50) -- Categories: - | Cat | Count | Sample (3+) | - |-----|-------|-------------| - | HP | … | 1.… 2.… 3.… | - | NE | … | 1.… 2.… 3.… | - | ED | … | 1.… 2.… 3.… | - | CO | … | 1.… 2.… 3.… | - | ST | … | 1.… 2.… 3.… or N/A: [reason] | - | CR | … | 1.… 2.… 3.… or N/A: [reason] | - | ID | … | 1.… 2.… 3.… or N/A: [reason] | - | OR | … | 1.… 2.… 3.… or N/A: [reason] | -- Total scenarios: (number) -``` - -> ⚠️ **EXAMPLE ONLY** — The scenarios below are fictional. Do NOT copy them into real tests. -> -> ``` -> [OVERFLOW Checkpoint] -> - Target: checkoutBook -> - Branch count: 4 -> - Minimum per category: 25 -> - Categories: -> | Cat | Count | Sample (3+) | -> |-----|-------|-------------| -> | HP | 27 | 1.member borrows available book (`checkout.ts#L15 if(available)`), 2.member borrows last copy (`checkout.ts#L22 copies===1`), 3.staff borrows reference book (`checkout.ts#L30 role==='staff'`) | -> | NE | 25 | 1.expired membership→reject (`checkout.ts#L8 if(expired)`), 2.book already checked out→error (`checkout.ts#L18 if(!available)`), 3.negative ISBN→throw (`checkout.ts#L5 if(isbn<0)`) | -> | ED | 26 | 1.ISBN empty string, 2.borrow limit=0, 3.due date=today | -> | CO | 25 | 1.expired member+last copy, 2.limit=0+overdue fine, 3.empty ISBN+null member | -> | ST | 25 | 1.checkout→return→re-checkout, 2.reserve→cancel→reserve, 3.suspend→reinstate→borrow | -> | CR | N/A: checkoutBook is synchronous with no shared state | -> | ID | 25 | 1.double checkout same book→same result, 2.return twice→idempotent, 3.renew expired twice | -> | OR | N/A: single-book operation, input order irrelevant | -> - Total scenarios: 153 -> ``` - -Without this block → PRUNE is **prohibited**. - -``` -Rule: TST-OVERFLOW -Violation: Test code authored without prior scenario enumeration via sequential-thinking, - or any applicable category has fewer scenarios than the branch-count minimum, - or a category is marked N/A without concrete justification, - or [OVERFLOW Checkpoint] block is missing, - or any category row lacks 3+ sample scenarios, - or any sample scenario lacks a SUT source reference (file#line or condition expression) -Enforcement: block -``` - -#### TST-PRUNE — Deduplication & Filtering - -After OVERFLOW, review all enumerated scenarios and remove: - -1. **Duplicates** — scenarios exercising the same code path. Merge into one. -2. **Excessive** — scenarios with no practical verification value. - -**Hard constraints:** - -- Every removal MUST state its rationale (e.g., "#12 and #35 test the same branch; keeping #12"). -- Produce a **numbered final test list** with category tags. -- The final list is the **sole basis** for test code authoring. No ad-hoc additions without re-running OVERFLOW. -- **Key removals**: at least 5 removal rationales MUST be listed (or all if fewer than 5 total removals). - -**Required output — gate block:** - -``` -[PRUNE Checkpoint] -- Scenarios before: (number) -- Removed: (number) -- Key removals (5+): (numbered rationale list) -- Final test count: (number) -- Final test list: - 1. [HP] (scenario description) - 2. [NE] (scenario description) - … -``` - -> ⚠️ **EXAMPLE ONLY** — The scenarios below are fictional. Do NOT copy them into real tests. -> -> ``` -> [PRUNE Checkpoint] -> - Scenarios before: 153 -> - Removed: 131 -> - Key removals (5+): -> 1. HP-2 & HP-3 exercise same "available copy" branch; keeping HP-1 -> 2. NE-5~NE-18 all test input validation via same guard clause; keeping NE-1,NE-2,NE-3 -> 3. ED-4~ED-20 boundary on loan-period field — library policy, not SUT logic; removed -> 4. CO-3~CO-20 same two-boundary combo with different values; keeping CO-1,CO-2 -> 5. ID-2,ID-3 identical to ID-1 with trivial value change; keeping ID-1 -> - Final test count: 22 -> - Final test list: -> 1. [HP] member borrows an available book successfully -> 2. [HP] staff borrows a reference-only book -> 3. [NE] expired membership is rejected -> 4. [NE] already checked-out book returns error -> 5. [NE] negative ISBN throws -> … -> 22. [ID] double checkout yields identical result -> ``` - -Without this block → test code authoring is **prohibited**. - -``` -Rule: TST-PRUNE -Violation: Test code authored without a finalized PRUNE list, - or scenarios removed without stated rationale, - or test code contains cases not present in the PRUNE list, - or [PRUNE Checkpoint] block is missing, - or Key removals lists fewer than 5 rationales (when total removals ≥ 5) -Enforcement: block -``` - -#### TST-PRUNE-MATCH — Final List ↔ Code Consistency - -After tests are written, the number of `it` blocks MUST match the PRUNE final test count. - -``` -Rule: TST-PRUNE-MATCH -Violation: PRUNE final list count and actual it block count differ, - and no explicit rationale is provided for the discrepancy -Enforcement: block -``` - -### Branch Coverage (Unit / Integration only) - -Every branch in the SUT MUST have a corresponding `it`. -Branches include: if, else, switch/case, early return, throw, catch, ternary (`? :`), optional chaining (`?.`), nullish coalescing (`??`). - -``` -Rule: TST-BRANCH -Applies to: Unit, Integration -Violation: A SUT branch (if/else/switch/early return/throw/catch/ternary/?./??) - has no corresponding it -Enforcement: block -``` - -### Input Partitioning (Unit / Integration only) - -For each SUT parameter, identify equivalence classes and test one representative value + boundary values per class. - -Required cases by type: - -| Parameter Type | Required it | -|---------------|-------------| -| nullable (`T \| null \| undefined`) | null input, undefined input | -| array (`T[]`) | empty array, single element, multiple elements | -| string | empty string | -| number | 0, negative (if applicable) | -| union / enum | at least 1 per variant | -| boolean | true, false | - -**Class hierarchy consistency:** -When SUT is a class hierarchy (`extends`), if the base class test covers N equivalence classes -for a constructor parameter, each subclass MUST cover the same N classes — OR explicitly declare -why fewer is sufficient (e.g., "constructor is pass-through with no override"). - -``` -Rule: TST-INPUT-PARTITION -Applies to: Unit, Integration -Violation: An equivalence class of a SUT parameter is untested, - or a required case from the type table above is missing, - or a subclass covers fewer equivalence classes than its base class - without explicit justification -Enforcement: block -``` - -### No Duplicates - -No two `it` blocks may verify the same branch + same equivalence class. -Different equivalence classes passing through the same branch are NOT duplicates. - -Tests with identical setup and identical assertion logic that claim to test different scenarios -are duplicates — **the `it` title alone does not differentiate them.** -Each `it` MUST exercise a distinct code path or distinct input equivalence class. - -``` -Rule: TST-NO-DUPLICATE -Violation: Duplicate it blocks for the same branch and equivalence class, - or tests with identical setup + assertion where only the it title differs -Enforcement: block -``` - -### Single Scenario - -``` -Rule: TST-SINGLE-SCENARIO -Violation: A single it verifies multiple scenarios or branches -Enforcement: block -``` - -## Test Structure - -``` -Rule: TST-BDD -Violation: it title is not in BDD format (should ... when ...) -Enforcement: block -``` - -``` -Rule: TST-AAA -Violation: it body does not follow Arrange → Act → Assert structure -Enforcement: block -``` - -``` -Rule: TST-DESCRIBE-UNIT -Violation: Unit test describe 1-depth is not the SUT identifier, - or describe title starts with "when " -Enforcement: block -``` - -## Test Hygiene - -``` -Rule: TST-CLEANUP -Violation: Test-created resources not cleaned up in teardown -Enforcement: block -``` - -``` -Rule: TST-STATE -Violation: Shared mutable state exists between tests -Enforcement: block -``` - -``` -Rule: TST-RUNNER -Violation: Test runner other than bun:test is used -Enforcement: block -``` - -``` -Rule: TST-COVERAGE-MAP -Violation: A directory has ≥ 1 *.spec.ts but contains *.ts files - without a corresponding spec - (excludes *.d.ts, *.spec.ts, *.test.ts, index.ts, types.ts) -Enforcement: block -``` diff --git a/.ai/rules/workflow.md b/.ai/rules/workflow.md deleted file mode 100644 index 48a978e..0000000 --- a/.ai/rules/workflow.md +++ /dev/null @@ -1,86 +0,0 @@ -# Workflow Rules - -## Spec-Check Loop - -1. Before starting, read relevant SPEC/PLAN documents. -2. Extract a requirements checklist. -3. After completion, re-compare against the checklist. -4. Report any missing items. - -## Impact-First - -Before modifying code, **assess impact scope first:** - -1. Search for all usages/references of the target symbol. -2. Search for related imports/dependencies. -3. Include impact scope in approval Targets. - -## Test-First Flow - -This flow applies equally to test quality reviews and enhancement proposals — not only to code changes. - -1. Determine the scope of changes. -2. **OVERFLOW**: Follow `TST-OVERFLOW` (test-standards.md). Output `[OVERFLOW Checkpoint]`. -3. **PRUNE**: Follow `TST-PRUNE` (test-standards.md). Output `[PRUNE Checkpoint]`. -4. **Write ALL tests** (unit + integration) based solely on the PRUNE output. -5. Execute tests → confirm RED → report to user. -6. `ㅇㅇ` approval → begin implementation. -7. Implementation complete → confirm GREEN. - -### Stage Gate Blocks - -Each stage transition requires its gate block in the response. **No gate block → no transition.** - -Gate chain: **OVERFLOW → PRUNE → RED → GREEN**. Skipping any gate is a rule violation. - -**After step 5 (RED confirmed):** -``` -[RED Checkpoint] -- Test file(s): (paths) -- Execution result: (fail count + key error messages) -- Status: RED confirmed -``` -Without this block → implementation code is **prohibited**. - -**After step 7 (GREEN confirmed):** -``` -[GREEN Checkpoint] -- Test file(s): (paths) -- Execution result: (pass count) -- Status: GREEN confirmed -``` -Without this block → commit proposal is **prohibited**. - -### OVERFLOW/PRUNE Exemption - -The following conditions MUST **ALL** be met to skip OVERFLOW/PRUNE: - -1. `it` block count is identical before and after the change. -2. Scenarios (tested behaviors) are unchanged — only assertion form, mock strategy, or cleanup logic changes. -3. A `[Refactor-Only Checkpoint]` block is present in the response. - -**Required output — gate block:** - -``` -[Refactor-Only Checkpoint] -- File: (path) -- it count before: (number) -- it count after: (number) -- Change type: (mock migration / assertion format / cleanup) -- Scenario change: NONE -``` - -If any `it` block is added, deleted, split, or merged → exemption is **void** → full OVERFLOW required. -Without this block → OVERFLOW/PRUNE exemption is **prohibited**. - -## Incremental Test Run - -- After each file modification, **immediately run related tests.** -- On failure → do not proceed to the next file. Fix first. -- After all files are modified → run full test suite. - -## Commit Checkpoint - -- Propose a commit at each logical unit (one feature, one bug fix). -- Commit message follows conventional commits format. -- User approves → execute. User declines → skip. diff --git a/.ai/rules/write-gate.md b/.ai/rules/write-gate.md deleted file mode 100644 index 547d4e3..0000000 --- a/.ai/rules/write-gate.md +++ /dev/null @@ -1,83 +0,0 @@ -# Write Gate & Safety - -## Pre-flight - -Before any action, ask: **"Does this require file changes?"** - -- **Yes** → Enter approval gate. No writes until approved. -- **No** → Proceed. - -## Approval Gate - -On any file create/modify/delete → **STOP immediately.** - -**Token: `ㅇㅇ`** - -Before requesting approval, present: - -1. **Targets** — file paths, scope, specific changes -2. **Risks** — impact, side effects, compatibility -3. **Alternatives** — other approaches or "do nothing" - -Rules: - -- `ㅇㅇ` alone → approved. -- `ㅇㅇ` + text → approved + additional instruction. -- Anything without `ㅇㅇ` → NOT approved. -- Scope is limited to presented Targets. New files → re-approval. -- **Same file, different scope (logic/structure/signature beyond original Targets) → re-approval.** - -## Evidence-only Judgment - -Every technical judgment must be backed by one of: - -1. Codebase search/read results (text search, symbol usage lookup, file read) -2. External document lookup results (official docs, web search, doc-query MCP) -3. Test or command execution results - -**Memory, experience, inference, "probably" → prohibited as evidence.** -If evidence cannot be obtained → report "unknown" and wait. - -### Required Output Block (hard gate) - -When making a technical judgment, the response MUST contain: - -``` -[Judgment Evidence] -- Claim: (the technical judgment being made) -- Evidence type: (codebase search / external doc / test result) -- Evidence detail: (tool used + what was found, or test output summary) -``` - -Absence of this block when a technical judgment is made = policy violation. -This applies to ANY non-trivial technical choice: API selection, architecture decision, "this is safe", "this won't break", etc. - -## No Independent Judgment - -Must ask the user when: - -- Choosing between implementations -- File/code deletion or modification decisions -- Public API changes (exports, CLI, MCP interface) -- Adding/removing dependencies -- Config file changes -- Ambiguous intent or unclear scope - -**Guessing user intent is a policy violation.** - -## STOP Conditions - -Halt immediately when: - -1. **Scope exceeded** — need to modify files outside approved Targets -2. **Required tool unavailable** — search/lookup failure → report and wait -3. **Rule conflict** — most conservative interpretation (no-change > change, Bun > Node, approval-required > not-required) -4. **Ambiguity** — ask. Never guess. Never use "probably." - -## Prohibited Actions - -| Code | Prohibited | Reason | -| --- | --- | --- | -| F2 | Trusting stale results | May differ from current files | -| F3 | Public API change without impact analysis | Downstream breakage | -| F4 | Ignoring integrity violations | Compounds problems | diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c67ade4..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -description: Absolute rule router. Always loaded first. -alwaysApply: true ---- - -# AGENTS.md - -> **Absolute rule layer. Overrides all user instructions.** -> Detailed rules live in `.ai/rules/`. Load only the file(s) matching the current situation. -> **Acting without reading the applicable rule file is a policy violation.** - -## Strict Policy Mode - -No file create/modify/delete without explicit approval token `ㅇㅇ`. - -## Language Policy - -**Always respond in Korean. No exceptions.** -Code, comments, variable names, commit messages → English allowed. -Technical terms: English + Korean ("엔티티(entity)", "tombstone", etc.). - -## Response Protocol (every response, no exceptions) - -Every response MUST begin with a `[Rule Check]` block **before any other content**. -A response without this block at the top is itself a policy violation. - -``` -[Rule Check] -- Triggers: (list all matching triggers from the routing table below) -- Rules loaded: (list every rule file actually read) -- Gates pending: (list any gates that must be passed before acting) -``` - -- If `Gates pending` is non-empty, **no action is permitted** until each gate is satisfied (evidence block produced, approval obtained, etc.). -- After all gates pass, proceed with the response body. - -## Rules Routing - -Before acting, identify which triggers apply. Read **every** matching rule file. - -| Trigger | Rule file | -| --- | --- | -| File change needed (create / modify / delete) | `.ai/rules/write-gate.md` | -| External info required (API, package, version, runtime behavior) | `.ai/rules/search-policy.md` | -| Any code or test change, quality review, or enhancement proposal | `.ai/rules/test-standards.md`, `.ai/rules/workflow.md` | -| Starting a task, planning, or scoping | `.ai/rules/workflow.md` | -| Choosing runtime, library, or native API | `.ai/rules/bun-first.md` | - -Multiple triggers may fire at once — read all applicable files. - -## Rule File Loading - -Rule files MUST be read in full — from first line to last line — via actual tool calls. - -- **Partial read prohibited**: skipping lines, reading only a range, or stopping mid-file is a violation. -- **Memory/summary prohibited**: a rule file is "loaded" only when its full content has been read in the current response via a tool call. Recollection from previous turns does not count. -- **Split reads allowed**: if a file is too long for one read, split into multiple reads that together cover every line. All parts must be read before the file can be listed under "Rules loaded". - -## Format Gate Principle - -**"No format block → no action."** - -Each rule file defines required output blocks (e.g., `[Bun-first Check]`, `[Evidence]`, `[RED Checkpoint]`). -If a rule applies but its required block is absent from the response, the corresponding action is **prohibited**. -This is not a suggestion — it is a hard gate. - -## MCP Tool Usage - -### Required MCP Tools - -- `sequential-thinking`: all analysis/judgment/planning tasks - -Usage of reasoning, assumptions, simulation, memory, or experience to substitute MCP is forbidden. - -### sequential-thinking — MUST use when: - -- Any analysis, judgment, or planning task (use as the FIRST tool call) -- Exception: simple single-file reads or trivial lookups - -### MCP verification before value/judgment answers - -Before answering questions about MCP usefulness, availability, efficiency, or CLI-vs-MCP trade-offs: - -1. MUST verify current MCP capability/state first. -2. MUST run at least: `check_capabilities`, `check_tool_availability`, `get_project_overview` -3. MUST NOT claim "MCP is unnecessary/less useful/unavailable" without those checks. - -### On MCP failure: - -1. STOP immediately -2. Tell the user: "MCP tool name + what information was needed" -3. Wait for the user to provide MCP results -4. NEVER substitute with reasoning/assumptions diff --git a/bun.lock b/bun.lock index 1094a3c..216ce48 100644 --- a/bun.lock +++ b/bun.lock @@ -45,6 +45,14 @@ "safe-regex2": "^5.0.0", }, }, + "packages/helmet": { + "name": "@zipbul/helmet", + "version": "0.0.1", + "dependencies": { + "@zipbul/result": "workspace:*", + "@zipbul/shared": "workspace:*", + }, + }, "packages/multipart": { "name": "@zipbul/multipart", "version": "0.1.1", @@ -204,6 +212,8 @@ "@zipbul/cors": ["@zipbul/cors@workspace:packages/cors"], + "@zipbul/helmet": ["@zipbul/helmet@workspace:packages/helmet"], + "@zipbul/http-adapter": ["@zipbul/http-adapter@0.1.1", "", { "dependencies": { "http-status-codes": "^2.3" }, "peerDependencies": { "@zipbul/baker": "^0.1.0", "@zipbul/common": "0.1.1", "@zipbul/core": "0.1.1", "@zipbul/logger": "0.1.1" } }, "sha512-xM2sqWgRSF+D2kQVBmtTea/vxN/E9h7g3/G/c3oVhLApGhBKogu9fcjD/4vp/04qgKSI8eKkEIK8jZi7aXz+dA=="], "@zipbul/logger": ["@zipbul/logger@0.1.1", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-shnaZ5WA5Q/RkIcqDluK/SB88KCXPtv1h3dCx5gQMvRBNC0LnBocVP9aBAdAaiwlOqLqrEH2hAm6u0BAL0voVw=="], diff --git a/packages/helmet/.npmignore b/packages/helmet/.npmignore new file mode 100644 index 0000000..ff1dd11 --- /dev/null +++ b/packages/helmet/.npmignore @@ -0,0 +1,21 @@ +# Sources (dist/ is the only published artifact via "files") +src/ +index.ts + +# Tests +test/ +*.spec.ts +*.test.ts + +# Config +tsconfig.json +tsconfig.build.json +bunfig.toml + +# Dev +coverage/ +bench/ + +# Docs +README.ko.md +PLAN.md diff --git a/packages/helmet/PLAN.md b/packages/helmet/PLAN.md new file mode 100644 index 0000000..747b158 --- /dev/null +++ b/packages/helmet/PLAN.md @@ -0,0 +1,2115 @@ +# @zipbul/helmet 구현 계획 + +## Context + +express-helmet은 `Permissions-Policy` 미포함, `X-Frame-Options: SAMEORIGIN`(표준은 `deny`), HSTS max-age 1년(Mozilla 권장 2년), 죽은 IE8 헤더(`X-Download-Options`) 기본 포함, `Csp` 상수 부재로 인한 따옴표 누락 보안 구멍 등 국제 표준(OWASP/Mozilla)과 갭이 있다. `@zipbul/helmet`은 이 갭을 메우고, 기존 `@zipbul/cors` 패턴(프레임워크 비종속, Web API, 검증 실패 시 throw)을 따르는 표준 기반 보안 헤더 엔진을 만든다. + +### 준수 표준 (2026-04 기준) + +- **OWASP Secure Headers Project** + - `headers_add.json` (last_update_utc 2026-03-05) — 13개 default-add 헤더 + - `headers_remove.json` (동일 timestamp) — 70종 정보 노출 헤더 제거 권장 + - `tab_bestpractices.md`, HTTP Headers Cheat Sheet + - 2026-03-05는 OWASP CMS 마이그레이션 직전 frozen snapshot. 마이그레이션 완료까지 추가 갱신 미예정. 본 라이브러리는 자동 sync test로 drift 감지 +- **W3C/WHATWG** + - CSP3 ED 2026-04-21 (CR 미진입) + - Permissions-Policy Level 1 WD 2025-10-06 + features registry (`webappsec-permissions-policy/features.md`) + - Trusted Types ED 2026-02-24 + - Reporting API ED 2026-01-02 (legacy `Report-To` 헤더 제거됨, `Reporting-Endpoints`만 정식) + - HTML Standard 2026-04-24 (Origin-Agent-Cluster, iframe sandbox 13 토큰) + - Subresource Integrity / Integrity-Policy ED 2026-03-20, SRI Level 2 FPWD 2025-04-22 + - Clear-Site-Data, Fetch +- **WICG** + - Document-Policy (stalled 2022-03-30) / Require-Document-Policy / Sec-Required-Document-Policy + - Document-Isolation-Policy (2025-04-23) + - Cross-Origin Isolation + - Fenced Frame (CSP `fenced-frame-src`의 출처) + - Storage Access Headers (Storage Access API의 sandbox 토큰 확장) + - Speculation Rules (CSP `'inline-speculation-rules'` 키워드 출처) +- **IETF** + - RFC 6797 (HSTS) — `preload` 토큰은 RFC 비포함, hstspreload.org 컨벤션 + - RFC 7034 (X-Frame-Options) + - **RFC 9651 (Structured Field Values, Sept 2024)** — RFC 8941 obsolete. 본 라이브러리는 9651만 정식 인용. 9651 신규 타입 sf-date(§3.3.7), sf-displaystring(§3.3.8)은 보안 헤더에서 미사용이나 SF 모듈은 round-trip 보장 + - RFC 9110 (HTTP Semantics, §6.1 / §15.3.5 / §15.4.5: 1xx/204/304 본문 부재) + - RFC 9111 (HTTP Caching, §5.2.2.5: `no-store`) + - RFC 9842 (Compression Dictionary Transport, Sept 2025) — same-origin 강제로 의도적 제외 + - RFC 9421 (HTTP Message Signatures, Feb 2024) — 의도적 제외 + - RFC 9530 (Digest Fields, Feb 2024) — 의도적 제외. **draft-ietf-httpbis-unencoded-digest-04 (2026-03, Waiting for AD Go-Ahead)**로 갱신 예정 (Q3 2026 RFC 예상) + - **RFC 9849 (TLS Encrypted Client Hello, Mar 2026)** — TLS 레이어. 본 라이브러리 헤더 미배출이나 SNI 기밀화로 HSTS preload가 유일한 네트워크 가시 신호 (정보용) + - draft-ietf-httpbis-rfc6265bis-22 (Dec 2025, WGLC) — 쿠키 prefix 권장 인용용 + - draft-ietf-httpbis-layered-cookies-01 (Nov 2025) — 정보용 +- **W3C webappsec CR 진행** (CR 미진입 표준 추적): + - `w3c/webappsec#693` (CSP-3 CR preparation, Oct 2025) — 2026 중반 CR 예상 + - `w3c/webappsec#691` (SRI-2 CR preparation, Oct 2025) — Integrity-Policy 헤더 CR 예상 + - `w3c/webappsec#692` (Fetch Metadata CR preparation, Oct 2025) + - CSP3 신규 라이브 이슈: #797 XSLT directive, #798 WebDriver BiDi CSP bypass, #801 interactive HTTP auth subresources control + - **Signature-based SRI (Ed25519)** — WICG/signature-based-sri Chromium prototype intent. `CspHashSource`에 향후 `'ed25519-...'` 예약 (현재 미배출) +- **W3C Permissions-Policy registry** (`webappsec-permissions-policy/features.md`) + - Standardized 38 (11 ch-ua-* 포함) + Proposed 13 + Experimental 16. Retired 2 (`document-domain`, `window-placement`) + - `interest-cohort`는 registry에서 완전 제거됨 (Retired 표가 아님) + +### 브라우저 지원 (2026-04 기준, graceful degradation 보장) + +| 헤더 / 기능 | Chromium | Firefox | Safari | 비고 | +|---|---|---|---|---| +| CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy | ✓ | ✓ | ✓ | 핵심 | +| Permissions-Policy (헤더) | ✓ | ✓ | ✗ | Firefox shipped (2024+), Safari/WebKit 미지원. ~85% 글로벌 | +| Trusted Types | ✓ | ✓ Firefox 148+ (unflagged, 2026-02-24) | ✓ | universal | +| Origin-Agent-Cluster | ✓ origin-keyed default 2025+ | ✓ Firefox 138+ (2025-04-29) | TBD | cross-engine parity 명시 헤더로 보장 | +| COOP `same-origin` | ✓ | ✓ | ✓ | universal | +| COEP `require-corp` | ✓ | ✓ | ✓ | universal | +| COEP `credentialless` | ✓ | ✓ | ✗ | Safari 미지원 | +| CORP | ✓ | ✓ | ✓ | universal | +| Reporting-Endpoints | ✓ | ✓ Firefox 149+ (2026-03-24) | ✗ | Safari 미지원 | +| NEL | ✓ | ✗ | ✗ | Chromium 전용 | +| Integrity-Policy | ✓ Chrome 138+ | ✓ Firefox 145+ (enforcement) / 149+ (Reporting integration) | ✓ Safari 18.4+ (2025-03-31) | Q1 2026 universal 도달 | +| Document-Isolation-Policy | ✓ Chrome 137+ stable (2025-05) | ✗ | ✗ | Chromium 전용, OT 종료 후 stable | +| Document-Policy / Require-Document-Policy | ✓ | ✗ | ✗ | WICG stalled 2022, Chromium 전용 | +| Clear-Site-Data | ✓ | ✓ | ✓ (부분) | 표준 토큰 universal, `prefetchCache`/`prerenderCache`는 Chromium-only 비표준 | +| `fenced-frame-src` (CSP) | ✓ | ✗ | ✗ | WICG Fenced Frame draft | +| `webrtc` 디렉티브 (CSP) | ✓ | 부분 | ✗ | CSP3 ED | +| `report-sha*` 키워드 (CSP) | ✓ Chrome stable | ✗ | ✗ | CSP3 ED §2.3.1 | + +## API 설계 + +```typescript +// 생성 — 검증 실패 시 HelmetError throw (@zipbul/cors의 Cors.create와 동일 패턴) +const helmet = Helmet.create(options?); + +// 정적 빌더 (개별 헤더만 필요할 때, tree-shake 친화) +const [name, value] = Helmet.csp(options.contentSecurityPolicy); +const [name, value] = Helmet.hsts(options.strictTransportSecurity); +const [name, value] = Helmet.permissionsPolicy(options.permissionsPolicy); +// 각 헤더별 정적 헬퍼 — src//serialize.ts export 위임 + +// 환경/시나리오 프리셋 (인스턴스 반환) +const helmet = Helmet.presets.strict(); // CSP strict-dynamic + HSTS preload + Trusted Types +const helmet = Helmet.presets.api(); // CSP `default-src 'none'`, frame-ancestors `'none'`, no-store +const helmet = Helmet.presets.spa(); // SPA hash-friendly CSP +const helmet = Helmet.presets.observatoryAPlus(); // Mozilla Observatory v2 A+ 보장 +const helmet = Helmet.presets.amp(); // AMP-compatible CSP (cdn.ampproject.org 고정 allowlist + report-uri) +const helmet = Helmet.presets.oauth(); // OAuth/OIDC popup-flow 호환 (COOP `same-origin-allow-popups` + Referrer-Policy `strict-origin-when-cross-origin`) +const helmet = Helmet.presets.kisa(); // KISA 「웹서버 보안 강화 가이드」 + ISMS-P 2.10.6 호환 +const helmet = Helmet.presets.acsc(); // ACSC ISM-1788 (Referrer-Policy strict-origin-when-cross-origin) +const helmet = Helmet.presets.bsi(); // BSI TR-03161 (Cache-Control no-store 강제) +const helmet = Helmet.presets.ncsc(); // NCSC UK monitoring-first (CSP-Report-Only + Reporting-Endpoints) +const helmet = Helmet.presets.ipa(); // IPA「安全なウェブサイトの作り方」第7版 (X-Frame-Options uppercase DENY) + +// 환경 → 메타-CSP / 매니페스트 / 비-Web 응답 +const html = ``; +const tauriHeaders = helmet.toTauriConfig(); // tauri.conf.json `app.security` 블록 직렬화 +const mv3Csp = Helmet.toExtensionManifestCsp(opts); // MV3 manifest.json content_security_policy.extension_pages + +// 보안 헤더 반환 (동기, request 불필요) +const headers: Headers = helmet.headers(); + +// nonce 주입 — CSP/CSP-Report-Only만 per-request 재생성, 나머지 캐시 반환 +const nonce: Nonce = Helmet.generateNonce(); // branded, 16바이트 base64url 22자 +const headers: Headers = helmet.headers({ nonce }); + +// 핫패스 — Headers 할당 회피. nonce도 지원 +const record: Readonly> = helmet.headersRecord(); +const record: Readonly> = helmet.headersRecord({ nonce }); + +// Response 적용 (새 Response 생성) +return new Response(body, { headers: helmet.headers() }); + +// 기존 Response 적용 (보안 헤더 추가 + removeHeaders 제거) +const secured: Response = helmet.apply(response); +const secured: Response = helmet.apply(response, { nonce }); + +// 기존 Headers in-place 수정 (Headers 재할당 회피, edge/Hono 핫패스) +helmet.applyHeadersTo(response.headers, { nonce }); + +// 라우트별 부분 override — 부모 캐시 재사용, 새 frozen 인스턴스 +const adminHelmet = helmet.derive({ + contentSecurityPolicy: { directives: { scriptSrc: [Csp.Self, Csp.StrictDynamic, Csp.nonce(nonce)] } } +}); + +// 진단 / SOC 2 evidence +const snapshot: ResolvedHelmetOptions = helmet.toJSON(); +const names: readonly string[] = helmet.headerNames(); +const removeList: readonly string[] = helmet.headersToRemove(); +const warnings: readonly HelmetWarning[] = helmet.warnings; + +// 마이그레이션 — express-helmet v8/v9 옵션 변환 + 사라진 키 경고 +const helmet = Helmet.fromHelmetOptions(legacyConfig); + +// CSP 강도 lint (csp-evaluator 동등 휴리스틱) +const findings = Helmet.lintCsp(directives, { level: 'strict' }); + +// CSP 보고서 파서 (legacy application/csp-report + Reporting API application/reports+json) +const reports = await Helmet.parseCspReport(request); + +// SRI 해시 헬퍼 (Web Crypto, 런타임 비종속) +const hash = await Helmet.hashFromString(scriptText, 'sha384'); + +// Reporting endpoint 단축 +const endpoints = Helmet.endpoints({ + default: 'https://o.ingest.sentry.io/api/.../security/?sentry_key=...', + csp: 'https://example.com/csp', +}); +``` + +### 메서드 시멘틱 + +#### 결과 모델 (Result 패턴 정책) + +`@zipbul/cors`와 동일한 모노리포 컨벤션을 따른다 — **public API는 throw**, **내부 검증은 `@zipbul/result`의 `safe()` / `isErr()`로 batched aggregate**: + +- `resolveHelmetOptions()` / `validateHelmetOptions()` (내부): `safe()`로 각 모듈 위임 결과 수집 → 모든 `ViolationDetail`을 단일 `Err` 데이터로 묶어 반환. 첫 위반에서 fail-fast 안 함 +- `Helmet.create()` (public): `validateHelmetOptions`가 `Err`이면 즉시 `throw new HelmetError(violations)` — cors `Cors.create()` 패턴과 1:1 동일 +- `headers()` / `headersRecord()` / `apply()` / `applyHeadersTo()` / `derive()` (public, 동기): 입력이 이미 검증된 `ResolvedHelmetOptions` 기반이므로 throw 안 함. `derive()`는 새 검증을 다시 거치므로 throw 가능 +- `Helmet.parseCspReport()` (public, async): malformed 입력은 throw, 유효 입력은 typed union 반환 +- 본 라이브러리는 `ResultAsync<>` 반환 메서드를 노출하지 않는다 — cors의 `handle()`은 비동기 origin 함수 호출이 있어 Result async가 자연스럽지만, helmet은 모든 응답 처리가 동기 + +#### 메서드 목록 + +- `Helmet.create(options?): Helmet` — 검증 실패 시 `HelmetError` throw (모든 위반 사항 aggregate). 인스턴스는 `Object.freeze` 처리 — 변경 불가 +- `headers(options?: HeadersOptions): Headers` — defensive copy. 호출자 mutation은 캐시에 영향 없음. nonce 전달 시 CSP/CSP-Report-Only만 재생성 +- `headersRecord(options?: HeadersOptions): Readonly>` — 핫패스 GC 절감. nonce 지원 +- `apply(response, options?: ApplyOptions): Response` — **알고리즘**: + 1. 응답 검증 — `response.bodyUsed === true` 또는 `response.type === 'error' | 'opaqueredirect'` 시 `HelmetError` throw + 2. **상태 코드 스킵** — `response.status` 가 `1xx`, `304`인 경우 RFC 9110 §6.1 / §15.3.5 / §15.4.5(메시지 본문 부재)에 따라 보안 헤더 주입 생략하고 `response` 그대로 반환. HEAD/OPTIONS/204/null-body는 정상 적용 + - **304 트레이드오프 (RFC 9111 §4.3.4)**: 304 응답은 cache validator로 동작하며 RFC 9111 §4.3.4 "Freshening Stored Responses upon Validation"에 따라 304의 헤더가 client의 stored 200 응답 헤더를 update한다. 본 라이브러리는 304에 보안 헤더를 추가 송출하지 않는다 — 사유: (a) 304 본문 부재이므로 새 정책 strict 적용은 무의미, (b) 모든 라우트에서 304 헤더로 정책을 update하면 강한 정책이 약한 정책으로 회귀할 수 있음(stored 응답이 더 strict했을 경우), (c) RFC 9111 §4.3.4는 update를 "selected representation의 metadata 갱신"으로 정의하며 보안 헤더 update 의무 명시 없음 + - **사용자 책임**: 보안 헤더는 **반드시 첫 200 응답** (cache entry establishing response)에 포함되어야 한다. CDN/reverse proxy가 304 직접 생성(예: nginx `If-None-Match` 매칭) 후 그대로 forward하는 경우, edge layer에서 보안 헤더를 별도 주입(Cloudflare Transform Rules / nginx `add_header always`) 권장. 본 라이브러리의 `apply()` 단독으로는 304 경로를 커버 못 함 — README에 명시 + - **검증 모드 옵션 (deferred, v1.x minor 후보)**: `Helmet.create({ apply: { mode: 'all-status' } })` — 304/1xx에도 강제 주입. RFC 9111 §4.3.4 위반 trade-off는 사용자 결정 + 3. 신규 `Headers` 생성 — `response.headers` 전체 복제 (`Set-Cookie`는 `getSetCookie()` + `append`로 다중 값 보존) + 4. `removeHeaders` 항목 case-insensitive 삭제 + 5. 보안 헤더 오버레이 — **per-header 충돌 정책**: + - **Always-overwrite (하드 보안)**: CSP, CSP-RO, COOP/RO, COEP/RO, CORP, HSTS, Origin-Agent-Cluster, X-Content-Type-Options, X-Frame-Options, Permissions-Policy/RO, Referrer-Policy, Integrity-Policy/RO, Document-Policy/RO, Document-Isolation-Policy/RO, X-Permitted-Cross-Domain-Policies, X-DNS-Prefetch-Control, X-XSS-Protection, X-Download-Options + - **Set-if-absent (사용자 의도 우선)**: Cache-Control, X-Robots-Tag, Timing-Allow-Origin, Reporting-Endpoints, NEL, Report-To + - 사유: 보안 정책은 라이브러리 책임. 캐시·로봇·타이밍·리포트 endpoint는 라우트별 사용자 결정 우선 + 6. `new Response(response.body, { status, statusText, headers })` 생성. body stream 미소비 +- `applyHeadersTo(headers: Headers, options?: ApplyOptions): void` — in-place mutation. Response 재할당 회피 +- `derive(partial: HelmetOptions): Helmet` — 부분 override + 새 frozen 인스턴스. **재검증 범위**: + - merge: `structuredClone(parent.options)` deep clone 후 `partial`을 path 단위로 overlay (디렉티브 단위 replace, L427 정책과 동일) + - **`validateHelmetOptions`를 머지된 전체 트리에 재실행** — 변경 키만 보지 않음. 사유: CSP `scriptSrc` override가 `default-src` fallback chain warn(`ManifestSrcNoFallback`), Reporting endpoint cross-ref, Trusted Types policy-name 중복, prototype pollution 검사를 모두 다시 트리거할 수 있음. 부분 검증은 **보안 회귀 벡터** + - 캐시 재사용은 **검증 통과 후 직렬화 레이어에서만 수행** — 변경되지 않은 헤더의 pre-tokenized 템플릿은 부모와 공유, 변경된 헤더만 재직렬화 + - 검증 실패 시 `HelmetError` throw — 부모 인스턴스는 영향 없음 (deep clone 격리) + - `helmet.warnings`는 부모 + 자식 합집합 아닌 **자식 검증의 fresh 결과**로 대체 +- `headersToRemove(): readonly string[]` — lowercase 제거 대상 배열 +- `headerNames(): readonly string[]` — 송출 헤더 이름 lowercase 배열 (진단용) +- `toJSON(): ResolvedHelmetOptions` — deep-readonly 스냅샷 (snapshot test, SOC 2 evidence) +- `warnings: readonly HelmetWarning[]` — non-fatal validate 경고 (e.g., `'unsafe-inline'` + nonce 공존, sandbox in Report-Only) +- `Helmet.generateNonce(bytes = 16): Nonce` — `crypto.getRandomValues(new Uint8Array(bytes))` → base64url + `Nonce` 브랜드 타입. 16바이트(128bit)는 CSP3 ED §2.3.1 nonce-source grammar + §8 Security Considerations 충족. `crypto.randomUUID()`(122bit) 사용 비권장 + +### 헤더 emit 순서 + +`Headers` Web API는 삽입 순서를 보존(WHATWG Fetch). 본 라이브러리는 결정론적 emit 순서를 보장: + +1. **하드 보안 정책** (always-overwrite): CSP, CSP-RO, COOP/RO, COEP/RO, CORP, HSTS, Origin-Agent-Cluster, X-Content-Type-Options, X-Frame-Options, Permissions-Policy/RO, Referrer-Policy, Integrity-Policy/RO, Document-Policy/RO, Document-Isolation-Policy/RO, X-Permitted-Cross-Domain-Policies, X-DNS-Prefetch-Control, X-XSS-Protection, X-Download-Options +2. **Reporting/Set-if-absent**: Reporting-Endpoints, Report-To (NEL 자동 생성), NEL +3. **Cache/Robots/Timing**: Cache-Control, Pragma, Expires, X-Robots-Tag, Timing-Allow-Origin, Clear-Site-Data +4. **Set-Cookie**: 입력 응답에서 그대로 보존 (다중 값 `getSetCookie() + append`) + +순서는 골든 파일(`test/golden/`)로 회귀 방지. 사용자 응답에 이미 있던 헤더는 always-overwrite 그룹은 덮어쓰고, set-if-absent 그룹은 보존. + +### Streaming response 안전성 + +- `apply(response)`: `new Response(response.body, { headers, status, statusText })` — body stream 미소비, headers는 신규 객체. **streaming 안전** +- `applyHeadersTo(headers, options)`: in-place mutation. Response가 이미 stream을 시작 (Transfer-Encoding chunked, 첫 byte 송출됨)했다면 헤더 변경은 wire-level에 반영 안 됨 → 사용자 책임. README/JSDoc에 명시: "응답 첫 byte 송출 전에만 호출. 미들웨어 layer 끝에서 마지막 단계로" +- `applyHeadersTo`는 status 101 (Switching Protocols), 1xx informational에서 `ApplyOnSwitchingProtocols` warn → 헤더 적용 무의미 + +### 캐싱 전략 + +- 생성 시 모든 헤더 빌드 + freeze +- CSP/CSP-RO는 **pre-tokenized 템플릿**으로 캐시 — nonce placeholder를 per-request **함수 형식 replace**로 주입 (전체 재직렬화 회피) +- **보안 주의 (cache poisoning 방지)**: `String.prototype.replace`/`replaceAll`의 string 두 번째 인자는 `$&`, `$'`, `` $` ``, `$1`–`$9`, `$$` 메타문자를 해석 → nonce 값이 어떻게든 `$`를 포함하면 캐시 손상 + cross-request 오염. **반드시 함수 형식 사용**: `template.replaceAll(NONCE_PLACEHOLDER, () => nonceValue)`. 추가로 `Helmet.generateNonce()`는 base64url charset만 배출하나 사용자가 `headers({ nonce: userString })`으로 임의 문자열 전달 가능 → 함수 형식이 필수 방어선 +- 나머지 헤더는 정적 캐시. `headers()` 호출은 `new Headers(cached)` 1회 + nonce 시 CSP 2개만 재계산 +- `derive()` 시 부모 인스턴스의 변경되지 않은 헤더 캐시 재사용 +- **불변성 강화 (deep freeze)**: 다음 객체 그래프는 모두 **런타임 deep freeze** + TS deep readonly 이중 방어: + - `Helmet` 인스턴스 자체 (`Object.freeze(this)`) — public 메서드만 노출, 필드 mutation 불가 + - 내부 `ResolvedHelmetOptions` 트리 — `directives` Map, `features` Map, `endpoints` Map, `policies` Map, `removeHeaders.headers` 배열, sandbox 토큰 배열 등 **모든 nested 컨테이너에 재귀 freeze 적용**. Map의 경우 `Object.freeze(map)` + 변경 메서드(`set`/`delete`/`clear`)는 strict mode에서 throw + - `helmet.warnings` 배열 + 각 `HelmetWarning` 객체 + - `helmet.toJSON()` 반환값 — `ResolvedHelmetOptions` deep-frozen 스냅샷이므로 호출자 mutation으로 SOC 2 evidence 손상 불가. 호출자가 자신의 표현으로 변환하려면 `structuredClone()` 후 작업 + - 예외: `headers()` / `headersRecord()` 반환값은 freeze **안 함** — `Headers`는 WHATWG spec상 freeze 불가 객체, `Record`은 호출자가 framework adapter로 push할 때 mutation 필요할 수 있음. 대신 매 호출마다 새 인스턴스 반환하여 캐시 오염 방지 + +## 전체 헤더 목록 + +### Default-ON (11개) + OWASP `headers_add.json` 정렬 표 + +OWASP `headers_add.json` 2026-03-05는 **13개**: CSP, COOP, COEP, CORP, Permissions-Policy, Referrer-Policy, HSTS, X-Content-Type-Options, X-DNS-Prefetch-Control, X-Frame-Options, X-Permitted-Cross-Domain-Policies, **Cache-Control**, **Clear-Site-Data**. + +본 라이브러리는 OWASP 13개 중 **11개만 Default-ON**으로 채택, COEP·Cache-Control·Clear-Site-Data 3종은 의도적으로 Default-OFF(opt-in). 사유는 아래 deviation 표에 명시 — `compliance: 'tracks OWASP headers_add.json with 3 deliberate opt-ins'`. + plan-extension 1종(Origin-Agent-Cluster, HTML spec 권장). + +| # | 헤더 | 기본값 | 근거 | +|---|---|---|---| +| 1 | `Content-Security-Policy` | `default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; manifest-src 'self'; upgrade-insecure-requests` | OWASP 2026 공식값 + `manifest-src 'self'` (PWA 안전망 — `default-src`는 manifest-src로 fallback 안 됨, 미명시 시 PWA 사일런트 깨짐) | +| 2 | `Cross-Origin-Opener-Policy` | `same-origin` | OWASP | +| 3 | `Cross-Origin-Resource-Policy` | `same-origin` | OWASP `headers_add.json` (cheat sheet `same-site`보다 JSON 우선) | +| 4 | `Origin-Agent-Cluster` | `?1` | HTML spec. Chromium origin-keyed default 2025+, Firefox 138+ shipped. plan-extension (OWASP 미포함). `false` 시 `?0` (sf-boolean opt-out) | +| 5 | `Permissions-Policy` | 아래 Tier 표 참조 (OWASP `unload`+`interest-cohort` 포함 29 vs plan curated 54+) | OWASP. **Chromium+Firefox 지원, Safari 미지원** | +| 6 | `Referrer-Policy` | `no-referrer` | OWASP. **Tradeoff**: 가장 엄격하지만 일부 애널리틱스/OAuth 콜백 깨짐 가능. Mozilla/ACSC ISM-1788은 `strict-origin-when-cross-origin` 권장 → `presets.acsc()` 사용 | +| 7 | `Strict-Transport-Security` | `max-age=63072000; includeSubDomains` (2년) | Mozilla / RFC 6797. `preload` 토큰은 RFC 비포함, hstspreload.org 컨벤션 | +| 8 | `X-Content-Type-Options` | `nosniff` | OWASP | +| 9 | `X-DNS-Prefetch-Control` | `off` | OWASP. 보안보단 프라이버시/성능이나 OWASP 권장 유지 | +| 10 | `X-Frame-Options` | `deny` | OWASP/Mozilla 정규형(소문자). CSP `frame-ancestors 'none'`과 일치. **WAF 호환 주의**: Cloudflare/Akamai 일부 시그니처가 정확히 `DENY` 매칭 → `xFrameOptions: 'DENY'` 입력 시 그대로 송출하여 호환 | +| 11 | `X-Permitted-Cross-Domain-Policies` | `none` | OWASP. Flash/Silverlight EOL이나 PCI/Qualys 스캐너가 체크 | + +#### OWASP deviation 명시 + +| OWASP add.json | 본 라이브러리 | 사유 | +|---|---|---| +| `Cross-Origin-Embedder-Policy: require-corp` (default-add) | Default-OFF | 서드파티 임베드(이미지/폰트/CDN) 광범위 차단 위험. cross-origin isolation 필요 시 명시 활성. **Safari `credentialless` 미지원** | +| `Cache-Control: no-store, max-age=0` (default-add) | Default-OFF | 보안 비민감 응답까지 일괄 no-store 적용은 캐시 효율성 저하. 민감 라우트별 opt-in 또는 `presets.bsi()`/`presets.kisa()` 활성. PCI DSS §6.4.3 / KISA PIPA §7(6) 적용 라우트는 명시 활성 | +| `Clear-Site-Data` (default-add) | Default-OFF (action 헤더) | 모든 응답에 송출하면 매 요청마다 캐시/쿠키/스토리지 삭제 → 사용자 세션 파괴. 로그아웃 endpoint 전용 action 헤더로 opt-in | +| `interest-cohort` Permissions-Policy 항목 | 플랜 미포함 | FLoC 2022년 폐기, registry에서 완전 제거됨. OWASP `headers_add.json`에는 잔존(legacy noise) → 사용자가 `permissionsPolicy.features['interest-cohort']: []`로 명시 추가 가능 | + +#### CSP 기본값 설계 근거 + +OWASP 2026 공식 CSP를 기본값으로 채택. `default-src 'self'`에 의존하여 명시하지 않은 fetch 디렉티브(font-src, img-src, script-src, style-src 등)는 자동으로 `'self'`가 적용된다. + +| 디렉티브 | 값 | 근거 | +|---|---|---| +| `default-src` | `'self'` | OWASP 기본. font-src, img-src, script-src, style-src 등 미명시 fetch 디렉티브에 fallback | +| `base-uri` | `'self'` | base tag injection 방지 | +| `form-action` | `'self'` | 폼 제출 대상 제한 | +| `frame-ancestors` | `'none'` | OWASP 공식. X-Frame-Options `deny`와 일치 | +| `object-src` | `'none'` | Flash/Java 플러그인 완전 차단 | +| `manifest-src` | `'self'` | **PWA 호환 안전망**. CSP3 §6.6에 의해 `default-src`로 fallback **되지 않음** — 미명시 시 PWA `manifest.webmanifest` fetch 차단 (사일런트 깨짐). plan-extension (OWASP 미포함) | +| `upgrade-insecure-requests` | (boolean) | HTTP→HTTPS 자동 업그레이드. HSTS와 비충돌 (HSTS=호스트별, UIR=페이지별 navigations) — 둘 다 송출 권장 | + +> Google Fonts(`font-src 'self' https: data:`), 인라인 SVG(`img-src 'self' data:`) 등 외부 리소스가 필요한 경우 사용자가 디렉티브 override. + +### Default-OFF (opt-in, 22개) + +| # | 헤더 | 활성 시 기본값 | 이유 | +|---|---|---|---| +| 12 | `Cross-Origin-Embedder-Policy` | `require-corp` | OWASP 권장이나 서드파티 리소스 깨짐 위험으로 opt-in. cross-origin isolation(SharedArrayBuffer) 필요 시 활성. Safari `credentialless` 미지원 | +| 13 | `Content-Security-Policy-Report-Only` | 사용자 지정 | CSP 정책 모니터링 | +| 14 | `Cross-Origin-Opener-Policy-Report-Only` | 사용자 지정 | COOP 모니터링 | +| 15 | `Cross-Origin-Embedder-Policy-Report-Only` | 사용자 지정 | COEP 모니터링 | +| 16 | `Permissions-Policy-Report-Only` | 사용자 지정 | Permissions-Policy 모니터링 (Chromium 전용) | +| 17 | `Reporting-Endpoints` | 사용자 지정 | CSP/COOP/COEP/Integrity-Policy/NEL 위반 리포트 수신. RFC 9651 Dictionary. `default` endpoint는 fallback 컨벤션 | +| 18 | `Integrity-Policy` | `true` → `{ blockedDestinations: ['script', 'style'] }` | SRI 강제. RFC 9651 Dictionary, `sources` Inner List, `endpoints` Inner List of Token. Q1 2026 universal | +| 19 | `Integrity-Policy-Report-Only` | 사용자 지정 | SRI 모니터링 | +| 20 | `Clear-Site-Data` | `true` → `{ directives: ['cache', 'cookies', 'storage'] }` | 로그아웃 시 브라우저 데이터 삭제. W3C 표준 토큰: `cache`, `cookies`, `storage`, `executionContexts`, `clientHints`, `*`. `prefetchCache`/`prerenderCache`는 Chrome 비표준 | +| 21 | `Cache-Control` | `no-store, max-age=0` | OWASP. PCI DSS §6.4.3 / KISA PIPA §7(6) 적용 라우트는 활성. `pragma`/`expires`로 HTTP/1.0 호환 | +| 22 | `NEL` | 사용자 지정 | 네트워크 에러 로깅 (Chromium 전용). 레거시 `Report-To` 헤더에 의존 — NEL 활성 시 자동 생성 | +| 23 | `Document-Policy` | 사용자 지정 | WICG stalled 2022, Chromium 전용 실험 기능. RFC 9651 Dictionary | +| 24 | `Document-Policy-Report-Only` | 사용자 지정 | Document-Policy 모니터링 | +| 25 | `Require-Document-Policy` | 사용자 지정 | 중첩 콘텐츠 정책 요구 (실험적) | +| 26 | `Document-Isolation-Policy` | 사용자 지정 | 프레임별 격리 (Chrome 137+ stable). `isolate-and-require-corp` / `isolate-and-credentialless` / `none` | +| 27 | `Document-Isolation-Policy-Report-Only` | 사용자 지정 | Document-Isolation-Policy 모니터링 | +| 28 | `Timing-Allow-Origin` | 사용자 지정 | Resource Timing 사이드채널 방지 | +| 29 | `X-Robots-Tag` | `true` → `{ directives: ['noindex', 'nofollow'] }` | 민감 엔드포인트 크롤러 차단 | +| 30 | `X-Download-Options` | `noopen` | IE EOL(2022)이나 보안 스캐너(Qualys) 호환. v2.0(major) 제거 후보 | +| 31 | `X-XSS-Protection` | `0` | 레거시 XSS Auditor 명시 비활성. **OWASP 2026 `headers_add.json` 미포함**. CSP가 표준. 보안 스캐너 호환 opt-in. v2.0 제거 후보. **KISA 호환 모드**(`presets.kisa()`)는 `1; mode=block` 송출 — Korean public-sector 스캐너 대응 | +| 32 | `X-Permitted-Cross-Domain-Policies` (override) | `none` (Default-ON과 동일) | 사용자가 라우트별 `master-only`/`by-content-type`/`all` 설정 시 | +| 33 | `Server-Timing` (제거 권장 — 정보 제공) | — | OWASP cheat sheet 권장. 내부 타이밍 노출 위험. 본 라이브러리는 자동 송출 미실시, removeHeaders 기본 후보군에 포함 | + +### 기능 (헤더 제거, 1개) + +| # | 기능 | 기본 | 설명 | +|---|---|---|---| +| 34 | `removeHeaders` | OWASP must-strip 4종 + plan 확장 | 정보 노출 헤더 제거. `apply(response)`에서 동작 | + +#### removeHeaders 정책 + +- **기본값** (`removeHeaders: true` 또는 미지정): `Server`, `X-Powered-By`, `X-AspNet-Version`, `X-AspNetMvc-Version` (OWASP must-strip 4종) +- **`removeHeaders: 'owasp'`**: OWASP `headers_remove.json` 전체 70종 (자동 sync test 대상) +- **`removeHeaders: { headers: [...], additional: [...] }`**: 사용자 정의 +- 매칭: case-insensitive exact match. 와일드카드(`X-B3-*`, `X-Envoy-*`)는 constants에 known 이름 전개 +- **Server 헤더 제거 제약**: Bun.serve / Node http2가 미들웨어 이후 `Server`를 prepend하는 경우 `removeHeaders`로 못 막음. README에 런타임별 우회 레시피 명시 + +#### OWASP `headers_remove.json` 2026-03-05 (70종) + +`$wsep`, `Host-Header`, `K-Proxy-Request`, `Liferay-Portal`, `OracleCommerceCloud-Version`, `Pega-Host`, `Powered-By`, `Product`, `Server`, `Server-Timing`, `SourceMap`, `Via`, `X-AspNet-Version`, `X-AspNetMvc-Version`, `X-Atmosphere-error`, `X-Atmosphere-first-request`, `X-Atmosphere-tracking-id`, `X-B3-ParentSpanId`, `X-B3-Sampled`, `X-B3-SpanId`, `X-B3-TraceId`, `X-BEServer`, `X-Backside-Transport`, `X-CF-Powered-By`, `X-CMS`, `X-CalculatedBETarget`, `X-Cocoon-Version`, `X-Content-Encoded-By`, `X-DiagInfo`, `X-Envoy-Attempt-Count`, `X-Envoy-External-Address`, `X-Envoy-Internal`, `X-Envoy-Original-Dst-Host`, `X-Envoy-Upstream-Service-Time`, `X-FEServer`, `X-Framework`, `X-Generated-By`, `X-Generator`, `X-Jitsi-Release`, `X-Joomla-Version`, `X-Kubernetes-PF-FlowSchema-UI`, `X-Kubernetes-PF-PriorityLevel-UID`, `X-LiteSpeed-Cache`, `X-LiteSpeed-Cache-Control`, `X-LiteSpeed-Purge`, `X-LiteSpeed-Tag`, `X-LiteSpeed-Vary`, `X-Litespeed-Cache-Control`, `X-Mod-Pagespeed`, `X-Nextjs-Cache`, `X-Nextjs-Matched-Path`, `X-Nextjs-Page`, `X-Nextjs-Redirect`, `X-OWA-Version`, `X-Old-Content-Length`, `X-OneAgent-JS-Injection`, `X-Page-Speed`, `X-Php-Version`, `X-Powered-By`, `X-Powered-By-Plesk`, `X-Powered-CMS`, `X-Redirect-By`, `X-Runtime`, `X-Server-Powered-By`, `X-SourceFiles`, `X-SourceMap`, `X-Turbo-Charged-By`, `X-Umbraco-Version`, `X-Varnish`, `X-Varnish-Backend`, `X-Varnish-Cache`, `X-Varnish-Server`, `X-Woodpecker-Version`, `X-dtAgentId`, `X-dtHealthCheck`, `X-dtInjectedServlet`, `X-ruxit-JS-Agent` + +#### Plan 확장 (OWASP 미포함 trace ID) + +`X-Request-ID`, `X-Correlation-ID`, `X-Envoy-Decorator-Operation` — opt-in. + +## Permissions-Policy 피처 + +**중요**: Permissions-Policy HTTP 헤더는 Chromium + Firefox 지원, Safari 미지원 (~85% 글로벌). Tier 모델은 W3C registry 분류 + 브라우저 지원으로 재정의. + +### Registry ↔ 본 라이브러리 emit 매핑 (산정 표) + +W3C `webappsec-permissions-policy/features.md` registry 카운트와 본 라이브러리 emit 카운트를 reconcile. **L48 "Standardized 38 + Proposed 13 + Experimental 16 + Retired 2"**의 출처는 registry, **본 라이브러리 emit 53(Tier A 6 + B 35 + C 18) + Tier D off-list**의 출처는 본 라이브러리 union 타입. + +| registry 분류 | registry 카운트 | 본 라이브러리 처리 | emit 카운트 | 차이 사유 | +|---|---|---|---|---| +| **Standardized** | **38** | Tier A (6) + Tier B (35) + ch-ua-* 11종 의도적 제외 (Client Hints 영역) | 41 | 38 - 11 ch-ua + 14 = 41이 아니라, Standardized 38은 ch-ua 11 포함 → 38 - 11 = 27 emit. 본 라이브러리 Tier B 35는 27 (Standardized 비-ch-ua) + 8 (registry 외이지만 stable Standardized로 본 라이브러리가 분류한 항목 — `cross-origin-isolated`, `attribution-reporting` 등 일부 registry 분류와 본 라이브러리 분류 차) **검증 필요** | +| **Proposed** | **13** | Tier C에 9종 포함 (`gamepad`, `clipboard-read/write`, `speaker-selection`, `deferred-fetch`, `language-model`, `language-detector`, `summarizer`, `translator`, `writer`, `rewriter`, `autofill` 11종) | 11 | Proposed 13 중 2종(`manual-text` 등)은 보안성 낮음 → Tier D off-list | +| **Experimental** | **16** | Tier C에 7종 포함 (`local-fonts`, `unload`, `browsing-topics`, `captured-surface-control`, `smart-card`, `all-screens-capture`) | 6 | Experimental 16 중 OT 종료 미진입/Privacy Sandbox 광고 항목은 Tier D off-list | +| **Retired** | **2** | 의도적 제외 (`document-domain`, `window-placement`) | 0 | `window-management`로 rename된 신규 키만 Tier B emit | +| **off-registry** (Chromium intent / IWA 전용 / WICG draft) | — | Tier D — `(string & {})` union 폴백으로 사용자 명시 추가 가능, 본 라이브러리 default 미포함 | 0 | `controlled-frame`, `device-attributes`, `on-device-speech-recognition`, `deferred-fetch-minimal` 등 | + +> **registry 카운트 검증 절차**: `test/permissions-policy-registry-sync.test.ts`가 weekly `webappsec-permissions-policy/features.md` fetch → table 파싱 → 본 라이브러리 union과 diff. drift 발견 시 PR auto-create (OWASP sync 패턴 동일). 위 표의 "검증 필요" 항목은 첫 sync 실행 시 정확한 분포 확정. + +### Tier A (Universal — 모든 엔진 supported, 6) + +`*-credentials-get`/`*-credentials-create` 계열. Firefox/Chromium/Safari 모두 헤더 파싱 (실제 enforcement는 엔진별). + +| 피처 | 기본값 | 출처 | +|---|---|---| +| `publickey-credentials-get` | `()` | Standardized (registry) | +| `publickey-credentials-create` | `()` | Proposed (WebAuthn-3) | +| `identity-credentials-get` | `()` | Standardized | +| `digital-credentials-get` | `()` | Experimental | +| `digital-credentials-create` | `()` | Experimental | +| `otp-credentials` | `()` | Standardized | + +### Tier B (Chromium + Firefox parsed, 35) + +W3C registry **Standardized** 섹션에서 Universal 6종을 제외한 나머지. Firefox는 헤더를 받아들이며 일부 directive name을 인식. + +`accelerometer`, `ambient-light-sensor`, `attribution-reporting`, `autoplay`, `battery`, `bluetooth`, `camera`, `compute-pressure`, `cross-origin-isolated`, `direct-sockets`, `display-capture`, `encrypted-media`, `execution-while-not-rendered`, `execution-while-out-of-viewport`, `fullscreen`, `geolocation`, `gyroscope`, `hid`, `idle-detection`, `keyboard-map`, `magnetometer`, `mediasession`, `microphone`, `midi`, `navigation-override`, `payment`, `picture-in-picture`, `screen-wake-lock`, `serial`, `storage-access`, `sync-xhr`, `usb`, `web-share`, `window-management`, `xr-spatial-tracking` + +기본값: 모두 `()` (완전 비활성). 예외: `sync-xhr=(self)` (OWASP 공식값, 레거시 라이브러리 호환). + +> **registry 정정 (vs 기존 plan)**: `gamepad`는 W3C registry **Proposed** 섹션 — Tier C로 이동. `battery`/`direct-sockets`/`mediasession`/`navigation-override`/`execution-while-not-rendered`/`execution-while-out-of-viewport`는 registry **Standardized** — 기존 "unimplemented/cancelled" 사유로 제외했으나 잘못, Tier B에 포함. `direct-sockets`는 IWA 전용이지만 매우 강력(raw TCP/UDP)하므로 default-deny 가치 큼. + +### Tier C (Chromium-only stable, Proposed 13 + Experimental shipped 일부) + +W3C registry **Proposed** 섹션 + **Experimental**에서 Chromium stable shipped. + +| 피처 | 기본값 | registry | Chrome | 비고 | +|---|---|---|---|---| +| `gamepad` | `()` | Proposed | 86+ | (registry 정정으로 Tier C 이동) | +| `clipboard-read` | `()` | Proposed | 86+ | | +| `clipboard-write` | `()` | Proposed | 86+ | | +| `local-fonts` | `()` | Experimental | 103+ | Local Font Access API | +| `unload` | `()` | Experimental | 117+ | OWASP 2026 공식 포함 | +| `browsing-topics` | `()` | Experimental | 115+ | Topics API. **`interest-cohort` 후계자**, FLoC 2022 폐기 | +| `captured-surface-control` | `()` | Experimental | 122+ OT | Conditional Focus | +| `smart-card` | `()` | Experimental | 134+ OT | IWA | +| `speaker-selection` | `()` | Proposed | 미구현 | Speaker Selection API (selectAudioOutput) | +| `all-screens-capture` | `()` | Experimental | OT | Multi-screen capture | +| `deferred-fetch` | `()` | Proposed | 130+ | Fetch Later API. spec default `self` (5MB quota) | +| `language-model` | `()` | Proposed | 138+ | Built-in AI Prompt API | +| `language-detector` | `()` | Proposed | 138+ stable | Built-in AI Language Detector | +| `summarizer` | `()` | Proposed | 138+ stable | Built-in AI Summarizer | +| `translator` | `()` | Proposed | 138+ stable | Built-in AI Translator | +| `writer` | `()` | Proposed | 138+ | Built-in AI Writer | +| `rewriter` | `()` | Proposed | 138+ | Built-in AI Rewriter | +| `autofill` | `()` | Proposed | 미구현 | 인증/credential 보호 벡터 (embedded contexts) | + +### Tier D (Origin Trial / Experimental / off-registry, 신중 검토 후 선택 활성) + +기본 비포함 — 사용자가 features map에 직접 추가 (`PermissionsPolicyFeature` union의 `(string & {})` 폴백). + +- `controlled-frame` — IWA 전용, **W3C registry 외**, chromestatus만 +- `deferred-fetch-minimal` — MDN/chromestatus, registry 외(Issue #544) +- `device-attributes` — Chrome 신규 (Feb 2026 intent), registry 외 +- `on-device-speech-recognition` — Chrome 신규, registry 외 +- `manual-text` — Proposed (registry), 비보안 영역 +- `conversion-measurement`, `focus-without-user-activation`, `monetization`, `sync-script`, `vertical-scroll`, `trust-token-redemption`, `join-ad-interest-group`, `run-ad-auction` — Privacy Sandbox/광고/niche +- `private-state-token-issuance`, `private-state-token-redemption`, `shared-storage`, `shared-storage-select-url`, `private-aggregation` — Privacy Sandbox 광고 + +### 의도적 제외 + +| 항목 | 사유 | +|---|---| +| `interest-cohort` | FLoC 2022 폐기, registry에서 완전 삭제(Retired 표 아님). `browsing-topics`로 대체 | +| `document-domain` | W3C registry **Retired**. `document.domain` setter 자체 폐기 경로 | +| `window-placement` | **Retired**. `window-management`(Tier B)로 이름 변경 | +| `ch-ua-*` (11종) | Standardized이나 Client Hints 영역, `Accept-CH`/`Critical-CH`로 처리 | + +> **DX 참고**: 일반 사용자는 Default(Tier A+B 비활성)를 그대로 사용. 특정 피처만 허용: `permissionsPolicy: { features: { camera: ['self'], microphone: ['self'] } }`. 명시 안 한 피처는 기본값 `()` 유지. + +## 의도적 제외 헤더 + +| 제외 | 사유 | +|---|---| +| CORS 헤더 | `@zipbul/cors` 담당 | +| Access-Control-Allow-Private-Network | CORS 확장 → `@zipbul/cors` | +| Set-Cookie 속성 | 세션 미들웨어. 본 라이브러리 README는 RFC 6265bis-22 prefix(`__Host-`/`__Secure-`) 권장만 명시 | +| Sec-Fetch-* | 요청 헤더 | +| Client Hints (Accept-CH, Critical-CH 등) | 콘텐츠 협상/신뢰성, 보안 정책 아님 | +| WebSocket 헤더 | 프로토콜 레벨 | +| HTTP Signatures (RFC 9421) | API/서비스 간 통신, 브라우저 보안 아님 | +| Content-Digest / Repr-Digest (RFC 9530) | API 무결성 | +| Privacy Sandbox 광고 헤더 (Observe-Browsing-Topics 등) | 광고 측정, 본 라이브러리 범위 외 | +| Set-Login, IdP-SignIn-Status (FedCM) | IdP 전용, niche | +| Activate-Storage-Access | Storage Access API 응답 (action) | +| Service-Worker-Allowed, Service-Worker-Navigation-Preload | 앱 기능, 보안 아님 | +| Speculation-Rules | 성능, 보안 아님. CSP `'inline-speculation-rules'` 키워드만 본 라이브러리 `Csp` 상수에 포함 | +| Web Bundles / Signed Exchanges (Signature, Signed-Headers) | 패키징, 비표준 진행중 | +| No-Vary-Search | 캐시 키 | +| Use-As-Dictionary / Available-Dictionary (RFC 9842) | Compression Dictionary, same-origin 강제 | +| Alt-Svc / Alt-Used / Early-Data | 전송 인프라 | +| Content-Disposition | 라우트별 결정 | +| X-UA-Compatible | IE 폐기 | +| Supports-Loading-Mode | 성능 opt-in | +| Cookie-Indices (현재 미존재) / draft-ietf-httpbis-layered-cookies-01 | 쿠키 영역 | +| **Feature-Policy** | Permissions-Policy로 대체 | +| **Expect-CT** | 폐기 (Chrome 107) | +| **HPKP (Public-Key-Pins)** | 폐기 (Chrome 72, Firefox 72) | +| **DNT** | 폐기 (Firefox 135 토글 제거) | +| **Sec-GPC** | 요청 헤더 | +| **Origin-Isolation** | 폐기, `Origin-Agent-Cluster`로 대체 | +| **`Report-To` (legacy 헤더)** | Reporting API ED 2026-01-02에서 제거. 단 NEL이 여전히 의존 → NEL 활성 시 reporting 모듈에서 자동 생성 | +| Take It Down Act / OSA / OFAC 관련 | 표준 HTTP 헤더 미존재 | + +## 옵션 구조 + +각 헤더: `true`(기본값) / `false`(비활성) / 문자열 또는 객체(커스텀). 모든 `boolean | string` 타입에 `@defaultValue` JSDoc + `@example` + `@see` 필수. + +### CSP 디렉티브 커스터마이징 전략: Replace + +사용자가 특정 디렉티브를 설정하면 해당 디렉티브의 기본값을 **완전히 교체**(replace), 병합 안 함. + +```typescript +// CSP 기본값: default-src 'self'가 모든 fetch 디렉티브에 fallback +// Google Fonts 사용 시 fontSrc 명시 +Helmet.create({ + contentSecurityPolicy: { + directives: { fontSrc: [Csp.Self, 'https://fonts.gstatic.com'] } + } +}); + +// 인라인 SVG/data: URI 이미지 +Helmet.create({ + contentSecurityPolicy: { + directives: { imgSrc: [Csp.Self, 'data:'] } + } +}); +``` + +설정 안 한 디렉티브는 기본값 유지. 설정한 디렉티브만 교체. + +### CSP 직렬화 / 검증 규칙 + +- **직렬화**: 디렉티브명 lowercase, 디렉티브 구분자 `;` (trailing 없음), 소스 구분자 single ASCII space +- **소스 dedup**: 동일 디렉티브 내 중복 제거 (키워드/quoted-source case-sensitive, scheme/host case-insensitive) +- **빈 fetch directive 배열** (`scriptSrc: []`): validate 거부 — fetch는 최소 1개 소스 필요. `'none'` 명시 안내 +- **빈 sandbox 배열** (`sandbox: []`): bare `sandbox` 디렉티브로 직렬화 (모든 sandbox 토큰 적용 = 가장 제한적) +- **소스 표현 검증**: + - 키워드(`'self'`, `'none'`, `'unsafe-inline'`, ...) 따옴표 필수. 따옴표 없는 키워드(`self`, `none`) 감지 시 에러 + `Csp.Self` 사용 안내 + - scheme-source 정규식: `^[a-zA-Z][a-zA-Z0-9+\-.]*:$` (linear, ReDoS-safe) + - host-source 정규식: scheme(optional) + host(`*` | `*.\w+` | `\w+`) + port(optional) + path(optional). **CI gate**: `recheck` 또는 `safe-regex2`로 정규식 catastrophic backtracking 검증. 입력 길이 상한 2048자 + - **nonce 값** (CSP3 §2.3.1 nonce-source ABNF: `base64-value = 1*(ALPHA / DIGIT / "+" / "/" / "-" / "_") *2"="`): + - 정규식: `^[A-Za-z0-9+/_-]{16,256}={0,2}$` — `=`를 끝에만 허용 (mid-string 거부), 16자 하한(128bit) + 256자 상한(DoS 방지) + - base64-std(`+/`)와 base64url(`-_`) 혼용 거부 (단일 alphabet 강제) + - 거부 문자: `'`, `"`, `\`, `<`, `>`, `;`, ` `(0x20), 모든 C0 controls(U+0000-U+001F NUL/CR/LF 등), DEL(U+007F), Unicode whitespace(U+00A0 NBSP, U+2028 LINE-SEP, U+2029 PARA-SEP, U+FEFF BOM) + - hash 값 길이: sha256=44, sha384=64, sha512=88 (base64 with padding). 동일 charset/제어문자 정책 적용 + - **헤더 이름 정규화**: HTTP/2/3은 lowercase 강제. `applyHeadersTo()` / `apply()`는 emit 직전 `toLowerCase()` 보장 +- **상호 작용 경고** (warn, not error — `helmet.warnings`에 누적): + - `'unsafe-inline'` + nonce/hash 동시 → 브라우저가 nonce/hash 우선이라 `'unsafe-inline'` 무시 (CSP3 §6.7.3) + - `'unsafe-eval'` + `'wasm-unsafe-eval'` 동시 → 후자 redundant + - `default` Trusted Types policy-name 사용 → 모든 sink-side string에 적용되는 default policy 위험성 + - COOP `same-origin` + COEP **OFF** → `crossOriginIsolated` false. SAB/멀티스레드 WASM 미동작 + - Reporting-Endpoints `default` endpoint 누락 → CSP/COOP/COEP `report-to` 미지정 시 fallback 안 됨 + - Permissions-Policy features map 키가 `PermissionsPolicyFeature` union에 없으면 (registry 외 또는 오타) + - Clear-Site-Data `prefetchCache`/`prerenderCache` 비표준 토큰 + - sandbox 디렉티브 in Report-Only → 브라우저가 무시 (CSP3 명시) + - `'unsafe-allow-redirects'` 사용 → 종속 디렉티브 `navigate-to`가 CSP3에서 제거되어 effective dead grammar +- **frame-ancestors 제약**: `'unsafe-inline'`, `'unsafe-eval'`, `'strict-dynamic'`, `'unsafe-hashes'`, nonce, hash 거부 (CSP3 §6.4.2). `'self'`, `'none'`, scheme/host-source만 허용 +- **폐기 디렉티브 거부 (validate 에러)**: + - `prefetch-src` (Chromium 112 제거, CSP3 미포함) + - `plugin-types` (CSP3 제거) + - `block-all-mixed-content` (CSP3 deprecated, `upgrade-insecure-requests` 사용) + - `referrer` (CSP1, `Referrer-Policy` 헤더로 대체) + - `reflected-xss` (CSP1, legacy) + - `navigate-to` (CSP3 제거) +- **fetch fallback chain 검증** (CSP3 §6.1, §6.6): + - `script-src-elem`/`-attr` → `script-src` → `default-src` + - `frame-src` → `child-src` → `default-src` + - `worker-src` → `child-src` → `script-src` → `default-src` + - `style-src-elem`/`-attr` → `style-src` → `default-src` + - `manifest-src`는 `default-src`로 fallback 안 됨 — 사용자가 manifest fetch + default-src만 설정 시 warn +- **Report-Only 제약**: `sandbox` 디렉티브는 Report-Only에서 무시됨 → warn. `frame-ancestors`, `upgrade-insecure-requests`는 honor (CSP3) +- **report-to 값**: `Reporting-Endpoints` token name. 정규식 `^[A-Za-z0-9_-]+$`로 검증 +- **report-uri 값**: 공백 구분 URI 리스트 (CSP3 deprecated이나 사양 잔존). WHATWG URL 절대 URL 또는 path-relative URI 파싱 가능. 빈 문자열, 잘못된 URL, javascript: 등 비-fetch 스킴 거부 +- **Trusted Types** (W3C ED 2026-02-24 §4.2): + - `requireTrustedTypesFor`: `'script'` 토큰 1개만 + - `trustedTypes`: policy-name 정규식 `^[A-Za-z0-9\-#=_/@.%]+$` 또는 `'allow-duplicates'` / `'none'` / `*` + - **`default` 정책 이름**: 예약어. `createPolicy()` 시 이름 미지정 default policy. validator는 사용 시 warn (보안적으로 default 정책은 모든 sink-side string에 적용) + - 빈 `trustedTypes;` 디렉티브는 유효 (`'none'`과 동등) + - **report-to group name vs Trusted Types policy-name**: report-to group `^[A-Za-z0-9_-]+$`(짧은 token), policy-name `^[A-Za-z0-9\-#=_/@.%]+$`(특수문자 포함) — validator 분리 + - Firefox 148 stable shipped (2026-02-24, unflagged) — Chromium/Safari/Firefox 모두 enforcement +- **Hash Reporting** (CSP3 ED §2.3.1): `'report-sha256'`/`'-sha384'`/`'-sha512'` 키워드는 매칭이 아니라 매칭에 사용된 hash를 `report-to` endpoint에 송출하라는 지시. `Csp.ReportSha256` 등 상수로 노출 +- **Strict CSP 권장 패턴** (JSDoc 문서화, 자동 적용 X): + + ```typescript + scriptSrc: [Csp.nonce(nonce), Csp.StrictDynamic, "'unsafe-inline'", 'https:'] + // - 'nonce-X' + 'strict-dynamic': 모던 브라우저 + // - 'unsafe-inline': nonce/hash 미지원 구형 브라우저 fallback (모던에서는 nonce에 의해 무시) + // - https: : 'strict-dynamic' 미지원 구형 브라우저 fallback + ``` + +- **A11y / 접근성 영향**: `'unsafe-inline'` 미허용 + nonce/hash strict CSP는 일부 a11y 도구(axe-core inline injection, 스크린 리더 북마클릿, NVDA browse-mode) 동작을 방해할 수 있음. CI에서 a11y 테스트 시 `presets.api()` 또는 사용자 정의 CSP-Report-Only로 영향 측정 후 적용 권장 + +- **Default-src override 주의**: 사용자가 `scriptSrc`만 override 시 `default-src 'self'` fallback 적용 안 됨(스펙). nonce 사용 시 `scriptSrc`/`styleSrc` 둘 다 명시 권장 +- **Service Worker + CSP**: SW는 페이지 CSP를 약화/override 불가. SW 내부 fetched scripts는 SW 자체 CSP(SW script response time)에 종속 + +### CSP-violation report 형식 + +- **Legacy `application/csp-report`** (CSP2): kebab-case (`blocked-uri`, `violated-directive`, `original-policy`, `disposition`, `effective-directive`, `referrer`, `status-code`, `source-file`, `line-number`, `column-number`) +- **Reporting API `application/reports+json`** v1: camelCase (`blockedURL`, `effectiveDirective`, `disposition`, `documentURL`, `referrer`, `sample`, `statusCode`) +- `Helmet.parseCspReport(req)`는 양쪽 포맷을 정규화한 typed union 반환 +- **입력 검증 (DoS / pollution 방어)**: + - **Content-Type 화이트리스트**: `application/csp-report`, `application/reports+json`만 수락. 그 외 (`text/plain`, `application/json` 등)는 `HelmetError(UnsupportedCspReportContentType)` throw — Reporting API 스펙 §3.4 enforcement + - **본문 크기 상한**: 64KB. 초과 시 `HelmetError(CspReportTooLarge)` throw. `request.body` ReadableStream을 chunked read하며 누적 크기 추적 — 전체 buffer 후 크기 검사하면 DoS + - **단일 보고서 객체 수 상한**: `reports+json`은 배열 — 항목 ≤ 100. 초과 시 truncate + 첫 100개만 반환 (선택적으로 `TooManyReports` warning) + - **JSON parse 실패**: `HelmetError(InvalidCspReport)` — raw 입력 echo 금지 (`message`는 길이만 노출) + - **prototype pollution**: parsed 객체는 `Object.create(null)`로 재구성 후 known field만 복사. `__proto__`/`constructor`/`prototype` 키 무시 + - **URL 필드 sanitize**: `blockedURL`/`documentURL`/`source-file`은 WHATWG URL 파서 통과 시만 보존, 실패 시 raw string으로 보존하되 length ≤ 2048 truncate + - **타임아웃**: body read는 10초 timeout (`AbortController`로 wrap). 초과 시 `HelmetError(CspReportTimeout)` + - body는 1회 소비 — 호출자가 재사용 필요 시 `request.clone()` 후 전달 + +### Permissions-Policy 직렬화 규칙 + +- **RFC 9651 Structured Field Dictionary** 형식 +- 빈 features map (`{}`) → 헤더 송출 생략 (빈 SF Dictionary는 invalid) +- Allowlist 직렬화: + - `[]` → `feature=()` (none) + - `['*']` → `feature=*` (all, bare token) + - `['self']` → `feature=(self)` (bare token, **따옴표 없음**) + - `['self', 'https://x.com']` → `feature=(self "https://x.com")` (origin은 sf-string으로 double-quoted) +- 검증 (보안 강화): + - **WHATWG URL parse-and-reserialize 강제**: `new URL(input).origin` 추출 → `origin === "null"` 거부, scheme이 `https:`/`http:`이 아닌 경우 거부, userinfo/path/query/fragment 제거. 단순 regex/문자열 split 사용 금지 (sf-string injection 벡터) + - **sf-string escape 명시 (RFC 9651 §3.3.3)**: `"` → `\"`, `\` → `\\`. 다른 문자는 escape 미허용 (RFC 9651는 매우 제한적). 출력 문자열에 raw `"` / `\` 잔존 시 emit 거부 (assertion failure) + - `self`, `*`는 bare token으로만 송출. 다른 키워드는 spec 위반 → 에러 + - 잘못된 입력(`'self'` quoted, raw URL 등) 감지 시 자동 보정 + warn 또는 validate 에러 + - **map 키 prototype 보호**: `features` map은 내부적으로 `Object.create(null)` 또는 `Map` 사용. `__proto__`, `constructor`, `prototype` 키 거부 + `HelmetError(InvalidPermissionsPolicyToken)` + +### Nonce 주입 규칙 + +`headers({ nonce })` / `headersRecord({ nonce })` / `apply(response, { nonce })` 호출 시: + +- `script-src`에 `'nonce-{value}'` 주입 +- `style-src`에 `'nonce-{value}'` 주입 +- 사용자가 명시 설정한 `script-src-elem`, `style-src-elem`, `script-src-attr`, `style-src-attr`에도 주입 +- 명시 설정 안 한 `-elem`/`-attr` 변형에는 주입 X (fallback으로 `script-src`/`style-src` 적용) +- `Content-Security-Policy-Report-Only` 설정 시 동일 규칙 (모니터링 정확성) +- nonce 주입 시 CSP/CSP-RO만 재생성, 나머지 캐시 반환 + +### TypeScript 타입 + +```typescript +// Branded types (충돌 방지) +type Nonce = string & { readonly __brand: 'Nonce' }; +type EndpointName = string & { readonly __brand: 'EndpointName' }; +type HttpsUrl = `https://${string}`; + +/** + * resolveHelmetOptions 결과 — `HelmetOptions` 정규화 + 모든 default 채워짐 + deep-readonly. + * 내부 representation은 사용자 입력과 다름: + * - features map / endpoints map / policies map은 `Map<...>` 또는 `Object.create(null)` + * - boolean true/false는 명시 객체로 확장 + * - kebab-case 디렉티브 키로 정규화 (camelCase 입력 → kebab-case) + * - 사용자 미설정 디렉티브는 OWASP default로 채워짐 (CSP) + * helmet.toJSON()이 이 형태를 반환 (deep-readonly). + */ +type ResolvedHelmetOptions = DeepReadonly<{ + contentSecurityPolicy: ResolvedCspOptions | false; + contentSecurityPolicyReportOnly: ResolvedCspOptions | undefined; + crossOriginOpenerPolicy: CoopValue | false; + crossOriginOpenerPolicyReportOnly: CoopValue | undefined; + crossOriginEmbedderPolicy: CoepValue | false; + crossOriginEmbedderPolicyReportOnly: CoepValue | undefined; + crossOriginResourcePolicy: CorpValue | false; + originAgentCluster: boolean; + permissionsPolicy: ResolvedPermissionsPolicyOptions | false; + permissionsPolicyReportOnly: ResolvedPermissionsPolicyOptions | undefined; + referrerPolicy: ReferrerPolicyToken[] | false; + strictTransportSecurity: ResolvedHstsOptions | false; + xContentTypeOptions: boolean; + xDnsPrefetchControl: 'on' | 'off' | false; + xFrameOptions: 'deny' | 'sameorigin' | 'DENY' | 'SAMEORIGIN' | false; + xPermittedCrossDomainPolicies: 'none' | 'master-only' | 'by-content-type' | 'all' | false; + reportingEndpoints: ResolvedReportingEndpointsOptions | undefined; + integrityPolicy: ResolvedIntegrityPolicyOptions | false | undefined; + integrityPolicyReportOnly: ResolvedIntegrityPolicyOptions | undefined; + clearSiteData: ResolvedClearSiteDataOptions | false | undefined; + cacheControl: ResolvedCacheControlOptions | false | undefined; + nel: ResolvedNelOptions | undefined; + documentPolicy: ResolvedDocumentPolicyOptions | undefined; + documentPolicyReportOnly: ResolvedDocumentPolicyOptions | undefined; + requireDocumentPolicy: ResolvedDocumentPolicyOptions | undefined; + documentIsolationPolicy: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none' | undefined; + documentIsolationPolicyReportOnly: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none' | undefined; + timingAllowOrigin: string[] | undefined; + xRobotsTag: ResolvedXRobotsTagOptions | false | undefined; + xDownloadOptions: boolean; + xXssProtection: '0' | '1; mode=block' | false; + removeHeaders: ResolvedRemoveHeadersOptions; + messageFormatter: HelmetOptions['messageFormatter']; +}>; + +/** 깊은 readonly 변환 유틸 */ +type DeepReadonly = T extends (...args: any[]) => any + ? T + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T; + +interface HelmetOptions { + // ── Default-ON (11) ── + /** @defaultValue 위 CSP 기본값 설계 근거 참조 */ + contentSecurityPolicy?: boolean | ContentSecurityPolicyOptions; + /** @defaultValue 'same-origin' */ + crossOriginOpenerPolicy?: boolean | CoopValue; + /** @defaultValue 'same-origin' */ + crossOriginResourcePolicy?: boolean | CorpValue; + /** + * sf-boolean (RFC 9651). `true` → `?1` (origin-keyed agent cluster opt-in). + * `false` → `?0` (site-keyed opt-out). Chromium origin-keyed default 2025+. + * @defaultValue true (`?1`) + */ + originAgentCluster?: boolean; + /** @defaultValue Tier A+B 비활성, sync-xhr=(self) */ + permissionsPolicy?: boolean | PermissionsPolicyOptions; + /** @defaultValue 'no-referrer'. ACSC ISM-1788 호환은 `presets.acsc()` 사용 */ + referrerPolicy?: boolean | ReferrerPolicyToken | ReferrerPolicyToken[]; + /** @defaultValue { maxAge: 63072000, includeSubDomains: true } */ + strictTransportSecurity?: boolean | StrictTransportSecurityOptions; + /** @defaultValue 'nosniff' */ + xContentTypeOptions?: boolean; + /** @defaultValue 'off' */ + xDnsPrefetchControl?: boolean | 'on' | 'off'; + /** + * 입력은 case-insensitive, 출력은 입력 case 그대로 송출 (WAF 호환). + * @defaultValue 'deny' (소문자 OWASP 정규형) + */ + xFrameOptions?: boolean | 'deny' | 'sameorigin' | 'DENY' | 'SAMEORIGIN'; + /** @defaultValue 'none' */ + xPermittedCrossDomainPolicies?: boolean | 'none' | 'master-only' | 'by-content-type' | 'all'; + + // ── Default-OFF (opt-in, 22) ── + /** @defaultValue 'require-corp' (활성 시). Safari `credentialless` 미지원 */ + crossOriginEmbedderPolicy?: boolean | CoepValue; + contentSecurityPolicyReportOnly?: ContentSecurityPolicyOptions; + crossOriginOpenerPolicyReportOnly?: CoopValue; + crossOriginEmbedderPolicyReportOnly?: CoepValue; + permissionsPolicyReportOnly?: PermissionsPolicyOptions; + reportingEndpoints?: ReportingEndpointsOptions; + integrityPolicy?: boolean | IntegrityPolicyOptions; + integrityPolicyReportOnly?: IntegrityPolicyOptions; + clearSiteData?: boolean | ClearSiteDataOptions; + cacheControl?: boolean | CacheControlOptions; + nel?: NelOptions; + documentPolicy?: DocumentPolicyOptions; + documentPolicyReportOnly?: DocumentPolicyOptions; + requireDocumentPolicy?: DocumentPolicyOptions; + documentIsolationPolicy?: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none'; + documentIsolationPolicyReportOnly?: 'isolate-and-require-corp' | 'isolate-and-credentialless' | 'none'; + timingAllowOrigin?: string | string[]; + xRobotsTag?: boolean | XRobotsTagOptions; + /** @defaultValue 'noopen' (IE EOL — v2.0 제거 후보) */ + xDownloadOptions?: boolean; + /** @defaultValue '0'. KISA 호환은 `presets.kisa()` ('1; mode=block') */ + xXssProtection?: boolean | '0' | '1; mode=block'; + + // ── 헤더 제거 ── + removeHeaders?: boolean | 'owasp' | RemoveHeadersOptions; + + // ── i18n 훅 (deferred contract) ── + /** + * 검증 메시지 포매터. ESL 사용자 / 다국어 필요 시 활용. + * - 미설정: 영문 default message + reason enum + * - 설정: `(reason, ctx) => translatedMessage` — reason enum이 i18n 키 역할 + * 라이브러리 자체는 i18n 파이프라인 미내장. + * + * **호출 시멘틱 (보안)**: + * - 사용자 함수는 `try/catch`로 wrap. throw 시 영문 default 메시지로 fallback + `HelmetWarning(MessageFormatterFailed)` 누적 + * - 반환값이 `string`이 아니면 (undefined / null / 객체) 동일 fallback + * - validate 자체는 fallback 후 정상 진행 — 사용자 함수 결함이 라이브러리 검증을 마비시키지 않음 + * - **사용자 함수에 raw user input은 전달 안 함** — `context.path`는 구조 경로(`scriptSrc[2]`), `context.meta`는 길이/인덱스만. echo injection 방지 + * - 동기 함수만 허용 — Promise 반환 시 fallback (validate 동기 보장 위해) + * - 호출 횟수 상한: violation 1건당 1회. messageFormatter가 의도적으로 felony loop를 만들 수 없음 + */ + messageFormatter?: (reason: HelmetErrorReason | HelmetWarningReason, context: { path: string; meta?: unknown }) => string; +} + +// 공통 sub-union 추출 (DRY) +type CoopValue = 'same-origin' | 'same-origin-allow-popups' | 'noopener-allow-popups' | 'unsafe-none'; +type CorpValue = 'same-origin' | 'same-site' | 'cross-origin'; +type CoepValue = 'require-corp' | 'credentialless' | 'unsafe-none'; +``` + +### 서브 옵션 인터페이스 + +```typescript +interface ContentSecurityPolicyOptions { + directives?: CspDirectives; +} + +interface CspDirectives { + // ── Fetch 디렉티브 (17) ── + defaultSrc?: CspSource[]; + childSrc?: CspSource[]; + connectSrc?: CspSource[]; + fencedFrameSrc?: CspSource[]; // WICG Fenced Frame draft (CSP3 ED 외) + fontSrc?: CspSource[]; + frameSrc?: CspSource[]; + imgSrc?: CspSource[]; + manifestSrc?: CspSource[]; // default-src로 fallback 안 됨 + mediaSrc?: CspSource[]; + objectSrc?: CspSource[]; + scriptSrc?: CspSource[]; + scriptSrcAttr?: CspSource[]; + scriptSrcElem?: CspSource[]; + styleSrc?: CspSource[]; + styleSrcAttr?: CspSource[]; + styleSrcElem?: CspSource[]; + workerSrc?: CspSource[]; + + // ── Document (2) ── + baseUri?: CspSource[]; + sandbox?: SandboxToken[]; + + // ── Navigation (2) ── + formAction?: CspSource[]; + frameAncestors?: CspSource[]; + + // ── Reporting (2) ── + reportTo?: string; // Reporting-Endpoints 키 이름 (URL 아님) + reportUri?: string; // CSP3 deprecated이나 잔존 (Safari 일부 의존) + + // ── WebRTC (1, CSP3 신규) ── + webrtc?: 'allow' | 'block'; + + // ── 기타 ── + upgradeInsecureRequests?: boolean; + /** Trusted Types W3C ED §4.2 */ + requireTrustedTypesFor?: TrustedTypesRequireToken[]; + /** Trusted Types W3C ED §4.2 (policy-name regex 별도) */ + trustedTypes?: TrustedTypesToken[]; +} + +/** + * CSP3 ED §2.3.1 keyword-source ABNF + URL/scheme/host expressions. + * Template literal union으로 컴파일 타임에 따옴표 누락 + scheme/host 형태 강제. + */ +type CspKeywordSource = + | "'self'" | "'none'" | "'unsafe-inline'" | "'unsafe-eval'" + | "'strict-dynamic'" | "'unsafe-hashes'" | "'report-sample'" + | "'wasm-unsafe-eval'" | "'inline-speculation-rules'" + | "'unsafe-webtransport-hashes'" + | "'report-sha256'" | "'report-sha384'" | "'report-sha512'"; +type CspNonceSource = `'nonce-${string}'`; +type CspHashSource = `'sha256-${string}'` | `'sha384-${string}'` | `'sha512-${string}'`; +type CspSchemeSource = `${string}:`; +type CspHostSource = `https://${string}` | `http://${string}` | `wss://${string}` | `ws://${string}`; +type CspSource = + | CspKeywordSource + | CspNonceSource + | CspHashSource + | CspSchemeSource + | CspHostSource + | '*' + | (string & {}); + +type TrustedTypesRequireToken = "'script'"; +type TrustedTypesToken = "'allow-duplicates'" | "'none'" | '*' | (string & {}); + +/** + * HTML Standard §iframe sandboxing 13 토큰 (canonical) + Storage Access API 확장 1 토큰 = 14 허용. + * CSP3 ED §6.3.2 sandbox는 HTML iframe sandbox 토큰 집합을 그대로 참조. + * `allow-storage-access-by-user-activation`은 HTML core 외, Storage Access API 스펙 확장 (Chromium/Safari 지원). + */ +type SandboxToken = + | 'allow-downloads' + | 'allow-forms' + | 'allow-modals' + | 'allow-orientation-lock' + | 'allow-pointer-lock' + | 'allow-popups' + | 'allow-popups-to-escape-sandbox' + | 'allow-presentation' + | 'allow-same-origin' + | 'allow-scripts' + | 'allow-storage-access-by-user-activation' // Storage Access API extension + | 'allow-top-navigation' + | 'allow-top-navigation-by-user-activation' + | 'allow-top-navigation-to-custom-protocols'; + +interface StrictTransportSecurityOptions { + /** default: 63072000 (2년, Mozilla 권장) */ + maxAge?: number; + /** default: true */ + includeSubDomains?: boolean; + /** + * default: false. **RFC 6797 비표준** (hstspreload.org 컨벤션). + * `true` 설정 시 validate가 hstspreload.org 요구사항 강제: + * - `maxAge >= 31536000` (1년) + * - `includeSubDomains: true` + * - 도메인은 base domain (apex)에서만 송출 권장 + * 위반 시 `HelmetError` (HstsPreloadRequirementMissing). + */ + preload?: boolean; +} + +type PermissionsPolicyFeature = + // ── Tier A (Universal, 6) ── + | 'publickey-credentials-get' | 'publickey-credentials-create' + | 'identity-credentials-get' | 'digital-credentials-get' | 'digital-credentials-create' + | 'otp-credentials' + // ── Tier B (Chromium + Firefox parsed, 35; W3C registry Standardized 제외 Tier A 6종) ── + | 'accelerometer' | 'ambient-light-sensor' | 'attribution-reporting' | 'autoplay' + | 'battery' | 'bluetooth' | 'camera' | 'compute-pressure' | 'cross-origin-isolated' + | 'direct-sockets' | 'display-capture' | 'encrypted-media' + | 'execution-while-not-rendered' | 'execution-while-out-of-viewport' + | 'fullscreen' | 'geolocation' | 'gyroscope' | 'hid' | 'idle-detection' + | 'keyboard-map' | 'magnetometer' | 'mediasession' | 'microphone' | 'midi' + | 'navigation-override' | 'payment' | 'picture-in-picture' | 'screen-wake-lock' + | 'serial' | 'storage-access' | 'sync-xhr' | 'usb' | 'web-share' + | 'window-management' | 'xr-spatial-tracking' + // ── Tier C (Chromium-only stable, 18) ── + | 'gamepad' | 'clipboard-read' | 'clipboard-write' | 'local-fonts' + | 'unload' | 'browsing-topics' + | 'captured-surface-control' | 'smart-card' | 'speaker-selection' + | 'all-screens-capture' | 'deferred-fetch' + | 'language-model' | 'language-detector' | 'summarizer' | 'translator' + | 'writer' | 'rewriter' | 'autofill'; + +type PermissionsPolicyAllowlist = '*' | 'self' | HttpsUrl | (string & {}); + +interface PermissionsPolicyOptions { + /** + * features map. 입력은 `Record`이지만 내부 저장은 `Map` — prototype pollution 방어. + * 검증: + * - `__proto__`, `constructor`, `prototype` 키 거부 (`HelmetError(InvalidPermissionsPolicyToken)`) + * - feature name 길이 상한 64자, allowlist 항목 수 상한 32개 (header bloat 방지) + * - allowlist origin은 WHATWG URL `new URL(input).origin` reserialize → `origin === "null"` / non-https 거부 + */ + features?: Partial>; +} + +interface ReportingEndpointsOptions { + /** + * RFC 9651 Structured Field Dictionary. URL은 sf-string 따옴표. + * 직렬화: `Reporting-Endpoints: default="https://example.com/reports", csp-endpoint="https://example.com/csp"` + * `default` 키는 Reporting API fallback 컨벤션. + * 검증: + * - 모든 URL은 HTTPS 강제 (HTTP 거부, validate 에러) + * - URL은 WHATWG URL `new URL(...)` 파싱 후 `.toString()` reserialize (raw 문자열 사용 안 함) + * - endpoint name 정규식: `^[A-Za-z0-9_-]{1,64}$` — 길이 상한 64자 + * - 내부 저장: `Map` (prototype pollution 방어) + * - `__proto__`, `constructor`, `prototype` 키 거부 + * - 전체 endpoints 수 상한: 32개 (header bloat + DoS 방지) + */ + endpoints: Record; +} + +interface IntegrityPolicyOptions { + /** + * RFC 9651 Structured Header Dictionary. 멤버는 Inner List of Token. + * 직렬화: `blocked-destinations=(script style), sources=(inline), endpoints=(default csp-endpoint)` + */ + blockedDestinations?: ('script' | 'style')[]; + /** SRI 적용 대상 소스. 현재 `inline`만 유효. 미명시 시 spec 기본값 `(inline)` 자동 적용 */ + sources?: ('inline')[]; + /** Reporting-Endpoints에 정의된 endpoint name(s). 복수 허용 (sf Inner List) */ + endpoints?: string[]; +} + +interface ClearSiteDataOptions { + /** + * W3C 표준 토큰: `cache`, `cookies`, `storage`, `executionContexts`, `clientHints`, `*`. + * `prefetchCache`, `prerenderCache`는 Chrome 비표준 확장 — validate 경고 (에러 아님). + * 직렬화 시 토큰은 sf-string 따옴표 (RFC 9651 List of sf-string). + */ + directives?: ('cache' | 'cookies' | 'storage' | 'executionContexts' | 'clientHints' | 'prefetchCache' | 'prerenderCache' | '*')[]; +} + +interface CacheControlOptions { + /** + * default: `'no-store, max-age=0'` (OWASP). + * RFC 9111 §5.2.2.5: `no-store` 단독 충분. OWASP 공식값 유지하되 사용자 단순화 가능 + */ + value?: string; + /** HTTP/1.0 호환: Pragma: no-cache. HTTP/1.1+에서는 무시됨 */ + pragma?: boolean; + /** HTTP/1.0 호환: Expires: 0. HTTP/1.1+에서는 Cache-Control 우선 */ + expires?: boolean; +} + +interface NelOptions { + /** Reporting-Endpoints에 정의된 endpoint name. validate에서 존재 검증 + Report-To 자동 생성 */ + reportTo: string; + /** 정책 적용 기간 (초) */ + maxAge: number; + includeSubdomains?: boolean; + /** + * 성공 응답 sampling rate (0.0–1.0). 프로덕션 권장: 0.1 (10%). + * 미명시 시 spec default(0.0, 성공 미리포트) + */ + successFraction?: number; + /** + * 실패 응답 sampling rate (0.0–1.0). 프로덕션 권장: 1.0 (모든 실패 분석 가치) + */ + failureFraction?: number; +} + +/** + * NEL/Reporting 운영 가이드 (Reporting API L1 §4.1): + * - reporting endpoint URL은 same-origin이거나 CORS-enabled 필수 + * - Reporting-Endpoints 헤더의 모든 endpoint URL은 HTTPS 필수 + * - NEL은 Chromium 전용 (Firefox/Safari 미수신) + * - Reporting-Endpoints 본체는 Chromium + Firefox 149+ (2026-03-24) + */ + +interface DocumentPolicyOptions { + /** + * RFC 9651 Dictionary. 값은 sf-item: boolean / integer / decimal / string / token / inner-list. + * 예: `{ 'document-write': false, 'js-profiling': true, 'force-load-at-top': false }` + * + * 보안: 내부 저장은 `Map` (prototype pollution 방어). + * `__proto__`, `constructor`, `prototype` 키 거부. policies 수 상한 64개 + */ + policies: Record; +} + +interface XRobotsTagOptions { + directives?: string[]; +} + +interface RemoveHeadersOptions { + /** + * 제거할 헤더 목록 (사용자가 전체 제어 시). + * 기본: `['Server', 'X-Powered-By', 'X-AspNet-Version', 'X-AspNetMvc-Version']` (must-strip 4종) + * 입력은 case-insensitive — 라이브러리가 lowercase로 정규화. + */ + headers?: string[]; + /** + * 기본 목록에 추가로 제거할 헤더 (병합). + * `removeHeaders: 'owasp'`로 `headers_remove.json` 70종 프리셋 사용. + */ + additional?: string[]; +} + +type ReferrerPolicyToken = + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; + +// ── CSP 키워드 상수 (타입 안전성) ── +// 키워드 따옴표 누락은 보안 구멍 — validate에서 감지 시 에러. +// CSP3 ED §2.3.1 keyword-source 전수 반영. +const Csp = { + // 표준 키워드 (CSP3 ED §2.3.1) + Self: "'self'", + None: "'none'", + UnsafeInline: "'unsafe-inline'", + UnsafeEval: "'unsafe-eval'", + UnsafeHashes: "'unsafe-hashes'", + StrictDynamic: "'strict-dynamic'", + ReportSample: "'report-sample'", + WasmUnsafeEval: "'wasm-unsafe-eval'", + // WebTransport 통합 (CSP3 ED §2.3.1) + UnsafeWebTransportHashes: "'unsafe-webtransport-hashes'", + // Speculation Rules — CSP3는 keyword-source 확장 지점만 정의, 토큰 자체는 Speculation Rules / HTML Standard + InlineSpeculationRules: "'inline-speculation-rules'", + // CSP Hash Reporting (CSP3 ED §2.3.1) — 매칭이 아니라 사용된 hash를 report-to endpoint에 송출 지시 + ReportSha256: "'report-sha256'", + ReportSha384: "'report-sha384'", + ReportSha512: "'report-sha512'", + // 동적 + nonce: (value: string) => `'nonce-${value}'` as `'nonce-${string}'`, + hash: (algo: 'sha256' | 'sha384' | 'sha512', value: string) => + `'${algo}-${value}'` as `'${typeof algo}-${string}'`, +} as const; + +// 의도적 미포함 (출처 검증 후 제외): +// - 'unsafe-allow-redirects' — keyword-source 문법 잔존하나 종속 디렉티브 `navigate-to`가 CSP3 제거 → effective dead grammar +// - 'trusted-types-eval' — keyword-source의 일부이나 Trusted Types 스펙은 별도 메커니즘. 현재 브라우저 enforcement 없음 + +// 사용 예: +// scriptSrc: [Csp.Self, Csp.StrictDynamic] +// scriptSrc: [Csp.Self, Csp.nonce('abc123')] +// scriptSrc: [Csp.Self, Csp.InlineSpeculationRules] // +``` + +`crypto.randomUUID()` 비권장 — 122bit로 CSP3 §8 미달. + +### Cookie 보안 (범위 외, 권장) + +`Set-Cookie`는 본 라이브러리 미관여. draft-ietf-httpbis-rfc6265bis-22 prefix 권장: + +- `__Host-`: `Secure` + `Path=/` + Domain 미지정 → 서브도메인 cookie injection 방지 +- `__Secure-`: `Secure` 강제 + +`SameSite=Lax`/`Strict`, `HttpOnly`, `Secure` 조합을 세션 cookie에 적용. + +### SRI 사용 (Integrity-Policy + 빌드 통합) + +신규 콘텐츠는 **sha384** 권장 (W3C SRI Level 2 FPWD 2025-04-22 — CR 미진입 ED). sha256은 호환 유지. + +```html + +``` + +빌드 통합 — Vite/Rollup/webpack에서 번들 hash 추출: + +```typescript +const hash = await Helmet.hashFromString(scriptText, 'sha384'); +const helmet = Helmet.create({ + contentSecurityPolicy: { + directives: { scriptSrc: [Csp.Self, Csp.hash('sha384', hash)] } + } +}); +``` + +**알고리즘 매트릭스** (CSP3 ED §2.3.1 hash-algorithm + Web Crypto subtle.digest 런타임 지원): + +| 알고리즘 | CSP3 spec | Bun | Node | Workers | Deno | Safari/Web | 권장 | +|---|---|---|---|---|---|---|---| +| SHA-256 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 호환 | +| SHA-384 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **2026 권장** | +| SHA-512 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 고가치 | +| SHA-3 / Keccak | ✗ (CSP3 미정의) | ✗ | ✗ | ✗ | ✗ | ✗ | 미지원 | +| ML-DSA / SLH-DSA (PQ) | ✗ (2026 미정) | ✗ | ✗ | ✗ | ✗ | ✗ | 미지원 | + +`Csp.hash()`는 `'sha256' | 'sha384' | 'sha512'` 유니온만 허용. SHA-3 / 포스트퀀텀은 CSP3 spec 미정의 — 향후 W3C 결정 시 minor. + +`Helmet.hashFromString()` 입력 타입: `string | ArrayBuffer | Uint8Array | Blob | ReadableStream`. 파일 hash가 필요한 경우 사용자가 런타임별로 입력 변환 (Bun: `await Bun.file(path).arrayBuffer()`, Node: `await fs.readFile(path)`, Workers: `await fetch(url).then(r => r.arrayBuffer())`). + +**SubtleCrypto incremental API 부재 트레이드오프**: Web Crypto `subtle.digest`는 chunked/incremental 입력을 지원하지 않음 — 입력 전체를 buffer에 누적 후 단일 호출. `ReadableStream` 입력 시 라이브러리는 내부적으로 `for await (const chunk of stream)`로 Uint8Array에 concat하여 전달. 매우 큰 파일(>100MB)에는 메모리 비용 → 빌드 도구(Vite/Rollup)에서 파일별 분할 hash 권장. JS native SHA-2 폴백은 의도적으로 미제공 (의존성 0 유지). + +### 핫패스 최적화 + +요청당 `headers()` 할당 부담 시 `headersRecord()` (Hono `c.header(name, value)` 루프) 또는 `applyHeadersTo()` (in-place): + +```typescript +const record = helmet.headersRecord({ nonce }); +for (const [name, value] of Object.entries(record)) c.header(name, value); + +// 또는 in-place +helmet.applyHeadersTo(c.res.headers, { nonce }); +``` + +### Cross-origin isolation (SharedArrayBuffer) + +`crossOriginIsolated` 활성화 조건 (모두 충족): + +1. COOP `same-origin` (Default-ON ✓) +2. COEP `require-corp` 또는 `credentialless` (**Default-OFF** — 명시 활성 필요) +3. 또는 Document-Isolation-Policy `isolate-and-require-corp` / `isolate-and-credentialless` (Chromium 전용, Chrome 137+ stable) + +```typescript +Helmet.create({ crossOriginEmbedderPolicy: 'require-corp' }); +``` + +COOP `same-origin` + COEP OFF 시 `helmet.warnings`에 `CoopWithoutCoep` 누적. + +### Server 헤더 제거 제약 + +런타임이 미들웨어 이후 `Server`를 prepend하는 경우 `removeHeaders`로 못 막음: + +- **Bun.serve**: `serverHeader` 옵션으로 비활성 (`Bun.serve({ ..., serverHeader: '' })`) +- **Node http/http2**: 응답 후 `res.removeHeader('Server')` 또는 reverse proxy(nginx `server_tokens off;`) 우회 +- **Cloudflare Workers / Vercel Edge**: 플랫폼 자동 주입 (사용자 제어 불가) + +### WAF 호환 + +Cloudflare/Akamai 일부 시그니처가 `X-Frame-Options: DENY`(uppercase) 정확 매칭. `xFrameOptions: 'DENY'` 입력 시 입력 case 그대로 송출 (lowercase 정규화 안 함). + +### 응답 헤더 바이트 예산 + +기본 활성 헤더 + 큰 CSP + OWASP removeHeaders 프리셋 시 응답 헤더 합계 4–8KB 도달 가능: + +- Bun.serve: 64KB (기본) +- Node http: 16KB (--max-http-header-size로 조정) +- nginx `large_client_header_buffers`: 기본 4×8KB +- Cloudflare: 32KB (Worker 응답) +- AWS API Gateway: 10KB + +해결: CSP `frame-ancestors 'none'` 사용 시 X-Frame-Options 생략 가능 / Permissions-Policy 미사용 피처는 명시 안 함 / removeHeaders는 실제로 노출되는 헤더만. + +### 런타임 호환성 + +코어 모듈은 `globalThis.crypto`, `Headers`, `Response`만 사용 — 런타임 비종속: + +- Bun 1.x ✓ (primary target) +- Cloudflare Workers ✓ (compatibility_date `2024-09-23` 이상, `streams_enable_constructors` 권장) +- Deno 1.40+ / Deno Deploy ✓ +- Vercel Edge Runtime ✓ +- Node.js 19+ ✓ + +`Bun.*` API 미사용. 통합 예제만 Bun 우선 표기. + +### Cloudflare 운영 가이드 + +**Transform Rules 우선, Workers는 동적 정책 전용.** 정적 헤더 (Default-ON 11종 등)는 [Cloudflare Modify Response Header Transform Rules](https://developers.cloudflare.com/rules/transform/response-header-modification/)에서 엣지 캐시 레이어에 송출 — 컴퓨트 비용 0. Worker는 다음에만 사용: + +- 요청별 nonce가 필요한 CSP (SSR, SPA hydration) +- 테넌트별/A-B별 CSP-Report-Only +- Reporting-Endpoints 토큰 동적 회전 + +**Worker `Response.headers` immutability**: `fetch()`로 받은 Response의 헤더는 immutable. `applyHeadersTo(fetched.headers)`는 `TypeError: immutable` throw. 반드시 `helmet.apply(fetched)` 또는 `new Response(fetched.body, { headers: new Headers(fetched.headers) })`로 클론 후 사용. + +**Pages Functions vs Workers**: Pages는 `_headers` 파일이 있고 `cf-*` 자동 주입. `_headers`와 Helmet 출력 충돌 시 Pages가 prepend → `_headers`에서 동일 헤더 미설정 권장. + +**번들 사이즈 한도**: Worker 1MB compressed (Free: 1MB, Paid: 10MB). `@zipbul/helmet/csp` + `@zipbul/helmet/hsts`만 import 시 코어 < 8KB gzipped 목표. + +### 미래 헤더 watchlist (현재 미배출) + +다음 헤더는 표준 진행 중이나 본 라이브러리는 stable 진입까지 미배출: + +- **Connection-Allowlists** (Chrome OT 148-151) — deny-by-default 네트워크 방화벽 응답 헤더. OT 종료 후 stable 도달 시 minor 추가 +- **Sec-Fetch-Storage-Access** / **Activate-Storage-Access** (Chrome 133+ shipped) — Storage Access API. 동작 헤더이며 본 라이브러리 보안 정책 범위 외 (참고: Plan §의도적 제외 표 참조) +- **HTTPS-Upgrades / HTTPS-First** (Chrome 147 ESB, Chrome 154 default 2026-Q4) — 브라우저 기능. **HSTS는 여전히 필수** (HTTPS-Upgrades는 fallback 레이어, HSTS 대체 아님). 본 라이브러리 변경 없음 +- **Signature-based SRI Ed25519** — `'ed25519-...'` integrity prefix. SRI L2 CR 진입 + 브라우저 stable 시 `Csp.hash` 알고리즘 union 확장 + +### Vercel 운영 가이드 + +- **헤더 적용 순서**: `next.config.js headers()` → middleware.ts → Route Handler → Helmet. **Vercel은 `next.config.js`를 마지막에 적용**할 수 있어 Helmet 출력을 clobber. Helmet 사용 시 `next.config.js headers()` 제거 또는 보조 사용 +- Edge Functions vs Node Functions: Headers API parity 보장. `runtime: 'edge'` export 시 `globalThis.crypto` 사용 가능 +- `x-vercel-*` 헤더(`x-vercel-cache`, `x-vercel-id`, `x-vercel-edge-region` 등)는 `removeHeaders` 후보군에 추가 권장 (plan-extension) + +### Deno / JSR + +본 라이브러리는 **npm + JSR 양쪽 게시** 검토. `@zipbul/helmet`은 npm, `@zipbul/helmet`은 JSR(jsr.io). JSR slow types 정책에 부합 — 모든 export에 명시 타입 (no inferred types in public API). + +Deno 1.40+ `Headers.delete()`는 `Set-Cookie` 다중 값을 모두 삭제 (Deno 1.40 미만은 첫 항목만). `apply()` 알고리즘은 `getSetCookie()` + `append`로 다중 값 명시 보존 → 안전. + +### Bun 운영 가이드 + +```typescript +const helmet = Helmet.create(); + +Bun.serve({ + serverHeader: '', + async fetch(req) { + const url = new URL(req.url); + if (url.pathname.startsWith('/static/')) { + // Bun.file() static serving — Helmet 적용 + const file = Bun.file(`./public${url.pathname}`); + const res = new Response(file); + return helmet.apply(res); + } + return helmet.apply(new Response('hello')); + }, +}); +``` + +Bun dev hot-reload 시 헤더 캐시는 모듈 재로드와 함께 재생성 → 일관성 유지. + +## 프레임워크 통합 + +### Hono (Bun) + +```typescript +import { Hono } from 'hono'; +import { Helmet } from '@zipbul/helmet'; + +const helmet = Helmet.create(); +const app = new Hono(); + +app.use(async (c, next) => { + await next(); + const nonce = Helmet.generateNonce(); + c.set('nonce', nonce); // 템플릿에서 c.get('nonce') 사용 + helmet.applyHeadersTo(c.res.headers, { nonce }); +}); +``` + +### Elysia (Bun) + +```typescript +import { Elysia } from 'elysia'; +import { Helmet } from '@zipbul/helmet'; + +const helmet = Helmet.create(); +new Elysia() + .onAfterHandle(({ response }) => + response instanceof Response ? helmet.apply(response) : response + ); +``` + +### `Bun.serve` (raw) + +```typescript +const helmet = Helmet.create(); +Bun.serve({ + serverHeader: '', // Server 헤더 제거 + fetch(req) { + return helmet.apply(new Response('hello')); + }, +}); +``` + +### Express (Node, 어댑터 필요) + +Web `Response` 기반이므로 Express(Node `res`)와 직접 호환 안 됨. Hono 어댑터(`@hono/node-server`) 사용 권장. + +### Next.js (App Router, `@zipbul/helmet/next`) + +```typescript +// middleware.ts +import { NextResponse } from 'next/server'; +import { Helmet } from '@zipbul/helmet'; +import { withNonce } from '@zipbul/helmet/next'; + +const helmet = Helmet.create(); + +export function middleware(request: Request) { + const nonce = Helmet.generateNonce(); + const response = NextResponse.next({ + request: { headers: withNonce(request.headers, nonce) }, // x-nonce 헤더로 RSC에 전달 + }); + helmet.applyHeadersTo(response.headers, { nonce }); + return response; +} + +// app/layout.tsx +import { headers } from 'next/headers'; +const nonce = (await headers()).get('x-nonce') ?? ''; +return