身份认证
服务端场景用 API key 认证 SDK;代表已登录用户操作时,用 OAuth 用户令牌认证。
SDK 提供两个 client,认证方式不同。根据代码运行的位置选择对应的那个。
OpenAPIClient | ListenHubClient | |
|---|---|---|
| 凭据 | API key(lh_sk_…) | OAuth 用户 access token |
| 发送的请求头 | Authorization: Bearer <apiKey> | Authorization: Bearer <accessToken> |
| 身份 | 你的账户 / key 持有者 | 已登录的用户 |
| 运行环境 | 服务器、脚本、CI | 面向用户的应用 |
| Base URL | https://api.marswave.ai/openapi | https://api.listenhub.ai/api |
API key 是拥有账户完整访问权限的密钥,必须留在服务端。绝不要把 key 放进浏览器、移动端或任何客户端打包产物。面向用户的应用请用 ListenHubClient + OAuth,让每个请求都以用户自己的账户身份执行。
API key(OpenAPIClient)
服务端和自动化场景用 OpenAPIClient。它面向公开的 OpenAPI 接口,用一个代表你账户的 API key 认证。
创建 key
在控制台 listenhub.ai/settings/api-keys 生成 key。key 的格式为 lh_sk_<keyId>_<secret>。secret 只在创建时显示一次——立即复制,并存到只有你的服务端能读到的地方。
构造 client
显式传入 key,或设置 LISTENHUB_API_KEY 后用无参构造。两者都不存在时,构造函数会抛错。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
// 读取 process.env.LISTENHUB_API_KEY
const client = new OpenAPIClient();
const { items } = await client.listSpeakers({ language: 'en' });运行时把 key 放进环境变量:
LISTENHUB_API_KEY=lh_sk_... node app.jsimport { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient({
apiKey: process.env.LISTENHUB_API_KEY, // 从你的密钥存储中加载
});优先从密钥管理器或环境变量读取 key,而不要硬编码。上面的示例同样从环境变量读取——绝不要把字面量 lh_sk_… 字符串粘进会提交的源码里。
client 会在每个请求上自动设置 Authorization: Bearer <apiKey>。这里没有需要刷新的令牌——key 在你轮换或删除它之前一直有效。
等价的原始 HTTP 请求
如果不用 SDK,向 OpenAPI base URL 发送相同的请求头:
curl https://api.marswave.ai/openapi/v1/speakers/list?language=en \
-H "Authorization: Bearer $LISTENHUB_API_KEY"轮换 key
key 泄露时,在控制台删除它并签发一个新的。删除立即生效——任何仍在使用旧 key 的请求都会开始返回认证错误(见 错误处理)。把替换的新 key 滚动进密钥存储并重新部署。
OAuth 用户令牌(ListenHubClient)
当每个请求都必须以某个具体的已登录用户身份执行时,用 ListenHubClient——例如桌面工具或 CLI,用户用自己的 ListenHub 账户登录。这个 client 携带的不是 API key,而是一个 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 失效(登出)。 |
登录用的 client 本身不需要凭据,所以构造一个裸的 ListenHubClient 来驱动 connectInit / connectToken。流程如下:
启动一个本地服务器接收 OAuth 回调,然后用该端口调用 connectInit。在用户浏览器中打开返回的 authUrl。
用户在浏览器中授权。ListenHub 重定向到 http://127.0.0.1:<callbackPort>/?code=<code>。从该请求中读取 code。
调用 connectToken({ sessionId, code }),用 code 换取 accessToken、refreshToken 和 expiresIn(access token 距过期的秒数)。把两个令牌都持久化。
用 access token 构造已认证的 client:new ListenHubClient({ accessToken })。
import * as http from 'node:http';
import { ListenHubClient } from '@marswave/listenhub-sdk';
// 1. 登录 client 不需要凭据
const loginClient = new ListenHubClient();
// 2. 起一个临时回调服务器,再启动会话
const { port, codePromise, server } = startCallbackServer();
const { authUrl, sessionId } = await loginClient.connectInit({ callbackPort: port });
const open = (await import('open')).default;
await open(authUrl); // 用户在这里登录
// 3. 用回调 code 换取令牌
const code = await codePromise;
const tokens = await loginClient.connectToken({ sessionId, code });
server.close();
// tokens: { accessToken, refreshToken, expiresIn }
// 把令牌持久化到用户机器上私密的位置。
// 4. 构造已认证的 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');
}
});
// 端口 0 → 由操作系统分配一个空闲端口
server.listen(0, '127.0.0.1');
const port = (server.address() as { port: number }).port;
return { port, codePromise, server };
}保持令牌有效
access token 在 expiresIn 秒后过期。有两种方式保持认证状态。
传静态令牌——当 client 生命周期很短时(一次脚本运行、一批请求):
const client = new ListenHubClient({ accessToken: tokens.accessToken });传 getter——当 client 生命周期很长时。accessToken 接受 () => string | undefined,SDK 在每个请求之前调用它。把当前令牌存在一个变量里,在旁路刷新它,getter 就总能返回最新值:
let current = tokens;
const client = new ListenHubClient({
// 每个请求前调用——返回你手头最新的令牌
accessToken: () => current.accessToken,
});
// 用定时器刷新(或在临近过期时惰性刷新)并替换进去
async function refreshTokens() {
current = await loginClient.refresh({ refreshToken: current.refreshToken });
}refresh 会返回一个新的 accessToken 和 一个新的 refreshToken。两个都要持久化——存下最新的 refreshToken,让用户跨重启保持登录状态,并用它做下一次刷新。
登出
用 refresh token 调用 revoke,让会话在服务端失效,然后在本地丢弃存储的令牌:
await loginClient.revoke({ refreshToken: current.refreshToken });
// 然后从本地存储删除 accessToken + refreshToken。凭据存放在哪里
- API key 放在服务端环境变量或密钥管理器里——通过
process.env.LISTENHUB_API_KEY读取。绝不能进入浏览器,也不能提交到版本控制。 - OAuth 令牌 是按用户区分的。把
accessToken和refreshToken存到该用户会话私密的位置——对 CLI 或桌面工具,可以是用户配置目录下权限为0600的文件。refreshToken要当作敏感信息:在被吊销之前,它可以一直签发新的 access token。
错误处理
认证失败时——被删除的 key、过期的令牌、被吊销的会话——两个 client 都会抛出 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 状态码(如 401)
// err.code → 响应封套里的后端错误码
// err.requestId → 联系支持时附上它
console.error(`[${err.status}] ${err.message} (request ${err.requestId})`);
}
}ListenHubError 携带 status、code 和 requestId。401 或 403 表示凭据被拒绝——对 API key,轮换它;对 OAuth 令牌,执行 refresh,如果刷新也失败,把用户重新带回登录流程。被限流(429)的请求会基于 Retry-After 自动重试,最多 maxRetries 次(默认 2),所以很少以错误形式暴露出来。