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    // access the project's agent databases
147    #[serde(skip_serializing_if = "is_default")]
148    pub database_admin: bool,
149    // manage the project's agent dispatch queue
150    #[serde(skip_serializing_if = "is_default")]
151    pub dispatch_admin: bool,
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
155#[serde(default, rename_all = "camelCase")]
156pub struct AgentEndpointGrants {
157    // invoke an agent's non-public HTTP endpoints
158    #[serde(skip_serializing_if = "is_default")]
159    pub call: bool,
160    // empty grants every agent in the project
161    #[serde(skip_serializing_if = "is_default")]
162    pub agent_name: String,
163    // empty grants every deployment
164    #[serde(skip_serializing_if = "is_default")]
165    pub deployment: String,
166}
167
168impl AgentEndpointGrants {
169    pub fn allows(&self, agent_name: &str, deployment: &str) -> bool {
170        self.call
171            && (self.agent_name.is_empty() || self.agent_name == agent_name)
172            && (self.deployment.is_empty() || self.deployment == deployment)
173    }
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
177#[serde(default, rename_all = "camelCase")]
178pub struct AgentThreadGrants {
179    #[serde(skip_serializing_if = "is_default")]
180    pub all: bool,
181    #[serde(skip_serializing_if = "is_default")]
182    pub scope: String,
183    #[serde(skip_serializing_if = "is_default")]
184    pub thread_ids: Vec<String>,
185    #[serde(skip_serializing_if = "is_default")]
186    pub list: bool,
187    #[serde(skip_serializing_if = "is_default")]
188    pub create: bool,
189    #[serde(skip_serializing_if = "is_default")]
190    pub write: bool,
191    #[serde(skip_serializing_if = "is_default")]
192    pub read: bool,
193    #[serde(skip_serializing_if = "is_default")]
194    pub delete: 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    fn in_scope(&self, scope: &str) -> bool {
223        self.all || (!self.scope.is_empty() && self.scope == scope)
224    }
225
226    fn includes(&self, thread_id: &str) -> bool {
227        !thread_id.is_empty() && (self.all || self.thread_ids.iter().any(|id| id == thread_id))
228    }
229}
230
231/// Grants for LiveKit Inference, set with [`AccessToken::with_inference_grants`].
232#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
233#[serde(default, rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct InferenceGrants {
236    /// Use LiveKit Inference (LLM, STT, TTS).
237    #[serde(skip_serializing_if = "is_default")]
238    pub perform: bool,
239}
240
241impl InferenceGrants {
242    /// Allows using LiveKit Inference (LLM, STT, TTS).
243    pub fn with_perform(mut self) -> Self {
244        self.perform = true;
245        self
246    }
247}
248
249/// Grants for LiveKit observability, set with [`AccessToken::with_observability_grants`].
250#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
251#[serde(default, rename_all = "camelCase")]
252#[non_exhaustive]
253pub struct ObservabilityGrants {
254    /// Publish observability data.
255    #[serde(skip_serializing_if = "is_default")]
256    pub write: bool,
257}
258
259impl ObservabilityGrants {
260    /// Allows publishing observability data.
261    pub fn with_write(mut self) -> Self {
262        self.write = true;
263        self
264    }
265}
266
267#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
268#[serde(default)]
269#[serde(rename_all = "camelCase")]
270pub struct Claims {
271    pub exp: usize,  // Expiration
272    pub iss: String, // ApiKey
273    pub nbf: usize,
274    #[serde(skip_serializing_if = "is_default")]
275    pub sub: String, // Identity
276
277    #[serde(skip_serializing_if = "is_default")]
278    pub name: String,
279    #[serde(skip_serializing_if = "is_default")]
280    pub kind: String,
281    #[serde(skip_serializing_if = "is_default")]
282    pub video: VideoGrants,
283    #[serde(skip_serializing_if = "is_default")]
284    pub sip: SIPGrants,
285    #[serde(skip_serializing_if = "is_default")]
286    pub agent: AgentGrants,
287    #[serde(skip_serializing_if = "is_default")]
288    pub agent_endpoint: AgentEndpointGrants,
289    #[serde(skip_serializing_if = "is_default")]
290    pub agent_thread: AgentThreadGrants,
291    #[serde(skip_serializing_if = "is_default")]
292    pub inference: InferenceGrants,
293    #[serde(skip_serializing_if = "is_default")]
294    pub observability: ObservabilityGrants,
295    #[serde(skip_serializing_if = "is_default")]
296    pub sha256: String, // Used to verify the integrity of the message body
297    #[serde(skip_serializing_if = "is_default")]
298    pub metadata: String,
299    #[serde(skip_serializing_if = "is_default")]
300    pub attributes: HashMap<String, String>,
301    #[serde(skip_serializing_if = "is_default")]
302    pub room_config: Option<livekit_protocol::RoomConfiguration>,
303}
304
305impl Claims {
306    pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
307        crate::jwt_provider::ensure_installed();
308        let token = jsonwebtoken::dangerous::insecure_decode::<Claims>(token)?;
309        Ok(token.claims)
310    }
311}
312
313#[derive(Clone)]
314pub struct AccessToken {
315    api_key: String,
316    api_secret: String,
317    claims: Claims,
318}
319
320impl Debug for AccessToken {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        // Don't show api_secret here
323        f.debug_struct("AccessToken")
324            .field("api_key", &self.api_key)
325            .field("claims", &self.claims)
326            .finish()
327    }
328}
329
330impl AccessToken {
331    pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
332        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
333        Self {
334            api_key: api_key.to_owned(),
335            api_secret: api_secret.to_owned(),
336            claims: Claims {
337                exp: now.add(DEFAULT_TTL).as_secs() as usize,
338                iss: api_key.to_owned(),
339                nbf: now.as_secs() as usize,
340                sub: Default::default(),
341                name: Default::default(),
342                kind: Default::default(),
343                video: VideoGrants::default(),
344                sip: SIPGrants::default(),
345                agent: AgentGrants::default(),
346                agent_endpoint: AgentEndpointGrants::default(),
347                agent_thread: AgentThreadGrants::default(),
348                inference: InferenceGrants::default(),
349                observability: ObservabilityGrants::default(),
350                sha256: Default::default(),
351                metadata: Default::default(),
352                attributes: HashMap::new(),
353                room_config: Default::default(),
354            },
355        }
356    }
357
358    #[cfg(test)]
359    pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
360        Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
361    }
362
363    pub fn new() -> Result<Self, AccessTokenError> {
364        // Try to get the API Key and the Secret Key from the environment
365        let (api_key, api_secret) = get_env_keys()?;
366        Ok(Self::with_api_key(&api_key, &api_secret))
367    }
368
369    pub fn with_ttl(mut self, ttl: Duration) -> Self {
370        let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
371        self.claims.exp = time.as_secs() as usize;
372        self
373    }
374
375    pub fn with_grants(mut self, grants: VideoGrants) -> Self {
376        self.claims.video = grants;
377        self
378    }
379
380    pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
381        self.claims.sip = grants;
382        self
383    }
384
385    pub fn with_agent_grants(mut self, grants: AgentGrants) -> Self {
386        self.claims.agent = grants;
387        self
388    }
389
390    pub fn with_agent_endpoint_grants(mut self, grants: AgentEndpointGrants) -> Self {
391        self.claims.agent_endpoint = grants;
392        self
393    }
394
395    pub fn with_agent_thread_grants(mut self, grants: AgentThreadGrants) -> Self {
396        self.claims.agent_thread = grants;
397        self
398    }
399
400    pub fn with_inference_grants(mut self, grants: InferenceGrants) -> Self {
401        self.claims.inference = grants;
402        self
403    }
404
405    /// Sets the observability grants for this token.
406    pub fn with_observability_grants(mut self, grants: ObservabilityGrants) -> Self {
407        self.claims.observability = grants;
408        self
409    }
410
411    pub fn with_identity(mut self, identity: &str) -> Self {
412        self.claims.sub = identity.to_owned();
413        self
414    }
415
416    pub fn with_name(mut self, name: &str) -> Self {
417        self.claims.name = name.to_owned();
418        self
419    }
420
421    pub fn with_kind(mut self, kind: &str) -> Self {
422        self.claims.kind = kind.to_owned();
423        self
424    }
425
426    pub fn with_metadata(mut self, metadata: &str) -> Self {
427        self.claims.metadata = metadata.to_owned();
428        self
429    }
430
431    pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
432    where
433        I: IntoIterator<Item = (K, V)>,
434        K: Into<String>,
435        V: Into<String>,
436    {
437        self.claims.attributes =
438            attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
439        self
440    }
441
442    pub fn with_sha256(mut self, sha256: &str) -> Self {
443        self.claims.sha256 = sha256.to_owned();
444        self
445    }
446
447    pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
448        self.claims.room_config = Some(config);
449        self
450    }
451
452    pub fn to_jwt(self) -> Result<String, AccessTokenError> {
453        crate::jwt_provider::ensure_installed();
454        if self.api_key.is_empty() || self.api_secret.is_empty() {
455            return Err(AccessTokenError::InvalidKeys);
456        }
457
458        if self.claims.video.room_join
459            && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
460        {
461            return Err(AccessTokenError::InvalidClaims(
462                "token grants room_join but doesn't have an identity or room",
463            ));
464        }
465
466        Ok(jsonwebtoken::encode(
467            &Header::new(jsonwebtoken::Algorithm::HS256),
468            &self.claims,
469            &EncodingKey::from_secret(self.api_secret.as_ref()),
470        )?)
471    }
472}
473
474#[derive(Clone)]
475pub struct TokenVerifier {
476    api_key: String,
477    api_secret: String,
478}
479
480impl Debug for TokenVerifier {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
483    }
484}
485
486impl TokenVerifier {
487    pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
488        Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
489    }
490
491    pub fn new() -> Result<Self, AccessTokenError> {
492        let (api_key, api_secret) = get_env_keys()?;
493        Ok(Self::with_api_key(&api_key, &api_secret))
494    }
495
496    pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
497        crate::jwt_provider::ensure_installed();
498        let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
499        validation.validate_exp = true;
500        validation.validate_nbf = true;
501        validation.set_issuer(&[&self.api_key]);
502
503        let token = jsonwebtoken::decode::<Claims>(
504            token,
505            &DecodingKey::from_secret(self.api_secret.as_ref()),
506            &validation,
507        )?;
508
509        Ok(token.claims)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use std::time::Duration;
516
517    use super::{
518        AccessToken, AgentEndpointGrants, AgentGrants, AgentThreadGrants, Claims, InferenceGrants,
519        ObservabilityGrants, SIPGrants, TokenVerifier, VideoGrants,
520    };
521
522    const TEST_API_KEY: &str = "myapikey";
523    const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
524    const TEST_TOKEN: &str = include_str!("test_token.txt");
525
526    #[test]
527    fn test_access_token() {
528        let room_config = livekit_protocol::RoomConfiguration {
529            name: "name".to_string(),
530            agents: vec![livekit_protocol::RoomAgentDispatch {
531                agent_name: "test-agent".to_string(),
532                metadata: "test-metadata".to_string(),
533                ..Default::default()
534            }],
535            ..Default::default()
536        };
537
538        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
539            .with_ttl(Duration::from_secs(60))
540            .with_identity("test")
541            .with_name("test")
542            .with_grants(VideoGrants::default())
543            .with_room_config(room_config.clone())
544            .to_jwt()
545            .unwrap();
546
547        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
548        let claims = verifier.verify(&token).unwrap();
549
550        assert_eq!(claims.sub, "test");
551        assert_eq!(claims.name, "test");
552        assert_eq!(claims.iss, TEST_API_KEY);
553        assert_eq!(claims.room_config, Some(room_config));
554
555        let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
556        assert!(incorrect_issuer.verify(&token).is_err());
557
558        let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
559        assert!(incorrect_token.verify(&token).is_err());
560    }
561
562    #[test]
563    fn test_verify_token_with_room_config() {
564        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
565        // This token was generated using the Python SDK.
566        let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
567
568        assert_eq!(
569            super::Claims {
570                sub: "identity".to_string(),
571                name: "name".to_string(),
572                room_config: Some(livekit_protocol::RoomConfiguration {
573                    agents: vec![livekit_protocol::RoomAgentDispatch {
574                        agent_name: "test-agent".to_string(),
575                        metadata: "test-metadata".to_string(),
576                        ..Default::default()
577                    }],
578                    ..Default::default()
579                }),
580                ..claims.clone()
581            },
582            claims
583        );
584    }
585
586    #[test]
587    fn test_unverified_token() {
588        let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
589
590        assert_eq!(claims.sub, "identity");
591        assert_eq!(claims.name, "name");
592        assert_eq!(claims.iss, TEST_API_KEY);
593        assert_eq!(
594            claims.room_config,
595            Some(livekit_protocol::RoomConfiguration {
596                agents: vec![livekit_protocol::RoomAgentDispatch {
597                    agent_name: "test-agent".to_string(),
598                    metadata: "test-metadata".to_string(),
599                    ..Default::default()
600                }],
601                ..Default::default()
602            })
603        );
604
605        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
606            .with_ttl(Duration::from_secs(60))
607            .with_identity("test")
608            .with_name("test")
609            .with_grants(VideoGrants {
610                room_join: true,
611                room: "test-room".to_string(),
612                ..Default::default()
613            })
614            .to_jwt()
615            .unwrap();
616
617        let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
618        assert_eq!(claims.sub, "test");
619        assert_eq!(claims.name, "test");
620        assert_eq!(claims.video.room, "test-room");
621        assert!(claims.video.room_join);
622
623        let parts: Vec<&str> = token.split('.').collect();
624        let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
625
626        let claims = Claims::from_unverified(&malformed_token)
627            .expect("Failed to parse token with wrong signature");
628        assert_eq!(claims.sub, "test");
629        assert_eq!(claims.name, "test");
630    }
631
632    #[test]
633    fn test_agent_grant_and_kind() {
634        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
635            .with_ttl(Duration::from_secs(60))
636            .with_identity("agent-1")
637            .with_kind("agent")
638            .with_grants(VideoGrants {
639                room_join: true,
640                room: "test-room".to_string(),
641                agent: true,
642                ..Default::default()
643            })
644            .to_jwt()
645            .expect("Failed to create token");
646
647        let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
648        let claims = verifier.verify(&token).expect("Failed to verify token.");
649        assert_eq!(claims.kind, "agent");
650        assert!(claims.video.agent);
651
652        let payload = |token: &str| {
653            let _ = Claims::from_unverified(token).expect("Failed to parse token");
654            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(token)
655                .expect("Failed to decode token")
656                .claims
657        };
658        let bare = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
659            .with_ttl(Duration::from_secs(60))
660            .with_grants(VideoGrants::default())
661            .to_jwt()
662            .expect("Failed to create token");
663        let p = payload(&bare);
664        let mut keys: Vec<&str> = p.as_object().unwrap().keys().map(String::as_str).collect();
665        keys.sort_unstable();
666        assert_eq!(keys, ["exp", "iss", "nbf"], "{p}");
667
668        let agent = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
669            .with_ttl(Duration::from_secs(60))
670            .with_grants(VideoGrants {
671                agent: true,
672                can_publish: Some(false),
673                ..Default::default()
674            })
675            .to_jwt()
676            .expect("Failed to create token");
677        let p = payload(&agent);
678        assert_eq!(p["video"], serde_json::json!({"agent": true, "canPublish": false}), "{p}");
679        let claims = Claims::from_unverified(&agent).expect("Failed to parse token");
680        assert!(claims.video.agent && !claims.video.can_publish() && claims.video.can_subscribe());
681        assert!(!claims.video.can_publish_data(), "absent canPublishData follows canPublish");
682    }
683
684    #[test]
685    fn test_defaults_are_not_serialized() {
686        assert_eq!(serde_json::to_string(&VideoGrants::default()).unwrap(), "{}");
687        let parsed: VideoGrants = serde_json::from_str("{}").unwrap();
688        assert_eq!(parsed, VideoGrants::default());
689        assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
690        let explicit = VideoGrants {
691            can_publish: Some(false),
692            can_publish_data: Some(true),
693            ..Default::default()
694        };
695        assert_eq!(
696            serde_json::to_string(&explicit).unwrap(),
697            r#"{"canPublish":false,"canPublishData":true}"#
698        );
699        assert!(explicit.can_publish_data());
700
701        assert_eq!(serde_json::to_string(&SIPGrants::default()).unwrap(), "{}");
702        let parsed: SIPGrants = serde_json::from_str("{}").unwrap();
703        assert_eq!(parsed, SIPGrants::default());
704        assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
705
706        let claims = Claims { exp: 1, iss: "k".to_string(), nbf: 0, ..Default::default() };
707        let json = r#"{"exp":1,"iss":"k","nbf":0}"#;
708        assert_eq!(serde_json::to_string(&claims).unwrap(), json);
709        let parsed: Claims = serde_json::from_str(json).unwrap();
710        assert_eq!(parsed, claims);
711        assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
712    }
713
714    #[test]
715    fn test_agent_grants_claim_names() {
716        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
717            .with_agent_grants(AgentGrants { database_admin: true, ..Default::default() })
718            .with_agent_endpoint_grants(AgentEndpointGrants {
719                call: true,
720                agent_name: "a".into(),
721                ..Default::default()
722            })
723            .with_agent_thread_grants(AgentThreadGrants {
724                scope: "s".into(),
725                thread_ids: vec!["t".into()],
726                list: true,
727                read: true,
728                ..Default::default()
729            })
730            .to_jwt()
731            .unwrap();
732        let p =
733            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
734        assert_eq!(p["agent"], serde_json::json!({"databaseAdmin": true}));
735        assert_eq!(p["agentEndpoint"], serde_json::json!({"call": true, "agentName": "a"}));
736        assert_eq!(
737            p["agentThread"],
738            serde_json::json!({"scope": "s", "threadIds": ["t"], "list": true, "read": true})
739        );
740        let claims = Claims::from_unverified(&token).unwrap();
741        assert!(claims.agent_thread.can_list("s") && claims.agent_thread.can_read("t"));
742        assert!(!claims.agent_thread.can_read("u") && !claims.agent_thread.can_create("s"));
743        assert!(claims.agent_endpoint.allows("a", "default"));
744    }
745
746    #[test]
747    fn test_inference_grants_claim_name() {
748        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
749            .with_inference_grants(InferenceGrants::default().with_perform())
750            .to_jwt()
751            .unwrap();
752        let p =
753            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
754        assert_eq!(p["inference"], serde_json::json!({"perform": true}));
755        assert!(Claims::from_unverified(&token).unwrap().inference.perform);
756    }
757
758    #[test]
759    fn test_observability_grants_claim_name() {
760        let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
761            .with_observability_grants(ObservabilityGrants::default().with_write())
762            .to_jwt()
763            .unwrap();
764        let p =
765            jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
766        assert_eq!(p["observability"], serde_json::json!({"write": true}));
767        assert!(Claims::from_unverified(&token).unwrap().observability.write);
768    }
769}