-
Notifications
You must be signed in to change notification settings - Fork 110
LiveKitAPI entrypoint, ability to use a token instead of api key/secret #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
07dced8
db7beb9
2b872c7
a69a62f
9d4c580
2d635fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'livekit-server-sdk': minor | ||
| --- | ||
|
|
||
| Add `LiveKitAPI`, a unified entry point that exposes every server API through a property (`api.room`, `api.egress`, `api.ingress`, `api.sip`, `api.agentDispatch`, `api.connector`). It can be constructed with an API key and secret, or with a pre-signed token (no secret required, so it can run client-side), and falls back to the `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, and `LIVEKIT_TOKEN` environment variables. Individual clients also accept a `token` via `ClientOptions`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'livekit-server-sdk': minor | ||
| --- | ||
|
|
||
| Add `SipCallError`, a `TwirpError` subclass thrown by `SipClient.createSipParticipant` / `transferSipParticipant` when a call fails with a SIP status. It exposes the SIP response code and reason as `sipStatusCode` / `sipStatus` getters, while other error metadata remains available via `metadata`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // SPDX-FileCopyrightText: 2026 LiveKit, Inc. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import { AgentDispatchClient } from './AgentDispatchClient.js'; | ||
| import type { ClientOptions } from './ClientOptions.js'; | ||
| import { ConnectorClient } from './ConnectorClient.js'; | ||
| import { EgressClient } from './EgressClient.js'; | ||
| import { IngressClient } from './IngressClient.js'; | ||
| import { RoomServiceClient } from './RoomServiceClient.js'; | ||
| import { SipClient } from './SipClient.js'; | ||
|
|
||
| /** Server host and non-auth options, shared by both authentication modes. */ | ||
| interface LiveKitAPICommonOptions { | ||
| /** Server host, including protocol. Falls back to the `LIVEKIT_URL` env var. */ | ||
| host?: string; | ||
| /** Optional timeout, in seconds, for all server requests. */ | ||
| requestTimeout?: number; | ||
| /** | ||
| * Whether to fail over to alternative regions on retryable errors (LiveKit | ||
| * Cloud hosts only). Defaults to true; set to false to disable. | ||
| */ | ||
| failover?: boolean; | ||
| } | ||
|
|
||
| /** API key and secret authentication (recommended for backend use). */ | ||
| interface ApiKeyAuth { | ||
| /** API key. Falls back to the `LIVEKIT_API_KEY` env var. */ | ||
| apiKey?: string; | ||
| /** API secret. Falls back to the `LIVEKIT_API_SECRET` env var. */ | ||
| secret?: string; | ||
| token?: never; | ||
| } | ||
|
|
||
| /** Pre-signed token authentication (client-side use; no secret required). */ | ||
| interface TokenAuth { | ||
| /** Pre-signed token, sent verbatim. Falls back to the `LIVEKIT_TOKEN` env var. */ | ||
| token: string; | ||
| apiKey?: never; | ||
| secret?: never; | ||
| } | ||
|
|
||
| /** | ||
| * Options for {@link LiveKitAPI}. Provide either an `apiKey` and `secret` or a | ||
| * pre-signed `token` — the two modes are mutually exclusive. Any omitted value | ||
| * falls back to its environment variable (`LIVEKIT_URL`, `LIVEKIT_API_KEY`, | ||
| * `LIVEKIT_API_SECRET`, `LIVEKIT_TOKEN`). | ||
| */ | ||
| export type LiveKitAPIOptions = LiveKitAPICommonOptions & (ApiKeyAuth | TokenAuth); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
|
|
||
| /** | ||
| * A single entry point to every LiveKit server API, exposing each service | ||
| * through a property, e.g. `api.room.createRoom(...)`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const api = new LiveKitAPI({ apiKey, secret }); // or new LiveKitAPI() to read from env | ||
| * await api.room.createRoom({ name: 'my-room' }); | ||
| * ``` | ||
| */ | ||
| export class LiveKitAPI { | ||
| private readonly _room: RoomServiceClient; | ||
|
|
||
| private readonly _egress: EgressClient; | ||
|
|
||
| private readonly _ingress: IngressClient; | ||
|
|
||
| private readonly _sip: SipClient; | ||
|
|
||
| private readonly _agentDispatch: AgentDispatchClient; | ||
|
|
||
| private readonly _connector: ConnectorClient; | ||
|
|
||
| /** | ||
| * @param options - server host, credentials, and client options; each value | ||
| * falls back to its environment variable when omitted. | ||
| */ | ||
| constructor(options: LiveKitAPIOptions = {}) { | ||
| const host = options.host || process.env.LIVEKIT_URL; | ||
| if (!host) { | ||
| throw new Error('host is required (pass it or set LIVEKIT_URL)'); | ||
| } | ||
| const { apiKey, secret } = options; | ||
| // Only fall back to LIVEKIT_TOKEN when no explicit credentials were given, so | ||
| // an ambient token can't silently override a passed-in api key and secret. | ||
| const token = options.token || (apiKey || secret ? undefined : process.env.LIVEKIT_TOKEN); | ||
| if (!token && !(apiKey ?? process.env.LIVEKIT_API_KEY)) { | ||
| throw new Error('either a token or an API key and secret are required'); | ||
| } | ||
|
|
||
| const clientOptions: ClientOptions = { | ||
| requestTimeout: options.requestTimeout, | ||
| failover: options.failover, | ||
| token, | ||
| }; | ||
| this._room = new RoomServiceClient(host, apiKey, secret, clientOptions); | ||
| this._egress = new EgressClient(host, apiKey, secret, clientOptions); | ||
| this._ingress = new IngressClient(host, apiKey, secret, clientOptions); | ||
| this._sip = new SipClient(host, apiKey, secret, clientOptions); | ||
| this._agentDispatch = new AgentDispatchClient(host, apiKey, secret, clientOptions); | ||
| this._connector = new ConnectorClient(host, apiKey, secret, clientOptions); | ||
| } | ||
|
|
||
| get room(): RoomServiceClient { | ||
| return this._room; | ||
| } | ||
|
|
||
| get egress(): EgressClient { | ||
| return this._egress; | ||
| } | ||
|
|
||
| get ingress(): IngressClient { | ||
| return this._ingress; | ||
| } | ||
|
|
||
| get sip(): SipClient { | ||
| return this._sip; | ||
| } | ||
|
|
||
| get agentDispatch(): AgentDispatchClient { | ||
| return this._agentDispatch; | ||
| } | ||
|
|
||
| get connector(): ConnectorClient { | ||
| return this._connector; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,20 @@ | |
| import { AccessToken } from './AccessToken.js'; | ||
| import type { SIPGrant, VideoGrant } from './grants.js'; | ||
|
|
||
| /** | ||
| * Authentication options for a service client. | ||
| */ | ||
| export interface ServiceBaseOptions { | ||
| /** API Key. */ | ||
| apiKey?: string; | ||
| /** API Secret. */ | ||
| secret?: string; | ||
| /** Token TTL. Defaults to `10m`. */ | ||
| ttl?: string; | ||
| /** Pre-signed token; sent verbatim, skipping per-call signing. */ | ||
| token?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Utilities to handle authentication | ||
| */ | ||
|
|
@@ -12,20 +26,37 @@ export class ServiceBase { | |
|
|
||
| private readonly secret?: string; | ||
|
|
||
| private readonly token?: string; | ||
|
|
||
| private readonly ttl: string; | ||
|
|
||
| /** | ||
| * @param options - authentication options | ||
| */ | ||
| constructor(options?: ServiceBaseOptions); | ||
| /** | ||
| * @deprecated pass a {@link ServiceBaseOptions} object instead. | ||
| * @param apiKey - API Key. | ||
| * @param secret - API Secret. | ||
| * @param ttl - token TTL | ||
| */ | ||
| constructor(apiKey?: string, secret?: string, ttl?: string) { | ||
| this.apiKey = apiKey; | ||
| this.secret = secret; | ||
| this.ttl = ttl || '10m'; | ||
| constructor(apiKey?: string, secret?: string, ttl?: string); | ||
| constructor(apiKeyOrOptions?: string | ServiceBaseOptions, secret?: string, ttl?: string) { | ||
| const options: ServiceBaseOptions = | ||
| typeof apiKeyOrOptions === 'object' | ||
| ? apiKeyOrOptions | ||
| : { apiKey: apiKeyOrOptions, secret, ttl }; | ||
| this.apiKey = options.apiKey; | ||
| this.secret = options.secret; | ||
| this.ttl = options.ttl || '10m'; | ||
| this.token = options.token; | ||
| } | ||
|
|
||
| async authHeader(grant: VideoGrant, sip?: SIPGrant): Promise<Record<string, string>> { | ||
| // A pre-signed token is sent verbatim; the caller is responsible for its grants. | ||
| if (this.token) { | ||
| return { Authorization: `Bearer ${this.token}` }; | ||
| } | ||
|
Comment on lines
55
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: It might be nice to expose here some sort of event the caller can use to determine if the token is expired or not. And if it is expired, then give the caller the ability to potentially regenerate it before actually making the API request. This sets the stage well for a future
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see reply the Rust SDK PR. I would like the server to the be the authority on token validity, instead of the client. making validity decisions on the client relies on having a correct client clock. I don't think that's a good idea. |
||
| const at = new AccessToken(this.apiKey, this.secret, { ttl: this.ttl }); | ||
| if (grant) { | ||
| at.addGrant(grant); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.