# ADR-0008: DTI signing header names-ийг configurable болгох

## Status

Accepted

Энэ ADR-ийн architecture decision батлагдсан. Code implementation-ийг тусдаа command-аар эхлүүлнэ.

## Context

DTI request signing одоогоор дараах тогтмол header names ашигладаг:

- `x-dti-key-id`
- `x-dti-timestamp`
- `x-dti-nonce`
- `x-dti-signature`

Зарим application, API gateway, reverse proxy, security policy болон organization naming convention өөр prefix эсвэл өөр header name шаардаж болно.

Header name-ийг application code дээр гараар солих нь client/server config drift үүсгэж, canonical signing flow болон header ownership-ийн дүрмийг эвдэх эрсдэлтэй.

Header values болон signing algorithm өөрчлөгдөхгүй. Зөвхөн request дээр дамжих дөрвөн header-ийн нэр configurable болно.

## Decision

### Shared type

`@napp/dti-core` дараах shared type болон default mapping export хийнэ:

```ts
export type DTISignHeaderNames = {
    keyId: string;
    timestamp: string;
    nonce: string;
    signature: string;
};

export const DTI_DEFAULT_SIGN_HEADER_NAMES: Readonly<DTISignHeaderNames> = {
    keyId: "x-dti-key-id",
    timestamp: "x-dti-timestamp",
    nonce: "x-dti-nonce",
    signature: "x-dti-signature",
};
```

Existing individual constants backward compatibility-д хадгалагдана:

- `DTI_SIGN_HEADER_KEY_ID`
- `DTI_SIGN_HEADER_TIMESTAMP`
- `DTI_SIGN_HEADER_NONCE`
- `DTI_SIGN_HEADER_SIGNATURE`

### Client config

`DTIClientSign` дээр `headerNames` option нэмнэ:

```ts
export type DTIClientSign = {
    keyId: string;
    secret: DTIClientSignSecret;
    timestamp?: () => string | Date | Promise<string | Date>;
    nonce?: () => string | Promise<string>;
    headerNames?: Partial<DTISignHeaderNames>;
};
```

Жишээ:

```ts
const signHeaderNames = {
    keyId: "x-app-key-id",
    timestamp: "x-app-timestamp",
    nonce: "x-app-nonce",
    signature: "x-app-signature",
} satisfies DTISignHeaderNames;

const client = new DTIClient("/api", {
    sign: {
        keyId: "client-a",
        secret: "shared-secret",
        headerNames: signHeaderNames,
    },
});
```

Client resolved mapping-ийн нэрээр signing header-үүдийг үүсгэнэ. Custom mapping ашигласан field дээр default `x-dti-*` header давхар үүсгэхгүй.

Per-call `sign` object нь global sign config-ийг бүхэлд нь override хийдэг existing дүрэм хэвээр байна. Иймээс global sign custom `headerNames` ашигладаг бол per-call sign object дээр шаардлагатай mapping-ийг дахин өгнө. Per-call object дээр `headerNames` байхгүй бол default mapping ашиглана.

`sign: false` үед resolved header mapping-оос үл хамааран library signing header үүсгэхгүй.

### Server config

`DTIServerSignOptions` дээр ижил `headerNames` option нэмнэ:

```ts
export type DTIServerSignOptions = {
    getSecret: DTISignSecretProvider;
    nonceStore: INonceStore;
    toleranceMs?: number;
    headerNames?: Partial<DTISignHeaderNames>;
};
```

```ts
const dti = createDTIExpressRouter({
    sign: {
        nonceStore,
        getSecret: async ({ keyId }) => resolveSecret(keyId),
        headerNames: signHeaderNames,
    },
});
```

Client болон server logical field бүр дээр ижил resolved header name ашиглах үүрэгтэй. Mapping зөрвөл server required header-ээ олохгүй бөгөөд `401 / DTI_SIGNATURE_REQUIRED` буцаана.

### Default merge

`headerNames` нь partial байж болно. Resolved mapping дараах байдлаар үүснэ:

```ts
const resolvedHeaderNames = {
    ...DTI_DEFAULT_SIGN_HEADER_NAMES,
    ...headerNames,
};
```

Жишээ нь зөвхөн signature header-ийг өөрчилж болно:

```ts
headerNames: {
    signature: "x-app-signature",
}
```

Энэ үед `keyId`, `timestamp`, `nonce` нь default нэрээ ашиглана.

### Validation rules

Resolved header mapping дараах дүрмийг хангана:

1. Дөрвөн logical field бүгд non-empty string байна.
2. Leading эсвэл trailing whitespace байхгүй байна.
3. HTTP header field-name-д зөвшөөрөгдөх token character л ашиглана.
4. Дөрвөн resolved name case-insensitive байдлаар unique байна.
5. Dynamic action, request эсвэл param-аас header name үүсгэхгүй; config нь client/router instance түвшинд тогтвортой байна.

Header name case-insensitive тул `X-App-Key` болон `x-app-key` ижил нэр гэж үзнэ.

Invalid config дээр:

```text
DTI_SIGN_HEADER_CONFIG_ERROR
```

Client global config-ийг client үүсэх үед, per-call config-ийг request илгээхээс өмнө validate хийнэ. Server config-ийг router үүсэх үед validate хийнэ.

