-
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
base: main
Are you sure you want to change the base?
Changes from all commits
07dced8
db7beb9
2b872c7
a69a62f
9d4c580
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,102 @@ | ||
| // 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'; | ||
|
|
||
| /** | ||
| * Options for {@link LiveKitAPI}. Provide either an `apiKey` and `secret`, or a | ||
| * pre-signed `token`. Any omitted value falls back to its environment variable. | ||
| */ | ||
| export interface LiveKitAPIOptions extends ClientOptions { | ||
| /** 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; | ||
| /** Pre-signed token, sent verbatim. Falls back to the `LIVEKIT_TOKEN` env var. */ | ||
| token?: string; | ||
|
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. is there a scenario in which we'd want to allow all three to be set together? |
||
| } | ||
|
|
||
| /** | ||
| * 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('https://my.livekit.cloud', { apiKey, secret }); | ||
|
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. nit: if all of |
||
| * await api.room.createRoom({ name: 'my-room' }); | ||
| * ``` | ||
| */ | ||
| export class LiveKitAPI { | ||
| readonly #room: RoomServiceClient; | ||
|
|
||
| readonly #egress: EgressClient; | ||
|
|
||
| readonly #ingress: IngressClient; | ||
|
|
||
| readonly #sip: SipClient; | ||
|
|
||
| readonly #agentDispatch: AgentDispatchClient; | ||
|
|
||
| readonly #connector: ConnectorClient; | ||
|
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. nit: the livekit-server-sdk doesn't use |
||
|
|
||
| /** | ||
| * @param host - server host, including protocol. Falls back to the | ||
| * `LIVEKIT_URL` env var. | ||
| * @param options - credentials and client options | ||
| */ | ||
| constructor(host?: string, options: LiveKitAPIOptions = {}) { | ||
| host = 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 |
||
| 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.