Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .changeset/cookie-rfc-conformance-and-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
"@zipbul/cookie": major
---

First release of `@zipbul/cookie`. RFC 6265 / RFC 6265bis-22 compliant cookie parser, serializer, signer, and AEAD encryptor for Bun. Aggregates four unreleased commits (`d941150` → `9c4835c`) plus the conformance/hardening pass below.

## Initial feature set (commits d941150, b8fb79a, 8d2053e)

- HMAC signing with `sha256` / `sha384` / `sha512`; rotation via `secrets: string[]`.
- AES-256-GCM encryption via Web Crypto; rotation via `encryptionSecret: string | string[]`.
- Cookie prefix validation: `__Secure-`, `__Host-`.
- Server-level cookie defaults (`httpOnly`, `secure`, `sameSite`, `path`, `domain`, `maxAge`, `expires`, `partitioned`) with per-cookie overrides via `createCookie()`.
- `secure: 'auto'` resolution against per-request `SerializeContext.isSecure`.
- `SameSite=None` requires `Secure` (RFC 6265bis §5.7).
- Partitioned (CHIPS) requires `Secure`; domain/path header-injection defense (`;`, `\r`, `\n`).
- 400-day Max-Age cap (RFC 6265bis §5.5).
- Cookie-name RFC 9110 token validation (`%` excluded for Bun.CookieMap interop).
- 4096-byte serialized cookie cap.
- `CookieJar`: request-scoped container with `get()` (Result-typed), `set()`, `delete()`, `has()`, `getRaw()`, `getSetCookieHeaders()`. Auto sign + encrypt on outbound, auto decrypt + unsign on inbound.
- Removed legacy `parse()` / `parseOne()` (replaced by `CookieJar` + Bun.CookieMap).
- Zero `node:crypto` dependency.

## Cross-name replay defense + integer/integrity hardening (commit 9c4835c)

- HMAC signs `cookie.name + 0x00 + cookie.value` — closes cross-name signature replay.
- AES-GCM uses `cookie.name` as AAD (NIST SP 800-38D §5.2.1) — closes cross-name ciphertext replay.
- Signing/encryption secrets enforced ≥ 32 characters (NIST SP 800-132).
- `secrets` array supports rotation: encrypt/sign uses first key, verify/decrypt iterates all (constant-time, no early-exit — position-based timing oracle measured 4.76× → 1.03×).
- `Max-Age` must be a finite integer (RFC 6265 §5.2.2): rejects `NaN`, `Infinity`, decimals at both `createCookie()` and `serialize()`.
- `CookieJar` error step distinguishes `unsign` failure (`SignatureVerificationFailed`) from `decrypt` failure (`DecryptionFailed`).

## RFC / browser conformance — defects fixed (15)

- `secure: 'auto'` no longer overrides explicit `secure` values in either direction (caller intent preserved via internal metadata map).
- `serialize()` now applies configured `httpOnly` / `path` / `sameSite` / `partitioned` defaults via `createCookie()`.
- `Expires` attribute capped at 400 days (RFC 6265bis §5.5), not just `Max-Age`.
- `__Host-` prefix validation rejects empty-string `Domain` (defense-in-depth).
- `createCookie` strips `null` attribute values (previously passed through).
- `Domain` validated against RFC 1034/1123 LDH (rejects `a..b.com`, `-bad.com`, etc.).
- Cookie size limit corrected: name+value ≤ 4096 octets (RFC 6265bis §5.6) — header total no longer rejected as a side-effect.
- Per-attribute 1024-octet cap added (RFC 6265bis §5.6).
- Prefix detection is now case-insensitive (`__host-`, `__SECURE-` are detected and validated).
- Default public-suffix check (single-label domains rejected) plus `publicSuffixCheck` hook for full PSL integration.
- `createCookie` performs eager validation (Max-Age, Expires, Domain, Path, size) — no longer deferred to `serialize()`.
- Secret entropy verified (≥ 8 distinct characters); error message now matches what the code enforces.
- `prefixValidation` default flipped to `true`.
- `wrapBunError` falls back to a dedicated `CookieParserError` reason instead of misclassifying as `InvalidCookieName`.
- `CookieJar.delete()` overrides `sameSite=none` / `secure='auto'` defaults locally so deletion always serializes regardless of request context.

## Cryptographic hardening (8)

- Keys derived via HKDF-SHA256/384/512 (RFC 5869, NIST SP 800-108) with package-specific salt and info parameters.
- 4-byte KID prefix embedded in HMAC signatures and AES-GCM ciphertexts; verification is strict KID match (no fallback).
- `onEncrypt({ keyIndex, counter })` hook for AES-GCM IV usage tracking (NIST SP 800-38D §8.3).
- `__Http-` / `__Host-Http-` prefix support (HttpOnly enforcement).
- `Priority=Low|Medium|High` attribute support.
- `CookieJar.getSetCookieHeaders()` parallelizes sign/encrypt across cookies (`Promise.all`).
- Per-instance HKDF-derived key cache.
- `test/{conformance,security,fuzz}/` suites tracked in source control.

## Public API

- `CookieParser.create(options?)` — entry point; returns parser with `createCookie`, `serialize`, `sign`, `unsign`, `encrypt`, `decrypt`, `validatePrefix`, `isSigningConfigured`, `isEncryptionConfigured`.
- `CookieJar` — request-scoped container.
- `CookieError` (with `reason: CookieErrorReason`).
- Types: `CookieParserOptions`, `CookieAttributes`, `SerializeContext`, `CookiePriority`, `SigningAlgorithm`, `CookieErrorReason`.

## Standards alignment

