Skip to content

Commit 4d0e83c

Browse files
authored
Generate multiple tokens with one signPlaybackId call (#499)
* Generate multiple tokens with one `signPlaybackId` call * Add missing DataTypeClaim * wait, I thought of a better signature * Revert "wait, I thought of a better signature" This reverts commit 5fdefba.
1 parent 5e91cba commit 4d0e83c

File tree

5 files changed

+295
-14
lines changed

5 files changed

+295
-14
lines changed

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,57 @@ const statsToken = mux.jwt.signViewerCounts('some-live-stream-id', {
106106
// https://stats.mux.com/counts?token={statsToken}
107107
```
108108

109+
### Signing multiple JWTs at once
110+
In cases you need multiple tokens, like when using Mux Player, things can get unwieldy pretty quickly. For example,
111+
```tsx
112+
const playbackToken = await mux.jwt.signPlaybackId(id, {
113+
expiration: "1d",
114+
type: "playback"
115+
})
116+
const thumbnailToken = await mux.jwt.signPlaybackId(id, {
117+
expiration: "1d",
118+
type: "thumbnail",
119+
})
120+
const storyboardToken = await mux.jwt.signPlaybackId(id, {
121+
expiration: "1d",
122+
type: "storyboard"
123+
})
124+
const drmToken = await mux.jwt.signPlaybackId(id, {
125+
expiration: "1d",
126+
type: "drm_license"
127+
})
128+
129+
<mux-player
130+
playback-token={playbackToken}
131+
thumbanil-token={thumbnailToken}
132+
storyboard-token={storyboardToken}
133+
drm-token={drmToken}
134+
playbackId={id}
135+
></mux-player>
136+
```
137+
138+
To simplify this use-case, you can provide multiple types to `signPlaybackId` to recieve multiple tokens. These tokens are provided in a format that Mux Player can take as props:
139+
```tsx
140+
// { "playback-token", "thumbnail-token", "storyboard-token", "drm-token" }
141+
const tokens = await mux.jwt.signPlaybackId(id, {
142+
expiration: "1d",
143+
type: ["playback", "thumbnail", "storyboard", "drm_license"]
144+
})
145+
146+
<mux-player
147+
{...tokens}
148+
playbackId={id}
149+
></mux-player>
150+
```
151+
152+
If you would like to provide params to a single token (e.g., if you would like to have a thumbnail `time`), you can provide `[type, typeParams]` instead of `type`:
153+
```tsx
154+
const tokens = await mux.jwt.signPlaybackId(id, {
155+
expiration: "1d",
156+
type: ["playback", ["thumbnail", { time: 2 }], "storyboard", "drm_license"]
157+
})
158+
```
159+
109160
## Parsing Webhook payloads
110161

111162
To validate that the given payload was sent by Mux and parse the webhook payload for use in your application,

deno_tests/api-resources/jwt.test.ts

+75-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless.
22

33
import Mux from '../../deno/mod.ts';
4-
import { TypeClaim, DataTypeClaim } from '../../deno/util/jwt-types.ts';
4+
import { TypeClaim, DataTypeClaim, TypeToken } from '../../deno/util/jwt-types.ts';
55
import { jwtVerify, importJWK, importPKCS8 } from 'https://deno.land/x/[email protected]/index.ts';
66
import { assertObjectMatch } from 'https://deno.land/[email protected]/testing/asserts.ts';
77
import { publicJwk, privatePkcs1, privatePkcs8 } from '../../tests/api-resources/rsaKeys.ts';
@@ -199,3 +199,77 @@ Deno.test(async function signViewerCounts() {
199199
},
200200
);
201201
});
202+
203+
Deno.test('signPlaybackId multiple types returns same tokens as multiple single calls', async () => {
204+
const id = 'abcdefgh';
205+
const expiration = '1d';
206+
207+
const playbackToken = await mux.jwt.signPlaybackId(id, {
208+
expiration,
209+
type: 'video',
210+
});
211+
const thumbnailToken = await mux.jwt.signPlaybackId(id, {
212+
expiration,
213+
type: 'thumbnail',
214+
});
215+
const storyboardToken = await mux.jwt.signPlaybackId(id, {
216+
expiration,
217+
type: 'storyboard',
218+
});
219+
const drmToken = await mux.jwt.signPlaybackId(id, {
220+
expiration,
221+
type: 'drm_license',
222+
});
223+
const gifToken = await mux.jwt.signPlaybackId(id, {
224+
expiration,
225+
type: 'gif',
226+
});
227+
const statsToken = await mux.jwt.signPlaybackId(id, {
228+
expiration,
229+
type: 'stats',
230+
});
231+
232+
const tokens = await mux.jwt.signPlaybackId(id, {
233+
expiration,
234+
type: ['video', 'thumbnail', 'storyboard', 'drm_license', 'gif', 'stats'],
235+
});
236+
237+
assertEquals(tokens[TypeToken.video], playbackToken);
238+
assertEquals(tokens[TypeToken.thumbnail], thumbnailToken);
239+
assertEquals(tokens[TypeToken.storyboard], storyboardToken);
240+
assertEquals(tokens[TypeToken.drm_license], drmToken);
241+
assertEquals(tokens[TypeToken.gif], gifToken);
242+
assertEquals(tokens[TypeToken.stats], statsToken);
243+
});
244+
245+
Deno.test(
246+
'signPlaybackId multiple types with params returns same tokens as multiple single calls',
247+
async () => {
248+
const id = 'abcdefgh';
249+
const expiration = '1d';
250+
251+
const playbackToken = await mux.jwt.signPlaybackId(id, {
252+
expiration,
253+
type: 'video',
254+
});
255+
const thumbnailParams = { time: '2' };
256+
const thumbnailToken = await mux.jwt.signPlaybackId(id, {
257+
expiration,
258+
type: 'thumbnail',
259+
params: thumbnailParams,
260+
});
261+
const storyboardToken = await mux.jwt.signPlaybackId(id, {
262+
expiration,
263+
type: 'storyboard',
264+
});
265+
266+
const tokens = await mux.jwt.signPlaybackId(id, {
267+
expiration,
268+
type: ['video', ['thumbnail', thumbnailParams], 'storyboard'],
269+
});
270+
271+
assertEquals(tokens[TypeToken.video], playbackToken);
272+
assertEquals(tokens[TypeToken.thumbnail], thumbnailToken);
273+
assertEquals(tokens[TypeToken.storyboard], storyboardToken);
274+
},
275+
);

src/resources/jwt.ts

+67-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,45 @@ import { APIResource } from '@mux/mux-node/resource';
44
import * as jwt from '@mux/mux-node/_shims/auto/jwt';
55
import {
66
type SignOptions,
7-
type MuxJWTSignOptions,
87
TypeClaim,
98
DataTypeClaim,
9+
TypeToken,
10+
type MuxJWTSignOptions,
11+
type MuxJWTSignOptionsMultiple,
12+
type Tokens,
13+
isMuxJWTSignOptionsMultiple,
1014
} from '@mux/mux-node/util/jwt-types';
1115

1216
export class Jwt extends APIResource {
17+
async signPlaybackId(
18+
playbackId: string,
19+
config: MuxJWTSignOptions<keyof typeof TypeClaim>,
20+
): Promise<string>;
21+
22+
async signPlaybackId(
23+
playbackId: string,
24+
config: MuxJWTSignOptionsMultiple<keyof typeof TypeClaim>,
25+
): Promise<Tokens>;
26+
1327
/**
14-
* Creates a new token to be used with a signed Playback ID
28+
* Creates a new token or tokens to be used with a signed Playback ID
1529
*/
1630
async signPlaybackId(
1731
playbackId: string,
18-
config: MuxJWTSignOptions<keyof typeof TypeClaim> = {},
32+
config:
33+
| MuxJWTSignOptions<keyof typeof TypeClaim>
34+
| MuxJWTSignOptionsMultiple<keyof typeof TypeClaim> = {},
35+
): Promise<string | Tokens> {
36+
if (isMuxJWTSignOptionsMultiple(config)) {
37+
return this.signPlaybackIdMultipleTypes(playbackId, config);
38+
} else {
39+
return this.signPlaybackIdSingleType(playbackId, config);
40+
}
41+
}
42+
43+
private async signPlaybackIdSingleType(
44+
playbackId: string,
45+
config: MuxJWTSignOptions<keyof typeof TypeClaim>,
1946
): Promise<string> {
2047
const claim = TypeClaim[config.type ?? 'video'];
2148
if (!claim) {
@@ -34,6 +61,43 @@ export class Jwt extends APIResource {
3461
return jwt.sign(config.params ?? {}, await jwt.getPrivateKey(this._client, config), tokenOptions);
3562
}
3663

64+
private async signPlaybackIdMultipleTypes(
65+
playbackId: string,
66+
config: MuxJWTSignOptionsMultiple<keyof typeof TypeClaim>,
67+
): Promise<Tokens> {
68+
const tokens: Tokens = {};
69+
70+
for (const typeOption of config.type) {
71+
let type: keyof typeof TypeClaim;
72+
let params: Record<string, string> | undefined;
73+
74+
if (Array.isArray(typeOption)) {
75+
[type, params] = typeOption;
76+
} else {
77+
type = typeOption;
78+
params = undefined;
79+
}
80+
81+
const singleConfig = {
82+
...config,
83+
type,
84+
params: {
85+
...config.params,
86+
...params,
87+
},
88+
};
89+
90+
const token = await this.signPlaybackIdSingleType(playbackId, singleConfig);
91+
92+
const tokenKey = TypeToken[type];
93+
if (tokenKey) {
94+
tokens[tokenKey] = token;
95+
}
96+
}
97+
98+
return tokens;
99+
}
100+
37101
/**
38102
* Creates a new token for a license for playing back DRM'd video content
39103
*/

src/util/jwt-types.ts

+30-9
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,6 @@ export interface JwtHeader {
3434
x5c?: string | string[] | undefined;
3535
}
3636

37-
export interface MuxJWTSignOptions<Type extends string = string> {
38-
keyId?: string;
39-
keySecret?: string | PrivateKey;
40-
keyFilePath?: string;
41-
type?: Type;
42-
expiration?: string;
43-
params?: Record<string, string>;
44-
}
45-
4637
export enum TypeClaim {
4738
video = 'v',
4839
thumbnail = 't',
@@ -51,6 +42,36 @@ export enum TypeClaim {
5142
stats = 'playback_id',
5243
drm_license = 'd',
5344
}
45+
export enum TypeToken {
46+
video = 'playback-token',
47+
thumbnail = 'thumbnail-token',
48+
storyboard = 'storyboard-token',
49+
drm_license = 'drm-token',
50+
gif = 'gif-token', // Not supported by Mux Player
51+
stats = 'stats-token', // Not supported by Mux Player
52+
}
53+
export type TypeTokenValues = (typeof TypeToken)[keyof typeof TypeToken];
54+
export type Tokens = Partial<Record<TypeTokenValues, string>>;
55+
// ['thumbnail', { time: 2 }]
56+
export type TypeWithParams<Type extends string = string> = [Type, MuxJWTSignOptions<Type>['params']];
57+
58+
interface MuxJWTSignOptionsBase<Type extends string = string> {
59+
keyId?: string;
60+
keySecret?: string | PrivateKey;
61+
keyFilePath?: string;
62+
type?: Type | Array<Type | TypeWithParams<Type>>;
63+
expiration?: string;
64+
params?: Record<string, string>;
65+
}
66+
export interface MuxJWTSignOptions<Type extends string = string> extends MuxJWTSignOptionsBase<Type> {
67+
type?: Type;
68+
}
69+
export interface MuxJWTSignOptionsMultiple<Type extends string = string> extends MuxJWTSignOptionsBase<Type> {
70+
type: Array<Type | TypeWithParams<Type>>;
71+
}
72+
export const isMuxJWTSignOptionsMultiple = (
73+
config: MuxJWTSignOptions | MuxJWTSignOptionsMultiple,
74+
): config is MuxJWTSignOptionsMultiple => Array.isArray(config.type);
5475

5576
export enum DataTypeClaim {
5677
video = 'video_id',

tests/api-resources/jwt.test.ts

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless.
22

33
import Mux from '@mux/mux-node';
4-
import { TypeClaim, DataTypeClaim } from '@mux/mux-node/util/jwt-types';
4+
import { TypeClaim, DataTypeClaim, TypeToken } from '@mux/mux-node/util/jwt-types';
55
import { decodeJwt, jwtVerify, importJWK, importPKCS8 } from 'jose';
66
import { publicJwk, privatePkcs1, privatePkcs8 } from './rsaKeys';
77
import crypto from 'crypto';
@@ -277,4 +277,75 @@ describe('resource jwt', () => {
277277
sub: 'abcdefgh',
278278
});
279279
});
280+
281+
test('signPlaybackId multiple types returns same tokens as multiple single calls', async () => {
282+
const id = 'abcdefgh';
283+
const expiration = '1d';
284+
285+
const playbackToken = await mux.jwt.signPlaybackId(id, {
286+
expiration,
287+
type: 'video',
288+
});
289+
const thumbnailToken = await mux.jwt.signPlaybackId(id, {
290+
expiration,
291+
type: 'thumbnail',
292+
});
293+
const storyboardToken = await mux.jwt.signPlaybackId(id, {
294+
expiration,
295+
type: 'storyboard',
296+
});
297+
const drmToken = await mux.jwt.signPlaybackId(id, {
298+
expiration,
299+
type: 'drm_license',
300+
});
301+
const gifToken = await mux.jwt.signPlaybackId(id, {
302+
expiration,
303+
type: 'gif',
304+
});
305+
const statsToken = await mux.jwt.signPlaybackId(id, {
306+
expiration,
307+
type: 'stats',
308+
});
309+
310+
const tokens = await mux.jwt.signPlaybackId(id, {
311+
expiration,
312+
type: ['video', 'thumbnail', 'storyboard', 'drm_license', 'gif', 'stats'],
313+
});
314+
315+
expect(tokens[TypeToken.video]).toEqual(playbackToken);
316+
expect(tokens[TypeToken.thumbnail]).toEqual(thumbnailToken);
317+
expect(tokens[TypeToken.storyboard]).toEqual(storyboardToken);
318+
expect(tokens[TypeToken.drm_license]).toEqual(drmToken);
319+
expect(tokens[TypeToken.gif]).toEqual(gifToken);
320+
expect(tokens[TypeToken.stats]).toEqual(statsToken);
321+
});
322+
323+
test('signPlaybackId multiple types with params returns same tokens as multiple single calls', async () => {
324+
const id = 'abcdefgh';
325+
const expiration = '1d';
326+
327+
const playbackToken = await mux.jwt.signPlaybackId(id, {
328+
expiration,
329+
type: 'video',
330+
});
331+
const thumbnailParams = { time: '2' };
332+
const thumbnailToken = await mux.jwt.signPlaybackId(id, {
333+
expiration,
334+
type: 'thumbnail',
335+
params: thumbnailParams,
336+
});
337+
const storyboardToken = await mux.jwt.signPlaybackId(id, {
338+
expiration,
339+
type: 'storyboard',
340+
});
341+
342+
const tokens = await mux.jwt.signPlaybackId(id, {
343+
expiration,
344+
type: ['video', ['thumbnail', thumbnailParams], 'storyboard'],
345+
});
346+
347+
expect(tokens[TypeToken.video]).toEqual(playbackToken);
348+
expect(tokens[TypeToken.thumbnail]).toEqual(thumbnailToken);
349+
expect(tokens[TypeToken.storyboard]).toEqual(storyboardToken);
350+
});
280351
});

0 commit comments

Comments
 (0)