From aeab982ea916aaff1df43a75b01602ac80b27d3e Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:38:49 +0000 Subject: [PATCH] fix(server-sdk): keep canPublishSources intact when encoding a token --- .changeset/access-token-reencode.md | 5 +++++ .../src/AccessToken.test.ts | 21 +++++++++++++++++++ .../livekit-server-sdk/src/grants.test.ts | 11 ++++++++++ packages/livekit-server-sdk/src/grants.ts | 6 +++++- 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/access-token-reencode.md diff --git a/.changeset/access-token-reencode.md b/.changeset/access-token-reencode.md new file mode 100644 index 00000000..19d9da02 --- /dev/null +++ b/.changeset/access-token-reencode.md @@ -0,0 +1,5 @@ +--- +'livekit-server-sdk': patch +--- + +Fix `AccessToken.toJwt()` throwing on its second call when the token has `canPublishSources`. Converting the grants to JWT claims no longer replaces the token's `TrackSource` values with strings. diff --git a/packages/livekit-server-sdk/src/AccessToken.test.ts b/packages/livekit-server-sdk/src/AccessToken.test.ts index d80a8029..6db2f474 100644 --- a/packages/livekit-server-sdk/src/AccessToken.test.ts +++ b/packages/livekit-server-sdk/src/AccessToken.test.ts @@ -6,6 +6,7 @@ import { RoomCompositeEgressRequest, RoomConfiguration, RoomEgress, + TrackSource, } from '@livekit/protocol'; import * as jose from 'jose'; import { describe, expect, it } from 'vitest'; @@ -161,3 +162,23 @@ describe('room configuration with agents and egress', () => { expect(decoded.roomConfig?.egress?.room?.roomName).toEqual('test-room'); }); }); + +describe('a token can be encoded more than once', () => { + it('keeps canPublishSources when toJwt is called twice', async () => { + const t = new AccessToken(testApiKey, testSecret, { identity: 'me' }); + t.addGrant({ + roomJoin: true, + room: 'myroom', + canPublishSources: [TrackSource.CAMERA, TrackSource.MICROPHONE], + }); + + const first = await t.toJwt(); + const second = await t.toJwt(); + + const v = new TokenVerifier(testApiKey, testSecret); + for (const jwt of [first, second]) { + const decoded = await v.verify(jwt); + expect(decoded.video?.canPublishSources).toEqual(['camera', 'microphone']); + } + }); +}); diff --git a/packages/livekit-server-sdk/src/grants.test.ts b/packages/livekit-server-sdk/src/grants.test.ts index 1bece31f..5f26ee1a 100644 --- a/packages/livekit-server-sdk/src/grants.test.ts +++ b/packages/livekit-server-sdk/src/grants.test.ts @@ -40,4 +40,15 @@ describe('ClaimGrants are parsed correctly', () => { expect(jwtPayload.observability).toBeTypeOf('object'); expect((jwtPayload.observability as ObservabilityGrant)?.write).toBe(true); }); + + it('does not modify the grants it converts', () => { + const claim: ClaimGrants = { + video: { canPublishSources: [TrackSource.CAMERA, TrackSource.MICROPHONE] }, + }; + + claimsToJwtPayload(claim); + + expect(claim.video?.canPublishSources).toEqual([TrackSource.CAMERA, TrackSource.MICROPHONE]); + expect(() => claimsToJwtPayload(claim)).not.toThrow(); + }); }); diff --git a/packages/livekit-server-sdk/src/grants.ts b/packages/livekit-server-sdk/src/grants.ts index 45e80063..dd06e031 100644 --- a/packages/livekit-server-sdk/src/grants.ts +++ b/packages/livekit-server-sdk/src/grants.ts @@ -27,7 +27,11 @@ export function claimsToJwtPayload( const claim: Record = { ...grant }; // eslint-disable-next-line no-restricted-syntax if (Array.isArray(claim.video?.canPublishSources)) { - claim.video.canPublishSources = claim.video.canPublishSources.map(trackSourceToString); + // copy the video grant so the caller's grants keep their TrackSource enum values + claim.video = { + ...claim.video, + canPublishSources: claim.video.canPublishSources.map(trackSourceToString), + }; } return claim; }