
# Node.js 관리 SDK

`@keenauth/node`는 KeenAuth 관리 API용 TypeScript SDK입니다. Node.js 22 이상이 필요하며 런타임 의존성이 없습니다. 아직 npm에는 게시되지 않았습니다.

이 패키지에는 다음 진입점이 있습니다:

| 가져오기 경로 | 용도 |
|---|---|
| `@keenauth/node` | 관리 API 클라이언트, 관리 토큰, 페이지네이션 및 오류 |
| `@keenauth/node/webhooks` | API 클라이언트를 로드하지 않고 웹훅 서명 검증 |
| `@keenauth/node/bff` | 서버 앱의 로그인: [Express](/docs/sdk/express) 및 [Fastify](/docs/sdk/fastify) |
| `@keenauth/node/middleware` | 베어러 토큰으로 [API 보호](/docs/sdk/protect-api) |
| `@keenauth/node/testing` | [오프라인 테스트 키트](/docs/sdk/testing) |

## 클라이언트 생성

비밀 API 키 또는 제한된 API 키를 사용하는 경우:

```ts
import { KeenAuth } from "@keenauth/node";

const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  apiKey: process.env.KEENAUTH_API_KEY!,
});

const user = await keenauth.users.getUser({ userId: "usr_01j9zzzzzzzzzzzzzzzzzzzz" });
```

`baseUrl`은 `/v1`이 포함된 리전 API 주소입니다. `https`를 사용해야 하며, `http`는 `localhost`에 대해서만 허용됩니다. 키는 `Authorization` 헤더에만 담기며 해당 주소로만 전송됩니다. 리디렉션은 따라가지 않습니다.

## API 키 없이 인증

`management` 애플리케이션은 정적 키 대신 수명이 짧은 토큰으로 API를 호출할 수 있습니다. 애플리케이션은 개인 키로 클라이언트 어설션에 서명하고(`private_key_jwt`, RFC 7523), SDK는 이를 환경의 토큰 엔드포인트에서 교환합니다.

1. 대시보드에서 `management` 유형의 애플리케이션과 필요한 권한을 가진 관리 클라이언트 레코드를 생성하십시오([관리 클라이언트 생성](/docs/api/management/management-clients/create) 참조).
2. 키 쌍을 생성하고 공개 키를 애플리케이션의 `public_key` 자격 증명으로 등록하십시오. 예를 들어 ES256의 경우: `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem`.
3. 개인 키는 비밀 저장소에 보관하십시오. 그런 다음 SDK에 전달하십시오:

```ts
import { readFileSync } from "node:fs";
import { KeenAuth } from "@keenauth/node";

const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  credentials: {
    issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
    clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
    privateKey: readFileSync("management-key.pem", "utf8"), // PEM, a private JWK or a KeyObject
    scope: ["users:read", "users:write"], // optional; default: the application's default scopes
  },
});
```

SDK의 동작은 다음과 같습니다:

- `resource`를 관리 API 식별자로 설정하여 `<issuer>/oauth2/token`에 `grant_type=client_credentials`를 전송합니다. 기본값은 `baseUrl`의 origin(예: `https://api.eu.keenauth.com`)입니다. 이를 변경하려면 `credentials.audience`를 설정하십시오.
- 요청마다 새 클라이언트 어설션이 전달됩니다. `iss`와 `sub`는 클라이언트 ID이고, `aud`는 issuer이며, `jti`는 무작위 값이고, 어설션은 120초 동안 유효합니다. `kid` 헤더는 키의 RFC 7638 썸프린트이며, 애플리케이션에 키가 여러 개 있을 때(키 교체 시) KeenAuth가 키를 찾는 데 사용됩니다.
- 알고리즘은 키를 따릅니다. RSA 키는 `RS256`으로 서명합니다(RSA-PSS를 사용하려면 `alg: "PS256"`을 설정하십시오). P-256 키는 `ES256`, Ed25519 키는 `EdDSA`를 사용합니다. RSA 키는 최소 2048비트여야 합니다.
- 액세스 토큰은 캐시되며 만료 60초 전에 갱신됩니다. 동시 호출은 하나의 갱신을 공유합니다. API가 `401`을 반환하면 캐시된 토큰이 삭제되고 다음 호출에서 새 토큰을 받습니다.

토큰 요청이 실패하면 OAuth `code`(예: `invalid_client` 또는 `invalid_scope`), `description`, `status`를 포함한 `TokenError`가 발생합니다. 공개 키를 개인 키 대신 사용하는 것과 같은 구성 오류는 클라이언트를 생성할 때 발생합니다.

`apiKey`와 `credentials`는 함께 사용할 수 없습니다.

