
# Node.js 管理 SDK

`@keenauth/node` は、KeenAuth 管理 API 用の TypeScript SDK です。Node.js 22 以降が必要で、ランタイム依存関係はありません。まだ npm には公開されていません。

このパッケージには次のエントリポイントがあります:

| Import | 用途 |
|---|---|
| `@keenauth/node` | 管理 API クライアント、管理トークン、ページネーション、エラー |
| `@keenauth/node/webhooks` | API クライアントを読み込まずに Webhook の署名を検証する |
| `@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 キーを使う場合:

```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` は、リージョンの API アドレスに `/v1` を付けたものです。`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 の動作は次のとおりです:

- `grant_type=client_credentials` を `<issuer>/oauth2/token` に送信し、`resource` には管理 API の識別子を設定します。既定値は `baseUrl` のオリジン（例: `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 秒前に更新されます。同時に行われた呼び出しは 1 回の更新を共有します。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` は待機せず、そのレスポンスを返します。
- それ以外の場合は、0.5 秒、次に 1 秒待機します。どちらも ±25% のランダムなジッターが加えられます。
- `timeoutMs`（既定値は 30 秒）は、リトライを含む呼び出し全体を制限します。待機によってその時間を超える場合、SDK は代わりに最後のレスポンスを返します。

SDK は、対応する操作が受け付ける `POST` のすべてにランダムな `Idempotency-Key` を追加するため、リトライされた作成処理によってオブジェクトが 2 つ作られることはありません。独自のリトライも安全にするには、同じキーを自分で渡してください:

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

## ページネーション

一覧のエンドポイントは 1 ページを返します。`data` と、`before` および `after` カーソルを含む `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` は、項目ではなくページ単位で結果を返します。
- サーバーが同じカーソルを 2 回返した場合、ループは `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` になります。

## Webhook を検証する

KeenAuth は Standard Webhooks v1 方式で Webhook に署名します。解析する前に、生のリクエストボディを使って署名を検証してください:

```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` が発生させる設定エラー）。各 kind には `WebhookSignatureError` などの専用クラスもあります。
- `verify` は解析済みの JSON ボディを返します。`verifyRaw` は検証済みのバイト列を返します。

Webhook は複数回届く場合があります。重複を無視するには `webhook-id` を使用してください。

## 型付きイベントとクレーム

`@keenauth/node/events` には、公開イベントスキーマから生成された、顧客向けイベントごとの型があります。`parseEvent` は検証済みのボディを確認し、`KeenAuthEvent | UnknownEvent` を返します。`isKnownEvent` の後で `switch (event.type)` を使うと、イベントが 1 つの型に絞り込まれます。`@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` は、検証済みトークンのクレームに型を付けます。`parseIdTokenClaims`（OpenID Connect Core）と `parseAccessTokenClaims`（RFC 9068）です。これらはクレームの型を確認しますが、検証は行いません。verifier が返す `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 はプロセスの存続期間中、エンドポイントとヘッダー値ごとに 1 回警告を出力します。警告にはメソッド、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 バージョンについてお知らせするためにも使います。
