Skip to main content

livekit_token/
access_token.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::HashMap,
17    env,
18    fmt::Debug,
19    ops::Add,
20    time::{Duration, SystemTime, UNIX_EPOCH},
21};
22
23use jsonwebtoken::{self, DecodingKey, EncodingKey, Header};
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use crate::get_env_keys;
28
29pub const DEFAULT_TTL: Duration = Duration::from_secs(3600 * 6); // 6 hours
30
31#[derive(Debug, Error)]
32pub enum AccessTokenError {
33    #[error("Invalid API Key or Secret Key")]
34    InvalidKeys,
35    #[error("Invalid environment")]
36    InvalidEnv(#[from] env::VarError),
37    #[error("invalid claims: {0}")]
38    InvalidClaims(&'static str),
39    #[error("failed to encode jwt")]
40    Encoding(#[from] jsonwebtoken::errors::Error),
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
44#[serde(default, rename_all = "camelCase")]
45pub struct VideoGrants {
46    // actions on rooms
47    #[serde(skip_serializing_if = "is_default")]
48    pub room_create: bool,
49    #[serde(skip_serializing_if = "is_default")]
50    pub room_list: bool,
51    #[serde(skip_serializing_if = "is_default")]
52    pub room_record: bool,
53
54    // actions on a particular room
55    #[serde(skip_serializing_if = "is_default")]
56    pub room_admin: bool,
57    #[serde(skip_serializing_if = "is_default")]
58    pub room_join: bool,
59    #[serde(skip_serializing_if = "is_default")]
60    pub room: String,
61    #[serde(skip_serializing_if = "is_default")]
62    pub destination_room: String,
63
64    // permissions within a room
65    #[serde(skip_serializing_if = "is_default")]
66    pub can_publish: Option<bool>,
67    #[serde(skip_serializing_if = "is_default")]
68    pub can_subscribe: Option<bool>,
69    #[serde(skip_serializing_if = "is_default")]
70    pub can_publish_data: Option<bool>,
71
72    // TrackSource types that a participant may publish.
73    // When set, it supercedes CanPublish. Only sources explicitly set here can be published
74    #[serde(skip_serializing_if = "is_default")]
75    pub can_publish_sources: Vec<String>, // keys keep track of each source
76
77    // by default, a participant is not allowed to update its own metadata
78    #[serde(skip_serializing_if = "is_default")]
79    pub can_update_own_metadata: Option<bool>,
80
81    // actions on ingresses
82    #[serde(skip_serializing_if = "is_default")]
83    pub ingress_admin: bool, // applies to all ingress
84
85    // participant is not visible to other participants (useful when making bots)
86    #[serde(skip_serializing_if = "is_default")]
87    pub hidden: bool,
88
89    // indicates to the room that current participant is a recorder
90    #[serde(skip_serializing_if = "is_default")]
91    pub recorder: bool,
92
93    // indicates to the room that current participant is an agent
94    #[serde(skip_serializing_if = "is_default")]
95    pub agent: bool,
96}
97
98fn is_default<T: Default + PartialEq>(v: &T) -> bool {
99    *v == T::default()
100}
101
102impl VideoGrants {
103    pub fn can_publish(&self) -> bool {
104        self.can_publish.unwrap_or(true)
105    }
106
107    pub fn can_subscribe(&self) -> bool {
108        self.can_subscribe.unwrap_or(true)
109    }
110
111    pub fn can_publish_data(&self) -> bool {
112        self.can_publish_data.unwrap_or_else(|| self.can_publish())
113    }
114
115    pub fn can_update_own_metadata(&self) -> bool {
116        self.can_update_own_metadata.unwrap_or(false)
117    }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct SIPGrants {
123    // manage sip resources
124    #[serde(default, skip_serializing_if = "is_default")]
125    pub admin: bool,
126    // make outbound calls
127    #[serde(default, skip_serializing_if = "is_default")]
128    pub call: bool,
129}
130
131impl Default for SIPGrants {
132    fn default() -> Self {
133        Self { admin: false, call: false }
134    }
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
138#[serde(default, rename_all = "camelCase")]
139pub struct AgentGrants {
140    // create/update/delete Cloud Agents
141    #[serde(skip_serializing_if = "is_default")]
142    pub admin: bool,
143    // manage simulations and scenarios
144    #[serde(skip_serializing_if = "is_default")]
145    pub simulation_admin: bool,
146    // manage the project's agent dispatch queue
147    #[serde(skip_serializing_if = "is_default")]
148    pub dispatch_admin: bool,
149}
150
151#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
152#[serde(default, rename_all = "camelCase")]
153pub struct AgentEndpointGrants {
154    // invoke an agent's non-public HTTP endpoints
155    #[serde(skip_serializing_if = "is_default")]
156    pub call: bool,
157    // empty grants every agent in the project
158    #[serde(skip_serializing_if = "is_default")]
159    pub agent_name: String,
160    // empty grants every deployment
161    #[serde(skip_serializing_if = "is_default")]
162    pub deployment: String,
163}
164
165impl AgentEndpointGrants {
166    pub fn allows(&self, agent_name: &str, deployment: &str) -> bool {
167        self.call
168            && (self.agent_name.is_empty() || self.agent_name == agent_name)
169            && (self.deployment.is_empty() || self.deployment == deployment)
170    }
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
174#[serde(default, rename_all = "camelCase")]
175pub struct AgentThreadGrants {
176    #[serde(skip_serializing_if = "is_default")]
177    pub all: bool,
178    #[serde(skip_serializing_if = "is_default")]
179    pub scope: String,
180    #[serde(skip_serializing_if = "is_default")]
181    pub thread_ids: Vec<String>,
182    #[serde(skip_serializing_if = "is_default")]
183    pub list: bool,
184    #[serde(skip_serializing_if = "is_default")]
185    pub create: bool,
186    #[serde(skip_serializing_if = "is_default")]
187    pub write: bool,
188    #[serde(skip_serializing_if = "is_default")]
189    pub read: bool,
190    #[serde(skip_serializing_if = "is_default")]
191    pub delete: bool,
192    // open a covered thread's database, whose id is the thread's
193    #[serde(skip_serializing_if = "is_default")]
194    pub sql: bool,
195}
196
197impl AgentThreadGrants {
198    pub fn can_create(&self, scope: &str) -> bool {
199        self.create && self.in_scope(scope)
200    }
201
202    pub fn can_list(&self, scope: &str) -> bool {
203        self.list && self.in_scope(scope)
204    }
205
206    pub fn can_update(&self) -> bool {
207        self.all && self.write
208    }
209
210    pub fn can_read(&self, thread_id: &str) -> bool {
211        self.read && self.includes(thread_id)
212    }
213
214    pub fn can_write(&self, thread_id: &str) -> bool {
215        self.write && self.includes(thread_id)
216    }
217
218    pub fn can_delete(&self, thread_id: &str) -> bool {
219        self.delete && self.includes(thread_id)
220    }
221
222    pub fn can_sql(&self, thread_id: &str) -> bool {
223        self.sql && self.includes(thread_id)
224    }
225
226    fn in_scope(&self, scope: &str) -> bool {
227        self.all || (!self.scope.is_empty() && self.scope == scope)
228    }
229
230    fn includes(&self, thread_id: &str) -> bool {
231        !thread_id.is_empty() && (self.all || self.thread_ids.iter().any(|id| id == thread_id))
232    }
233}
234
235#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
236#[serde(default, rename_all = "camelCase")]
237pub struct AgentDatabaseGrants {
238    // databases this token may read, write or delete
239    #[serde(skip_serializing_if = "is_default")]
240    pub database_ids: Vec<String>,
241    #[serde(skip_serializing_if = "is_default")]
242    pub read: bool,
243    #[serde(skip_serializing_if = "is_default")]
244    pub write: bool,
245    #[serde(skip_serializing_if = "is_default")]
246    pub delete: bool,
247    // create databases in the project
248    #[serde(skip_serializing_if = "is_default")]
249    pub create: bool,
250    // list the project's databases
251    #[serde(skip_serializing_if = "is_default")]
252    pub list: bool,
253}
254
255impl AgentDatabaseGrants {
256    pub fn can_read(&self, database_id: &str) -> bool {
257        self.read && self.includes(database_id)
258    }
259
260    pub fn can_write(&self, database_id: &str) -> bool {
261        self.write && self.includes(database_id)
262    }
263
264    pub fn can_delete(&self, database_id: &str) -> bool {
265        self.delete && self.includes(database_id)
266    }
267
268    pub fn can_create(&self) -> bool {
269        self.create
270    }
271
272    pub fn can_list(&self) -> bool {
273        self.list
274    }
275
276    fn includes(&self, database_id: &str) -> bool {
277        !database_id.is_empty() && self.database_ids.iter().any(|id| id == database_id)
278    }
279}
280
281/// Grants for LiveKit Inference, set with [`AccessToken::with_inference_grants`].
282#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
283#[serde(default, rename_all = "camelCase")]
284#[non_exhaustive]
285pub struct InferenceGrants {
286    /// Use LiveKit Inference (LLM, STT, TTS).
287    #[serde(skip_serializing_if = "is_default")]
288    pub perform: bool,
289}
290
291impl InferenceGrants {
292    /// Allows using LiveKit Inference (LLM, STT, TTS).
293    pub fn with_perform(mut self) -> Self {
294        self.perform = true;
295        self
296    }
297}
298
299/// Grants for LiveKit observability, set with [`AccessToken::with_observability_grants`].
300#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
301#[serde(default, rename_all = "camelCase")]
302#[non_exhaustive]
303pub struct ObservabilityGrants {
304    /// Publish observability data.
305    #[serde(skip_serializing_if = "is_default")]
306    pub write: bool,
307}
308
309impl ObservabilityGrants {
310    /// Allows publishing observability data.
311    pub fn with_write(mut self) -> Self {
312        self.write = true;
313        self
314    }
315}
316
317#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
318#[serde(default)]
319#[serde(rename_all = "camelCase")]
320pub struct Claims {
321    pub exp: usize,  // Expiration
322    pub iss: String, // ApiKey
323    pub nbf: usize,
324    #[serde(skip_serializing_if = "is_default")]
325    pub sub: String, // Identity
326
327    #[serde(skip_serializing_if = "is_default")]
328    pub name: String,
329    #[serde(skip_serializing_if = "is_default")]
330    pub kind: String,
331    #[serde(skip_serializing_if = "is_default")]
332    pub video: VideoGrants,
333    #[serde(skip_serializing_if = "is_default")]
334    pub sip: SIPGrants,
335    #[serde(skip_serializing_if = "is_default")]
336    pub agent: AgentGrants,
337    #[serde(skip_serializing_if = "is_default")]
338    pub agent_endpoint: AgentEndpointGrants,
339    #[serde(skip_serializing_if = "is_default")]
340    pub agent_thread: AgentThreadGrants,
341    #[serde(skip_serializing_if = "is_default")]
342    pub agent_database: AgentDatabaseGrants,
343    #[serde(skip_serializing_if = "is_default")]
344    pub inference: InferenceGrants,
345    #[serde(skip_serializing_if = "is_default")]
346    pub observability: ObservabilityGrants,
347    #[serde(skip_serializing_if = "is_default")]
348    pub sha256: String, // Used to verify the integrity of the message body
349    #[serde(skip_serializing_if = "is_default")]
350    pub metadata: String,
351    #[serde(skip_serializing_if = "is_default")]
352    pub attributes: HashMap<String, String>,
353    #[serde(skip_serializing_if = "is_default")]
354    pub room_config: Option<livekit_protocol::RoomConfiguration>,
355}
356
357impl Claims {
358    pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
359        crate::jwt_provider::ensure_installed();
360        let token = jsonwebtoken::dangerous::insecure_decode::<Claims>(token)?;
361        Ok(token.claims)
362    }
363}
364
365#[derive(Clone)]
366pub struct AccessToken {
367    api_key: String,
368    api_secret: String,
369    claims: Claims,
370}
371
372impl Debug for AccessToken {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        // Don't show api_secret here
375        f.debug_struct("AccessToken")
376            .field("api_key", &self.api_key)
377            .field("claims", &self.claims)
378            .finish()
379    }
380}
381
382impl AccessToken {
383    pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
384        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
385        Self {
386            api_key: api_key.to_owned(),
387            api_secret: api_secret.to_owned(),
388            claims: Claims {
389                exp: now.add(DEFAULT_TTL).as_secs() as usize,
390                iss: api_key.to_owned(),
391                nbf: now.as_secs() as usize,
392                sub: Default::default(),
393                name: Default::default(),
394                kind: Default::default(),
395                video: VideoGrants::default(),
396                sip: SIPGrants::default(),
397                agent: AgentGrants::default(),
398                agent_endpoint: AgentEndpointGrants::default(),
399                agent_thread: AgentThreadGrants::default(),
400                agent_database: AgentDatabaseGrants::default(),
401                inference: InferenceGrants::default(),
402                observability: ObservabilityGrants::default(),
403                sha256: Default::default(),
404                metadata: Default::default(),
405                attributes: HashMap::new(),
406                room_config: Default::default(),
407            },
408        }
409    }
410
411    #[cfg(test)]
412    pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
413        Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
414    }
415
416    pub fn new() -> Result<Self, AccessTokenError> {
417        // Try to get the API Key and the Secret Key from the environment
418        let (api_key, api_secret) = get_env_keys()?;
419        Ok(Self::with_api_key(&api_key, &api_secret))
420    }
421
422    pub fn with_ttl(mut self, ttl: Duration) -> Self {
423        let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
424        self.claims.exp = time.as_secs() as usize;
425        self
426    }
427
428    pub fn with_grants(mut self, grants: VideoGrants) -> Self {
429        self.claims.video = grants;
430        self
431    }
432
433    pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
434        self.claims.sip = grants;
435        self
436    }
437
438    pub fn with_agent_grants(mut self, grants: AgentGrants) -> Self {
439        self.claims.agent = grants;
440        self
441    }
442
443    pub fn with_agent_endpoint_grants(mut self, grants: AgentEndpointGrants) -> Self {
444        self.claims.agent_endpoint = grants;
445        self
446    }
447
448    pub fn with_agent_thread_grants(mut self, grants: AgentThreadGrants) -> Self {
449        self.claims.agent_thread = grants;
450        self
451    }
452
453    pub fn with_agent_database_grants(mut self, grants: AgentDatabaseGrants) -> Self {
454        self.claims.agent_database = grants;
455        self
456    }
457
458    pub fn with_inference_grants(mut self, grants: InferenceGrants) -> Self {
459        self.claims.inference = grants;
460        self
461    }
462
463    /// Sets the observability grants for this token.
464    pub fn with_observability_grants(mut self, grants: ObservabilityGrants) -> Self {
465        self.claims.observability = grants;
466        self
467    }
468
469    pub fn with_identity(mut self, identity: &str) -> Self {
470        self.claims.sub = identity.to_owned();
471        self
472    }
473
474    pub fn with_name(mut self, name: &str) -> Self {
475        self.claims.name = name.to_owned();
476        self
477    }
478
479    pub fn with_kind(mut self, kind: &str) -> Self {
480        self.claims.kind = kind.to_owned();
481        self
482    }
483
484    pub fn with_metadata(mut self, metadata: &str) -> Self {
485        self.claims.metadata = metadata.to_owned();
486        self
487    }
488
489    pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
490    where
491        I: IntoIterator<Item = (K, V)>,
492        K: Into<String>,
493        V: Into<String>,
494    {
495        self.claims.attributes =
496            attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
497        self
498    }
499
500    pub fn with_sha256(mut self, sha256: &str) -> Self {
501        self.claims.sha256 = sha256.to_owned();
502        self
503    }
504
505    pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
506        self.claims.room_config = Some(config);
507        self
508    }
509
510    pub fn to_jwt(self) -> Result<String, AccessTokenError> {
511        crate::jwt_provider::ensure_installed();
512        if self.api_key.is_empty() || self.api_secret.is_empty() {
513            return Err(AccessTokenError::InvalidKeys);
514        }
515
516        if self.claims.video.room_join
517            && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
518        {
519            return Err(AccessTokenError::InvalidClaims(
520                "token grants room_join but doesn't have an identity or room",
521            ));
522        }
523
524        Ok(jsonwebtoken::encode(
525            &Header::new(jsonwebtoken::Algorithm::HS256),
526            &self.claims,
527            &EncodingKey::from_secret(self.api_secret.as_ref()),
528        )?)
529    }
530}
531
532#[derive(Clone)]
533pub struct TokenVerifier {
534    api_key: String,
535    api_secret: String,
536}
537
538impl Debug for TokenVerifier {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
541    }
542}
543
544impl TokenVerifier {
545    pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
546        Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
547    }
548
549    pub fn new() -> Result<Self, AccessTokenError> {
550        let (api_key, api_secret) = get_env_keys()?;
551        Ok(Self::with_api_key(&api_key, &api_secret))
552    }
553
554    pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
555        crate::jwt_provider::ensure_installed();
556        let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
557        validation.validate_exp = true;
558        validation.validate_nbf = true;
559        validation.set_issuer(&[&self.api_key]);
560
561        let token = jsonwebtoken::decode::<Claims>(
562            token,
563            &DecodingKey::from_secret(self.api_secret.as_ref()),
564            &validation,
565        )?;
566
567        Ok(token.claims)
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use std::time::Duration;
574
575    use super::{
576        AccessToken, AgentDatabaseGrants, AgentEndpointGrants, AgentGrants, AgentThreadGrants,
577        Claims, InferenceGrants, ObservabilityGrants, SIPGrants, TokenVerifier, VideoGrants,
578    };
579
580    const TEST_API_KEY: &str = "myapikey";
581    const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
582    const TEST_TOKEN: &str = include_str!("test_token.txt");
583
584    #[test]
585    fn test_access_token() {
586        let room_config = livekit_protocol::RoomConfiguration {
587            name: "name".to_string(),
588            agents: vec![livekit_protocol::RoomAgentDispatch {
589                agent_name: "test-agent".to_string(),
590                metadata: "test-metadata".to_string(),
591                ..Default::default()
592            }],
593            ..Default::default()
594        };
595
596        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
597            .with_ttl(Duration::from_secs(60))
598            .with_identity("test")
599            .with_name("test")
600            .with_grants(VideoGrants::default())
601            .with_room_config(room_config.clone())
602            .to_jwt()
603            .unwrap();
604
605        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
606        let claims = verifier.verify(&token).unwrap();
607
608        assert_eq!(claims.sub, "test");
609        assert_eq!(claims.name, "test");
610        assert_eq!(claims.iss, TEST_API_KEY);
611        assert_eq!(claims.room_config, Some(room_config));
612
613        let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
614        assert!(incorrect_issuer.verify(&token).is_err());
615
616        let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
617        assert!(incorrect_token.verify(&token).is_err());
618    }
619
620    #[test]
621    fn test_verify_token_with_room_config() {
622        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
623        // This token was generated using the Python SDK.
624        let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
625
626        assert_eq!(
627            super::Claims {
628                sub: "identity".to_string(),
629                name: "name".to_string(),
630                room_config: Some(livekit_protocol::RoomConfiguration {
631                    agents: vec![livekit_protocol::RoomAgentDispatch {
632                        agent_name: "test-agent".to_string(),
633                        metadata: "test-metadata".to_string(),
634                        ..Default::default()
635                    }],
636                    ..Default::default()
637                }),
638                ..claims.clone()
639            },
640            claims
641        );
642    }
643
644    #[test]
645    fn test_unverified_token() {
646        let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
647
648        assert_eq!(claims.sub, "identity");
649        assert_eq!(claims.name, "name");
650        assert_eq!(claims.iss, TEST_API_KEY);
651        assert_eq!(
652            claims.room_config,
653            Some(livekit_protocol::RoomConfiguration {
654                agents: vec![livekit_protocol::RoomAgentDispatch {
655                    agent_name: "test-agent".to_string(),
656                    metadata: "test-metadata".to_string(),
657                    ..Default::default()
658                }],
659                ..Default::default()
660            })
661        );
662
663        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
664            .with_ttl(Duration::from_secs(60))
665            .with_identity("test")
666            .with_name("test")
667            .with_grants(VideoGrants {
668                room_join: true,
669                room: "test-room".to_string(),
670                ..Default::default()
671            })
672            .to_jwt()
673            .unwrap();
674
675        let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
676        assert_eq!(claims.sub, "test");
677        assert_eq!(claims.name, "test");
678        assert_eq!(claims.video.room, "test-room");
679        assert!(claims.video.room_join);
680
681        let parts: Vec<&str> = token.split('.').collect();
682        let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
683
684        let claims = Claims::from_unverified(&malformed_token)
685            .expect("Failed to parse token with wrong signature");
686        assert_eq!(claims.sub, "test");
687        assert_eq!(claims.name, "test");
688    }
689
690    #[test]
691    fn test_agent_grant_and_kind() {
692        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
693            .with_ttl(Duration::from_secs(60))
694            .with_identity("agent-1")
695            .with_kind("agent")
696            .with_grants(VideoGrants {
697                room_join: true,
698                room: "test-room".to_string(),
699                agent: true,
700                ..Default::default()
701            })
702            .to_jwt()
703            .expect("Failed to create token");
704
705        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
706        let claims = verifier.verify(&token).expect("Failed to verify token.");
707        assert_eq!(claims.kind, "agent");
708        assert!(claims.video.agent);
709
710        let payload = |token: &str| {
711            let _ = Claims::from_unverified(token).expect("Failed to parse token");
712            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(token)
713                .expect("Failed to decode token")
714                .claims
715        };
716        let bare = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
717            .with_ttl(Duration::from_secs(60))
718            .with_grants(VideoGrants::default())
719            .to_jwt()
720            .expect("Failed to create token");
721        let p = payload(&bare);
722        let mut keys: Vec<&str> = p.as_object().unwrap().keys().map(String::as_str).collect();
723        keys.sort_unstable();
724        assert_eq!(keys, ["exp", "iss", "nbf"], "{p}");
725
726        let agent = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
727            .with_ttl(Duration::from_secs(60))
728            .with_grants(VideoGrants {
729                agent: true,
730                can_publish: Some(false),
731                ..Default::default()
732            })
733            .to_jwt()
734            .expect("Failed to create token");
735        let p = payload(&agent);
736        assert_eq!(p["video"], serde_json::json!({"agent": true, "canPublish": false}), "{p}");
737        let claims = Claims::from_unverified(&agent).expect("Failed to parse token");
738        assert!(claims.video.agent && !claims.video.can_publish() && claims.video.can_subscribe());
739        assert!(!claims.video.can_publish_data(), "absent canPublishData follows canPublish");
740    }
741
742    #[test]
743    fn test_defaults_are_not_serialized() {
744        assert_eq!(serde_json::to_string(&VideoGrants::default()).unwrap(), "{}");
745        let parsed: VideoGrants = serde_json::from_str("{}").unwrap();
746        assert_eq!(parsed, VideoGrants::default());
747        assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
748        let explicit = VideoGrants {
749            can_publish: Some(false),
750            can_publish_data: Some(true),
751            ..Default::default()
752        };
753        assert_eq!(
754            serde_json::to_string(&explicit).unwrap(),
755            r#"{"canPublish":false,"canPublishData":true}"#
756        );
757        assert!(explicit.can_publish_data());
758
759        assert_eq!(serde_json::to_string(&SIPGrants::default()).unwrap(), "{}");
760        let parsed: SIPGrants = serde_json::from_str("{}").unwrap();
761        assert_eq!(parsed, SIPGrants::default());
762        assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
763
764        let claims = Claims { exp: 1, iss: "k".to_string(), nbf: 0, ..Default::default() };
765        let json = r#"{"exp":1,"iss":"k","nbf":0}"#;
766        assert_eq!(serde_json::to_string(&claims).unwrap(), json);
767        let parsed: Claims = serde_json::from_str(json).unwrap();
768        assert_eq!(parsed, claims);
769        assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
770    }
771
772    #[test]
773    fn test_agent_grants_claim_names() {
774        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
775            .with_agent_grants(AgentGrants { dispatch_admin: true, ..Default::default() })
776            .with_agent_endpoint_grants(AgentEndpointGrants {
777                call: true,
778                agent_name: "a".into(),
779                ..Default::default()
780            })
781            .with_agent_thread_grants(AgentThreadGrants {
782                scope: "s".into(),
783                thread_ids: vec!["t".into()],
784                list: true,
785                read: true,
786                sql: true,
787                ..Default::default()
788            })
789            .with_agent_database_grants(AgentDatabaseGrants {
790                database_ids: vec!["DB_a".into()],
791                read: true,
792                ..Default::default()
793            })
794            .to_jwt()
795            .unwrap();
796        let p =
797            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
798        assert_eq!(p["agent"], serde_json::json!({"dispatchAdmin": true}));
799        assert_eq!(p["agentEndpoint"], serde_json::json!({"call": true, "agentName": "a"}));
800        assert_eq!(
801            p["agentThread"],
802            serde_json::json!({
803                "scope": "s", "threadIds": ["t"], "list": true, "read": true, "sql": true
804            })
805        );
806        assert_eq!(p["agentDatabase"], serde_json::json!({"databaseIds": ["DB_a"], "read": true}));
807        let claims = Claims::from_unverified(&token).unwrap();
808        assert!(claims.agent_thread.can_list("s") && claims.agent_thread.can_read("t"));
809        assert!(!claims.agent_thread.can_read("u") && !claims.agent_thread.can_create("s"));
810        assert!(claims.agent_thread.can_sql("t") && !claims.agent_thread.can_sql("u"));
811        assert!(claims.agent_database.can_read("DB_a") && !claims.agent_database.can_write("DB_a"));
812        assert!(!claims.agent_database.can_read("DB_b") && !claims.agent_database.can_read(""));
813        assert!(claims.agent_endpoint.allows("a", "default"));
814
815        let db = AgentDatabaseGrants {
816            database_ids: vec!["DB_a".into()],
817            delete: true,
818            create: true,
819            list: true,
820            ..Default::default()
821        };
822        assert_eq!(
823            serde_json::to_value(&db).unwrap(),
824            serde_json::json!({
825                "databaseIds": ["DB_a"], "delete": true, "create": true, "list": true
826            })
827        );
828        assert!(db.can_delete("DB_a") && !db.can_delete("DB_b") && !db.can_read("DB_a"));
829        assert!(db.can_create() && db.can_list());
830        assert!(!AgentDatabaseGrants::default().can_create());
831        let all = AgentThreadGrants { all: true, sql: true, ..Default::default() };
832        assert!(all.can_sql("AT_t") && !all.can_sql(""));
833    }
834
835    #[test]
836    fn test_inference_grants_claim_name() {
837        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
838            .with_inference_grants(InferenceGrants::default().with_perform())
839            .to_jwt()
840            .unwrap();
841        let p =
842            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
843        assert_eq!(p["inference"], serde_json::json!({"perform": true}));
844        assert!(Claims::from_unverified(&token).unwrap().inference.perform);
845    }
846
847    #[test]
848    fn test_observability_grants_claim_name() {
849        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
850            .with_observability_grants(ObservabilityGrants::default().with_write())
851            .to_jwt()
852            .unwrap();
853        let p =
854            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
855        assert_eq!(p["observability"], serde_json::json!({"write": true}));
856        assert!(Claims::from_unverified(&token).unwrap().observability.write);
857    }
858}