인증
서버 사이드 작업에서는 API 키로, 로그인한 사용자를 대신해 동작할 때는 OAuth 사용자 토큰으로 SDK를 인증하세요.
SDK는 두 개의 클라이언트를 제공하며, 각각 인증 방식이 다릅니다. 코드가 실행되는 위치에 맞는 쪽을 선택하세요.
OpenAPIClient | ListenHubClient | |
|---|---|---|
| 자격 증명 | API 키(lh_sk_…) | OAuth 사용자 access token |
| 전송되는 헤더 | Authorization: Bearer <apiKey> | Authorization: Bearer <accessToken> |
| 실행 주체 | 내 계정 / 키 소유자 | 로그인한 사용자 |
| 실행 위치 | 서버, 스크립트, CI | 사용자 대상 앱 |
| Base URL | https://api.marswave.ai/openapi | https://api.listenhub.ai/api |
API 키는 계정에 대한 전체 권한을 가진 비밀 정보입니다. 반드시 서버에만 두세요. 브라우저, 모바일, 그 밖의 어떤 클라이언트 측 번들에도 키를 포함해 배포하지 마세요. 사용자 대상 앱에서는 OAuth를 쓰는 ListenHubClient를 사용해, 각 요청이 사용자 본인의 계정으로 실행되도록 하세요.
API 키(OpenAPIClient)
서버 사이드 작업과 자동화에는 OpenAPIClient를 사용하세요. 공개 OpenAPI 인터페이스를 대상으로 하며, 내 계정을 나타내는 단일 API 키로 인증합니다.
키 발급하기
대시보드 listenhub.ai/settings/api-keys에서 키를 생성하세요. 키 형식은 lh_sk_<keyId>_<secret>입니다. secret은 생성 시 한 번만 표시됩니다 — 그때 바로 복사해서, 내 서버만 읽을 수 있는 곳에 보관하세요.
클라이언트 생성하기
키를 명시적으로 전달하거나, LISTENHUB_API_KEY를 설정한 뒤 인자 없이 생성하세요. 둘 다 없으면 생성자가 예외를 던집니다.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
// Reads process.env.LISTENHUB_API_KEY
const client = new OpenAPIClient();
const { items } = await client.listSpeakers({ language: 'en' });환경 변수에 키를 넣고 실행하세요:
LISTENHUB_API_KEY=lh_sk_... node app.jsimport { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient({
apiKey: process.env.LISTENHUB_API_KEY, // load from your secret store
});키는 하드코딩하지 말고 시크릿 매니저나 환경 변수에서 읽는 방식을 권장합니다. 위 예제도 환경 변수에서 가져옵니다 — 커밋되는 소스에 lh_sk_… 리터럴 문자열을 붙여 넣지 마세요.
클라이언트는 모든 요청에 Authorization: Bearer <apiKey>를 대신 설정해 줍니다. 갱신해야 할 토큰은 없습니다 — 키는 교체하거나 삭제하기 전까지 계속 유효합니다.
원본 HTTP 요청 등가물
SDK를 쓰지 않는다면, OpenAPI base URL로 동일한 헤더를 보내세요:
curl https://api.marswave.ai/openapi/v1/speakers/list?language=en \
-H "Authorization: Bearer $LISTENHUB_API_KEY"키 교체하기
키가 유출되면 대시보드에서 삭제하고 새 키를 발급하세요. 삭제는 즉시 적용됩니다 — 여전히 이전 키를 쓰는 요청은 인증 에러로 실패하기 시작합니다(에러 처리 참고). 새 키를 시크릿 저장소에 반영하고 재배포하세요.
OAuth 사용자 토큰(ListenHubClient)
각 요청이 특정한 로그인 사용자로 실행되어야 할 때는 ListenHubClient를 사용하세요 — 예를 들어 사용자가 자기 ListenHub 계정으로 로그인하는 데스크톱 도구나 CLI가 그렇습니다. 이 클라이언트는 API 키 대신, 짧은 브라우저 플로우로 획득하는 OAuth access token을 사용합니다.
흐름
ListenHubClient는 표준적인 authorize → exchange → refresh → revoke 생명주기에 대응하는 네 개의 인증 메서드를 제공합니다:
| 메서드 | 용도 |
|---|---|
connectInit({ callbackPort }) | 로그인 세션을 시작합니다. sessionId와 브라우저에서 열 authUrl을 반환합니다. |
connectToken({ sessionId, code }) | 콜백으로 받은 code를 토큰으로 교환합니다. accessToken, refreshToken, expiresIn을 반환합니다. |
refresh({ refreshToken }) | 현재 access token이 만료되면 새 토큰을 발급합니다. 동일한 토큰 구조를 반환합니다. |
revoke({ refreshToken }) | refresh token을 무효화합니다(로그아웃). |
로그인용 클라이언트 자체는 자격 증명이 필요 없으므로, connectInit / connectToken을 구동할 때는 아무 옵션 없는 ListenHubClient를 생성하세요. 흐름은 다음과 같습니다:
OAuth 콜백을 받을 로컬 서버를 띄운 다음, 그 포트로 connectInit을 호출하세요. 반환된 authUrl을 사용자의 브라우저에서 엽니다.
사용자가 브라우저에서 인가합니다. ListenHub는 http://127.0.0.1:<callbackPort>/?code=<code>로 리다이렉트합니다. 그 요청에서 code를 읽으세요.
connectToken({ sessionId, code })을 호출해 code를 accessToken, refreshToken, expiresIn(access token 만료까지 남은 초)으로 교환하세요. 두 토큰 모두 저장합니다.
access token으로 인증된 클라이언트를 생성하세요: new ListenHubClient({ accessToken }).
import * as http from 'node:http';
import { ListenHubClient } from '@marswave/listenhub-sdk';
// 1. Login client needs no credentials
const loginClient = new ListenHubClient();
// 2. Stand up a temporary callback server, then start the session
const { port, codePromise, server } = startCallbackServer();
const { authUrl, sessionId } = await loginClient.connectInit({ callbackPort: port });
const open = (await import('open')).default;
await open(authUrl); // user signs in here
// 3. Exchange the callback code for tokens
const code = await codePromise;
const tokens = await loginClient.connectToken({ sessionId, code });
server.close();
// tokens: { accessToken, refreshToken, expiresIn }
// Persist tokens somewhere private to the user's machine.
// 4. Build an authenticated client
const client = new ListenHubClient({ accessToken: tokens.accessToken });
const me = await client.getCurrentUser();콜백 서버는 node:http 몇 줄이면 됩니다 — 임시 포트를 리스닝하다가 리다이렉트가 도착하면 resolve합니다:
function startCallbackServer() {
let resolveCode!: (code: string) => void;
const codePromise = new Promise<string>((r) => (resolveCode = r));
const server = http.createServer((req, res) => {
const code = new URL(req.url!, 'http://localhost').searchParams.get('code');
if (code) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Login successful! You can close this tab.</h1>');
resolveCode(code);
} else {
res.writeHead(400).end('Missing code');
}
});
// Port 0 → OS assigns a free port
server.listen(0, '127.0.0.1');
const port = (server.address() as { port: number }).port;
return { port, codePromise, server };
}토큰을 최신 상태로 유지하기
access token은 expiresIn초 후에 만료됩니다. 인증 상태를 유지하는 방법은 두 가지입니다.
정적 토큰 전달 — 클라이언트의 수명이 짧을 때(한 번의 스크립트 실행, 한 묶음의 요청):
const client = new ListenHubClient({ accessToken: tokens.accessToken });getter 전달 — 클라이언트의 수명이 길 때. accessToken은 () => string | undefined를 받으며, SDK가 매 요청 전에 이를 호출합니다. 현재 토큰을 변수에 보관하고 별도로 갱신하면, getter는 항상 최신 값을 돌려줍니다:
let current = tokens;
const client = new ListenHubClient({
// Called before each request — return the freshest token you hold
accessToken: () => current.accessToken,
});
// Refresh on a timer (or lazily, just before expiry) and swap it in
async function refreshTokens() {
current = await loginClient.refresh({ refreshToken: current.refreshToken });
}refresh는 새 accessToken 과 새 refreshToken을 함께 반환합니다. 둘 다 저장하세요 — 최신 refreshToken을 보관해야 재시작 후에도 사용자의 로그인 상태가 유지되고, 다음 갱신에도 그것을 사용합니다.
로그아웃
refresh token으로 revoke를 호출해 서버 측 세션을 무효화한 뒤, 로컬에 저장된 토큰을 폐기하세요:
await loginClient.revoke({ refreshToken: current.refreshToken });
// Then delete accessToken + refreshToken from local storage.자격 증명을 두는 곳
- API 키는 서버 환경 변수나 시크릿 매니저에 두고
process.env.LISTENHUB_API_KEY로 읽으세요. 절대 브라우저에 도달해서도, 소스 관리에 커밋되어서도 안 됩니다. - OAuth 토큰은 사용자별로 구분됩니다.
accessToken과refreshToken은 해당 사용자 세션만 접근할 수 있는 곳에 저장하세요 — CLI나 데스크톱 도구라면 사용자 설정 디렉터리 아래의0600권한 파일이 적당합니다.refreshToken은 민감 정보로 다루세요: 폐기되기 전까지 새 access token을 계속 발급할 수 있습니다.
에러 처리
인증이 실패하면 — 삭제된 키, 만료된 토큰, 폐기된 세션 — 두 클라이언트 모두 ListenHubError를 던집니다:
import { OpenAPIClient, ListenHubError } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
try {
await client.getSubscription();
} catch (err) {
if (err instanceof ListenHubError) {
// err.status → HTTP status (e.g. 401)
// err.code → backend error code from the envelope
// err.requestId → include this when contacting support
console.error(`[${err.status}] ${err.message} (request ${err.requestId})`);
}
}ListenHubError는 status, code, requestId를 담고 있습니다. 401 또는 403은 자격 증명이 거부됐다는 뜻입니다 — API 키라면 교체하고, OAuth 토큰이라면 refresh를 실행하세요. 갱신마저 실패하면 사용자를 로그인 플로우로 다시 보내세요. 레이트 리밋에 걸린(429) 요청은 Retry-After에 따라 maxRetries(기본값 2)까지 자동으로 재시도되므로, 에러로 드러나는 경우는 드뭅니다.