- RFC 6265 / RFC 6265bis-22 server-side normatives
- RFC 1034 / RFC 1123 (Domain LDH)
- RFC 6265bis §4.1.3.1/2, §5.7 (`__Secure-` / `__Host-` / `__Http-`)
- RFC 6265bis §5.5 (400-day cap on Expires and Max-Age)
- RFC 6265bis §5.6 (4096 / 1024 octet caps)
- CHIPS (Partitioned + Secure)
- NIST SP 800-38D (AES-256-GCM: 12-byte IV, 128-bit tag, AAD bound to cookie name)
- RFC 5869 / NIST SP 800-108 (HKDF)
- FIPS 198-1 (HMAC + constant-time verify)
- RFC 9110 §5.6.2 (cookie-name token grammar)

## Notes on `CookieErrorReason`

Reason codes are kebab-case strings (`'invalid-cookie-name'`, `'host-prefix-forbids-domain'`, etc.). Consumers matching by string value should use the `CookieErrorReason` enum.

## Test coverage

- 330 tests / 510 assertions / 99.49% line coverage / 99.62% function coverage
- Conformance: RFC 6265bis §§4.1.1, 4.1.2.1, 4.1.2.2, 4.1.2.7, 4.1.3.1, 4.1.3.2, 5.5, 5.6, 5.7; CHIPS; NIST SP 800-38D; FIPS 198-1; RFC 9110 §5.6.2
- Security: header injection, cross-name signature/ciphertext replay, algorithm confusion, ciphertext truncation/tampering, signature malformation, prototype pollution, weak secrets, oversized payloads, control characters in name, percent in name, error-type leakage
- Fuzz: 200-run fast-check property tests for sign/encrypt roundtrip, IV randomness, name-binding, AAD-binding, tampering rejection, name validation, jar roundtrip, 4096-octet boundary, key rotation invariants
21 changes: 15 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/cookie/.npmignore
Original file line number Diff line number Diff line change
@@ -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/
docker-compose.yml

# Docs (README/LICENSE/CHANGELOG auto-included by npm)
README.ko.md
32 changes: 32 additions & 0 deletions packages/cookie/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# @zipbul/cookie TODO

## 완료

- [x] `cloneCookieWithValue`에서 `||` → `??` 변경 (`maxAge: 0`, `path: ""`, `expires: 0` 등 falsy 유의미 값 소실 버그)
- [x] `options.ts` 추출 — `resolveCookieParserOptions()` + `validateCookieParserOptions()` (Result 패턴)
- [x] `types.ts` 추가 — `ResolvedCookieParserOptions`, `ResolvedCookieDefaults`, `SigningAlgorithm`
- [x] `@zipbul/result` 의존성 추가
- [x] `create()` 리팩터 — resolve → validate → `if (isErr) throw` 패턴
- [x] CookieParserOptions 확장 — 쿠키 기본 속성 (httpOnly, secure, sameSite, path, domain, maxAge, expires, partitioned)
- [x] `createCookie()` 메서드 — 파서 기본값 병합 + 개별 쿠키 override
- [x] 서명 알고리즘 설정 — algorithm 옵션 (`sha256` | `sha384` | `sha512`)
- [x] Prefix 자동 검증 — prefixValidation 옵션
- [x] secure auto-detect — `secure: 'auto'` + `SerializeContext`
- [x] `serialize()` 확장 — nullable defaults 적용, auto-secure 해소, auto-prefix 검증
- [x] RFC 6265 / RFC 6265bis-22 / RFC 1034·1123 / NIST SP 800-38D·800-108 / RFC 5869 / FIPS 198-1 / CHIPS 표준 부합
- [x] HKDF-SHA256/384/512 키 도출 + 4-byte KID 박힘 (sign + encrypt 모두 strict 매칭)
- [x] `__Http-` / `__Host-Http-` prefix, `Priority=` attribute, `onEncrypt` IV 카운터 hook
- [x] `CookieJar.getSetCookieHeaders()` 병렬화
- [x] `test/{conformance,security,fuzz}/` git 트래킹 (330 테스트 / 라인 커버리지 99.49%)

## 계획

- 현재 cookie 파서 스코프 내 미해결 항목 없음.

## 의도적으로 스코프 밖

이 패키지가 책임지지 않는 항목 — 별도 패키지에서 처리:

- 세션 저장소, CSRF 토큰, observability/audit hook 통합 → 미들웨어 패키지
- 전체 PSL 데이터 자동 동기화 → `psl` / `tldts` 등을 `publicSuffixCheck` 옵션에 주입
- DBSC (Device-Bound Session Credentials) → 표준 미확정. cookie 파서가 아닌 인증 프로토콜 영역
13 changes: 13 additions & 0 deletions packages/cookie/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[test]
onlyFailures = true
coverage = true
coverageReporter = ["text", "lcov"]
coverageThreshold = 0.95
coveragePathIgnorePatterns = [
"node_modules/**",
"dist/**",
"../result/**"
]

[test.reporter]
dots = true
6 changes: 6 additions & 0 deletions packages/cookie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { CookieParser } from './src/cookie-parser';
export { CookieJar } from './src/cookie-jar';
export { CookieError } from './src/interfaces';
export type { CookieAttributes, CookieParserOptions, SerializeContext } from './src/interfaces';
export { CookieErrorReason } from './src/enums';
export type { SigningAlgorithm } from './src/types';
31 changes: 31 additions & 0 deletions packages/cookie/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@zipbul/cookie",
"version": "0.0.0",
"type": "module",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"sideEffects": false,
"publishConfig": {
"provenance": true
},
"scripts": {
"build": "bun build index.ts --outdir dist --target bun --format esm --packages external --production && tsc -p tsconfig.build.json",
"test": "bun test",
"coverage": "bun test --coverage"
},
"dependencies": {
"@zipbul/result": "workspace:*"
},
"engines": {
"bun": ">=1.3.0"
}
}
Loading
Loading