1use 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); #[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 #[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 #[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 #[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 #[serde(skip_serializing_if = "is_default")]
75 pub can_publish_sources: Vec<String>, #[serde(skip_serializing_if = "is_default")]
79 pub can_update_own_metadata: Option<bool>,
80
81 #[serde(skip_serializing_if = "is_default")]
83 pub ingress_admin: bool, #[serde(skip_serializing_if = "is_default")]
87 pub hidden: bool,
88
89 #[serde(skip_serializing_if = "is_default")]
91 pub recorder: bool,
92
93 #[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 #[serde(default, skip_serializing_if = "is_default")]
125 pub admin: bool,
126 #[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 #[serde(skip_serializing_if = "is_default")]
142 pub admin: bool,
143 #[serde(skip_serializing_if = "is_default")]
145 pub simulation_admin: bool,
146 #[serde(skip_serializing_if = "is_default")]
148 pub database_admin: bool,
149 #[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 #[serde(skip_serializing_if = "is_default")]
159 pub call: bool,
160 #[serde(skip_serializing_if = "is_default")]
162 pub agent_name: String,
163 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
233#[serde(default, rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct InferenceGrants {
236 #[serde(skip_serializing_if = "is_default")]
238 pub perform: bool,
239}
240
241impl InferenceGrants {
242 pub fn with_perform(mut self) -> Self {
244 self.perform = true;
245 self
246 }
247}
248
249#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
251#[serde(default, rename_all = "camelCase")]
252#[non_exhaustive]
253pub struct ObservabilityGrants {
254 #[serde(skip_serializing_if = "is_default")]
256 pub write: bool,
257}
258
259impl ObservabilityGrants {
260 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, pub iss: String, pub nbf: usize,
274 #[serde(skip_serializing_if = "is_default")]
275 pub sub: String, #[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, #[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 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 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 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 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}