Node.js 관리 SDK
@keenauth/node는 KeenAuth 관리 API용 TypeScript SDK입니다. Node.js 22 이상이 필요하며 런타임 의존성이 없습니다. 아직 npm에는 게시되지 않았습니다.
이 패키지에는 다음 진입점이 있습니다:
| 가져오기 경로 | 용도 |
|---|---|
@keenauth/node |
관리 API 클라이언트, 관리 토큰, 페이지네이션 및 오류 |
@keenauth/node/webhooks |
API 클라이언트를 로드하지 않고 웹훅 서명 검증 |
@keenauth/node/bff |
서버 앱의 로그인: Express 및 Fastify |
@keenauth/node/middleware |
베어러 토큰으로 API 보호 |
@keenauth/node/testing |
오프라인 테스트 키트 |
클라이언트 생성
비밀 API 키 또는 제한된 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은 /v1이 포함된 리전 API 주소입니다. https를 사용해야 하며, http는 localhost에 대해서만 허용됩니다. 키는 Authorization 헤더에만 담기며 해당 주소로만 전송됩니다. 리디렉션은 따라가지 않습니다.
API 키 없이 인증
management 애플리케이션은 정적 키 대신 수명이 짧은 토큰으로 API를 호출할 수 있습니다. 애플리케이션은 개인 키로 클라이언트 어설션에 서명하고(private_key_jwt, RFC 7523), SDK는 이를 환경의 토큰 엔드포인트에서 교환합니다.
- 대시보드에서
management유형의 애플리케이션과 필요한 권한을 가진 관리 클라이언트 레코드를 생성하십시오(관리 클라이언트 생성 참조). - 키 쌍을 생성하고 공개 키를 애플리케이션의
public_key자격 증명으로 등록하십시오. 예를 들어 ES256의 경우:openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem. - 개인 키는 비밀 저장소에 보관하십시오. 그런 다음 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의 동작은 다음과 같습니다:
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를 추가하므로, 재시도된 생성 요청이 객체를 두 번 만들지 않습니다. 직접 작성한 재시도도 안전하게 만들려면 같은 키를 직접 전달하십시오:
await keenauth.users.createUser({
userCreateRequest: { email: "ada@example.com" },
idempotencyKey: "signup-7d1f0c2a-0d7e-4c55",
});
페이지네이션
목록 엔드포인트는 한 페이지를 반환합니다. before와 after 커서가 포함된 data와 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
}
before가 없으면 커서가null이 될 때까지after로 앞으로 이동합니다.before가 있으면 뒤로 이동합니다.limit은 페이지 크기(1-100)이며 모든 요청에 전송됩니다. 다른 매개변수도 그대로 유지됩니다.paginatePages는 항목 대신 전체 페이지를 제공합니다.- 서버가 같은 커서를 두 번 반환하면 루프는
PaginationError와 함께 중단됩니다.
오류
오류 응답은 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가 됩니다.
웹훅 검증
KeenAuth는 Standard Webhooks v1 방식으로 웹훅에 서명합니다. 파싱하기 전에 원본 요청 본문으로 서명을 검증하십시오:
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는 두 단계를 모두 수행합니다:
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에 호출하십시오:
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으로 전달됩니다. 대신 로거로 보내려면 다음과 같이 하십시오:
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 버전 안내에 사용합니다.