## 재시도

SDK는 API가 `429`, `502`, `503` 또는 `504`를 반환하거나 연결이 실패하는 경우 요청을 총 최대 3회까지 재시도합니다. `GET`, `PUT`, `DELETE` 요청과 `Idempotency-Key`가 포함된 `POST` 요청만 재시도합니다. `If-Match`가 포함된 `PATCH`는 `429`에서만 재시도합니다. 속도 제한기가 요청을 실행하기 전에 거부하며, `If-Match`는 변경 사항이 두 번 적용되는 것을 방지하기 때문입니다. 그 밖의 `PATCH`는 재시도하지 않습니다.

- 응답에 `Retry-After` 헤더(초 또는 HTTP 날짜)가 있으면 SDK는 해당 시간만큼 기다립니다. 60초를 넘는 `Retry-After`는 기다리지 않고 SDK가 해당 응답을 호출자에게 반환합니다.
- 그렇지 않으면 0.5초, 그다음 1초를 기다리며, 각 대기 시간에는 ±25%의 무작위 지터가 적용됩니다.
- `timeoutMs`(기본값 30초)는 재시도를 포함한 전체 호출을 제한합니다. 대기 시간이 이 제한을 넘으면 SDK는 마지막 응답을 대신 반환합니다.

SDK는 멱등성 키를 지원하는 모든 `POST`에 무작위 `Idempotency-Key`를 추가하므로, 재시도된 생성 요청이 객체를 두 번 만들지 않습니다. 직접 작성한 재시도도 안전하게 만들려면 같은 키를 직접 전달하십시오:

```ts
await keenauth.users.createUser({
  userCreateRequest: { email: "ada@example.com" },
  idempotencyKey: "signup-7d1f0c2a-0d7e-4c55",
});
```

## 페이지네이션

목록 엔드포인트는 한 페이지를 반환합니다. `before`와 `after` 커서가 포함된 `data`와 `listMetadata`가 제공됩니다. `paginate`는 커서를 자동으로 따라갑니다:

```ts
import { paginate } from "@keenauth/node";

for await (const user of paginate((p) => keenauth.users.listUsers(p), { limit: 100 })) {
  console.log(user.id);
  if (user.email === "ada@example.com") break; // stops without loading the next page
}
```

- `before`가 없으면 커서가 `null`이 될 때까지 `after`로 앞으로 이동합니다. `before`가 있으면 뒤로 이동합니다.
- `limit`은 페이지 크기(1-100)이며 모든 요청에 전송됩니다. 다른 매개변수도 그대로 유지됩니다.
- `paginatePages`는 항목 대신 전체 페이지를 제공합니다.
- 서버가 같은 커서를 두 번 반환하면 루프는 `PaginationError`와 함께 중단됩니다.

## 오류

오류 응답은 `status`, `code`, `title`, `detail`, `requestId`와 필드별 `errors`를 포함한 `ProblemError`(RFC 9457)가 됩니다. 메시지가 아니라 `code`를 확인하십시오. SDK가 알지 못하는 코드는 문자열로 계속 사용할 수 있습니다.

```ts
import { ProblemError } from "@keenauth/node";

try {
  await keenauth.users.getUser({ userId });
} catch (e) {
  if (e instanceof ProblemError && e.code === "not_found") {
    // …
  }
}
```

프록시의 HTML 페이지처럼 문제 문서가 없는 응답은 원본 `response`를 포함한 생성된 `ResponseError`가 됩니다.

## 웹훅 검증

KeenAuth는 Standard Webhooks v1 방식으로 웹훅에 서명합니다. 파싱하기 전에 원본 요청 본문으로 서명을 검증하십시오:

```ts
import express from "express";
import { Webhook, WebhookVerificationError } from "@keenauth/node/webhooks";

const webhook = new Webhook(process.env.KEENAUTH_WEBHOOK_SECRET!); // idp_whsec_…

const app = express();
app.post("/webhooks/keenauth", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body, req.headers);
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.status(400).send(e.kind);
    throw e;
  }
  // handle event.type, then answer quickly
  res.sendStatus(204);
});
```

- SDK는 `webhook-id`, `webhook-timestamp`, `webhook-signature` 헤더를 확인합니다. 타임스탬프는 서버 시계와 앞뒤 5분 이내여야 합니다.
- 시크릿을 교체하는 동안에는 두 시크릿을 모두 전달하십시오: `new Webhook([newSecret, oldSecret])`. 어떤 `v1` 서명이든 어떤 시크릿과 일치하면 요청이 수락됩니다.
- 서명은 상수 시간으로 비교됩니다.
- `e.kind`는 실패 원인을 알려 줍니다: `headers`(헤더 누락), `timestamp`(숫자가 아니거나 허용 범위를 벗어남), `signature`(일치하는 서명 없음), `secret`(구성된 시크릿이 올바른 `idp_whsec_` 값이 아님, `new Webhook`에서 발생하는 구성 오류). 각 종류에는 `WebhookSignatureError`와 같은 고유 클래스도 있습니다.
- `verify`는 파싱된 JSON 본문을 반환합니다. `verifyRaw`는 검증된 바이트를 반환합니다.

