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 および Fastify
@keenauth/node/middleware バアラートークンによる API の保護
@keenauth/node/testing オフラインテストキット

クライアントを作成する

シークレットキーまたは制限付き API キーを使う場合:

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 タイプのアプリケーションと、必要な権限を持つ管理クライアントレコードを作成します(管理クライアントを作成する を参照)。
  2. 鍵ペアを生成し、公開鍵をアプリケーションの public_key 認証情報として登録します。例えば ES256 の場合: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem。
  3. 秘密鍵はシークレットストアに保管してください。SDK に渡します:
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 の動作は次のとおりです:

トークンのリクエストが失敗すると、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 はリトライされません。

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

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

ページネーション

一覧のエンドポイントは 1 ページを返します。data と、before および after カーソルを含む listMetadata です。paginate はカーソルをたどって自動的に次のページを取得します:

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
}

エラー

エラーレスポンスは、status、code、title、detail、requestId およびフィールドごとの errors を持つ ProblemError(RFC 9457)になります。メッセージではなく code を確認してください。SDK が認識しないコードは文字列として保持されます。

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 に署名します。解析する前に、生のリクエストボディを使って署名を検証してください:

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);
});

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

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

@keenauth/node/events には、公開イベントスキーマから生成された、顧客向けイベントごとの型があります。parseEvent は検証済みのボディを確認し、KeenAuthEvent | UnknownEvent を返します。isKnownEvent の後で switch (event.type) を使うと、イベントが 1 つの型に絞り込まれます。@keenauth/node/webhooks の verifyEvent は両方の手順を行います:

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;
}

@keenauth/node/claims は、検証済みトークンのクレームに型を付けます。parseIdTokenClaims(OpenID Connect Core)と parseAccessTokenClaims(RFC 9068)です。これらはクレームの型を確認しますが、検証は行いません。verifier が返す claims に対して呼び出してください:

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 に渡されます。代わりにロガーへ送るには:

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 バージョンについてお知らせするためにも使います。