Error `details` дотор secret болон signature value оруулахгүй. Зөвхөн invalid logical field, header name, reason дамжуулж болно.

### Header ownership and merge order

Resolved signing header names нь signing enabled үед library-owned байна.

Existing merge order өөрчлөгдөхгүй:

1. global `headers`
2. global `auth`
3. per-call `headers`
4. per-call `auth`
5. resolved DTI signing headers
6. `Content-Type` fallback

App-level custom header resolved signing header name-тэй давхцвал library signing value хамгийн сүүлд override хийнэ.

Application нь signing header names-ийг `authorization`, `cookie`, `content-type`, proxy-owned эсвэл өөр security-sensitive header-тэй давхцуулахгүй байх үүрэгтэй. Library бүх application-specific reserved header-ийг мэдэх боломжгүй тул valid HTTP name бүрийг blanket block хийхгүй.

Library common proxy/security-sensitive header name-ийн тусдаа denylist хэрэгжүүлэхгүй. Syntax болон case-insensitive uniqueness validation хийнэ; deployment-specific collision review application-ийн ownership байна.

### Canonical payload

Header names canonical payload-д орохгүй. Дараах утгууд өмнөх ADR-0006 дүрмээр sign хийгдэнэ:

- method
- path/query
- timestamp header-ийн value
- nonce header-ийн value
- `action.signature(param)` result

Иймээс header name өөрчлөх нь signing algorithm эсвэл canonical payload format-ийг өөрчлөхгүй.

### Migration policy

Server нэг request дээр зөвхөн нэг resolved mapping уншина. Legacy болон new header aliases-ийг зэрэг хүлээж авахгүй.

DTI signing header migration alias feature дэмжихгүй. Alias шаардлагатай deployment тусдаа router endpoint, gateway rewrite эсвэл coordinated rollout ашиглана.

Header rename хийхдээ client/server config-ийг coordinated deployment-оор шинэчилнэ. Zero-downtime migration шаардлагатай бол old/new mapping-тэй тусдаа router endpoint эсвэл deployment үе шат ашиглана.

Library environment variable-ийг автоматаар уншихгүй. Env key-ээс `headerNames` config үүсгэхийг application шийднэ.

### Acceptance criteria

Implementation complete гэж үзэхийн тулд:

1. Config байхгүй үед existing `x-dti-*` header names өөрчлөгдөхгүй байна.
2. Client/server ижил full custom mapping ашиглахад signed request амжилттай байна.
3. Partial mapping default names-тэй зөв merge хийгдэнэ.
4. Custom field дээр default header давхар илгээгдэхгүй байна.
5. Case-insensitive duplicate name reject хийгдэнэ.
6. Empty, whitespace эсвэл invalid HTTP header name reject хийгдэнэ.
7. Client/server mapping mismatch үед `401 / DTI_SIGNATURE_REQUIRED` гарна.
8. App custom header custom signing name-тэй давхцвал library value override хийнэ.
9. Per-call sign object global mapping-ийг inherit хийхгүй, өөрийн mapping эсвэл default mapping ашиглана.
10. `sign: false` үед signing header үүсэхгүй байна.
11. Existing signing, nonce replay, timestamp, header merge test regression-гүй байна.
12. TypeScript test-ийг `node:test`, `node:assert/strict` ашиглан нэмсэн байна.

## Consequences

- Existing хэрэглэгч config нэмэхгүйгээр өмнөх header names-ийг ашиглана.
- Gateway болон organization-specific naming convention дэмжигдэнэ.
- Client/server mapping config drift нь runtime `401` алдаа үүсгэнэ.
- Partial config нь migration болон нэг field rename хийхэд хялбар боловч resolved mapping-ийг deployment config дээр тодорхой харагдуулах шаардлагатай.
- Custom names app header-тэй collision үүсгэж болох тул application config review шаардлагатай.
- Header rename нь canonical payload өөрчлөх шаардлагагүй.
- Alias fallback байхгүй тул migration coordinated байна.

## Alternatives Considered

### Constructor/router top-level option болгох

`signHeaderNames`-ийг client/router root дээр байрлуулж болно. Гэхдээ header names нь signing behavior-ийн хэсэг тул `sign.headerNames` дотор байлгах нь ownership-ийг тодорхой болгоно.

### Зөвхөн бүрэн mapping зөвшөөрөх

Configuration илүү explicit болно. Гэхдээ нэг header rename хийхэд дөрвөн default value давтан бичих шаардлагатай тул partial override болон default merge сонгов.

### Header name resolver callback ашиглах

Action эсвэл request бүрээр dynamic name сонгож болно. Гэхдээ client/server drift, cache/debug complexity болон security ambiguity нэмэгдэх тул static config сонгов.

### Legacy болон custom header aliases зэрэг хүлээж авах

Zero-downtime migration хялбар болно. Гэхдээ нэг logical value олон header-ээс ирэхэд precedence, ambiguity болон header smuggling эрсдэл нэмэгдэнэ. Нэг resolved mapping ашиглана.

### Canonical payload-д header names оруулах

Header mapping өөрчлөлтийг cryptographically bind хийнэ. Гэхдээ client/server config mismatch аль хэдийн required header validation-аар reject болох бөгөөд payload compatibility-д шаардлагагүй нэмэлт complexity үүснэ.