웹훅은 두 번 이상 도착할 수 있습니다. 중복을 무시하려면 `webhook-id`를 사용하십시오.

## 타입이 지정된 이벤트와 클레임

`@keenauth/node/events`에는 공개 이벤트 스키마에서 생성된 모든 고객 이벤트의 타입이 있습니다. `parseEvent`는 검증된 본문을 확인하고 `KeenAuthEvent | UnknownEvent`를 반환합니다. `isKnownEvent` 이후에는 `switch (event.type)`이 이벤트를 하나의 타입으로 좁힙니다. `@keenauth/node/webhooks`의 `verifyEvent`는 두 단계를 모두 수행합니다:

```ts
import { verifyEvent } from "@keenauth/node/webhooks";
import { isKnownEvent } from "@keenauth/node/events";

const event = verifyEvent(webhook, req.body, req.headers); // WebhookVerificationError or EventParseError
if (!isKnownEvent(event)) {
  // A type this SDK version does not know yet. event.type and event.data.object.id are still typed.
  return res.sendStatus(204);
}
switch (event.type) {
  case "user.created":
    console.log(event.data.object.id);
    break;
  case "email_sending.suspended":
    console.log(event.data.object.reason); // "bounce_rate" | "complaint_rate" | (string & {})
    break;
}
```

- `parseEvent`는 JSON 텍스트, 바이트 또는 이미 디코딩된 값을 받습니다. 봉투와 해당 타입의 스키마에 선언된 멤버를 확인합니다. 멤버가 없거나 타입이 잘못되면 `data.object.reason`과 같은 경로가 포함된 `EventParseError`가 발생합니다.
- 알 수 없는 타입이거나, 이 SDK가 모델링하지 않는 `schema_version`을 가진 알려진 타입은 `UnknownEvent`입니다. 이는 오류가 아닙니다.
- 스키마에 선언되지 않은 멤버는 객체에 그대로 남습니다.
- 열거형 필드는 알려진 값과 `string`으로 타입이 지정되므로, 서버의 새 값도 타입 검사를 통과하고 파싱됩니다.

`@keenauth/node/claims`는 검증된 토큰의 클레임 타입을 지정합니다: OpenID Connect Core의 `parseIdTokenClaims`와 RFC 9068의 `parseAccessTokenClaims`입니다. 이 함수들은 클레임 타입만 확인하고 아무것도 검증하지 않으므로, 검증기가 반환한 `claims`에 호출하십시오:

```ts
import { parseAccessTokenClaims } from "@keenauth/node/claims";

const at = await verifier.verifyAccessToken(token);
const claims = parseAccessTokenClaims(at.claims);
claims.client_id; // string
claims.org_roles; // string[] | undefined
claims["tenant_tier"]; // unknown: your own claims
```

맞지 않는 클레임은 이름과 함께 `ClaimsParseError`를 발생시킵니다.

## 지원 중단 경고

응답에 `Deprecation`(RFC 9745) 또는 `Sunset`(RFC 8594) 헤더가 있으면 SDK는 프로세스가 실행되는 동안 엔드포인트와 헤더 값마다 경고를 한 번씩 기록합니다. 경고에는 메서드, ID를 `{id}`로 바꾼 경로, 헤더 값이 포함됩니다. 토큰, 쿼리 문자열, 사용자 데이터는 포함되지 않습니다. 경고 때문에 오류가 발생하지 않습니다.

기본적으로 경고는 `KeenAuthDeprecationWarning` 타입과 함께 `process.emitWarning`으로 전달됩니다. 대신 로거로 보내려면 다음과 같이 하십시오:

```ts
const keenauth = new KeenAuth({
  baseUrl: "https://api.eu.keenauth.com/v1",
  apiKey: process.env.KEENAUTH_API_KEY!,
  onWarning: (message) => logger.warn(message),
});
```

## 텔레메트리 헤더

모든 요청에는 `Idp-Client: keenauth-ts/<version> (node/<version>)` 헤더가 포함됩니다. 이 헤더에는 개인 데이터가 없으며 끌 수 없습니다. KeenAuth는 이를 지원 목적과 지원 중단된 SDK 버전 안내에 사용합니다.
