ListenHubSDKs & CLI
JavaScript SDK

身份认证

服务端场景用 API key 认证 SDK;代表已登录用户操作时,用 OAuth 用户令牌认证。

SDK 提供两个 client,认证方式不同。根据代码运行的位置选择对应的那个。

OpenAPIClientListenHubClient
凭据API key(lh_sk_…OAuth 用户 access token
发送的请求头Authorization: Bearer <apiKey>Authorization: Bearer <accessToken>
身份你的账户 / key 持有者已登录的用户
运行环境服务器、脚本、CI面向用户的应用
Base URLhttps://api.marswave.ai/openapihttps://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.js
import { 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 换取令牌。返回 accessTokenrefreshTokenexpiresIn
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 换取 accessTokenrefreshTokenexpiresIn(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 令牌 是按用户区分的。把 accessTokenrefreshToken 存到该用户会话私密的位置——对 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 携带 statuscoderequestId401403 表示凭据被拒绝——对 API key,轮换它;对 OAuth 令牌,执行 refresh,如果刷新也失败,把用户重新带回登录流程。被限流(429)的请求会基于 Retry-After 自动重试,最多 maxRetries 次(默认 2),所以很少以错误形式暴露出来。

后续步骤

On this page