@moq-web/cat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cwt.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { type CwtClaims, type MoqtScope, type CborValue } from './types.js';
2
+ /**
3
+ * Encode CWT claims to a CBOR map (as bytes).
4
+ */
5
+ export declare function cwtClaimsEncode(claims: CwtClaims): Uint8Array;
6
+ /**
7
+ * Decode CWT claims from CBOR map bytes.
8
+ *
9
+ * @throws {CwtError} If the data is not a valid CWT claims map
10
+ */
11
+ export declare function cwtClaimsDecode(data: Uint8Array): CwtClaims;
12
+ /**
13
+ * Convert a decoded CBOR map to CwtClaims.
14
+ */
15
+ export declare function cwtClaimsFromMap(map: Map<number | string, CborValue>): CwtClaims;
16
+ /**
17
+ * Encode MoQT scopes to a CBOR array value.
18
+ * Each scope: [actions_array, namespace_match?, track_match?]
19
+ */
20
+ export declare function moqtScopesEncode(scopes: MoqtScope[]): CborValue[];
21
+ /**
22
+ * Decode MoQT scopes from a CBOR array.
23
+ */
24
+ export declare function moqtScopesDecode(scopesArray: CborValue[]): MoqtScope[];
25
+ /**
26
+ * Check if CWT claims have expired.
27
+ */
28
+ export declare function cwtIsExpired(claims: CwtClaims, nowSeconds?: number, clockSkewSeconds?: number): boolean;
29
+ /**
30
+ * Check if CWT claims are not yet valid (before nbf).
31
+ */
32
+ export declare function cwtIsNotYetValid(claims: CwtClaims, nowSeconds?: number, clockSkewSeconds?: number): boolean;
33
+ /**
34
+ * Check if CWT claims match a required audience.
35
+ */
36
+ export declare function cwtMatchesAudience(claims: CwtClaims, requiredAudience: string): boolean;
37
+ /**
38
+ * CWT encoding/decoding error.
39
+ */
40
+ export declare class CwtError extends Error {
41
+ constructor(message: string);
42
+ }
package/dist/cwt.js ADDED
@@ -0,0 +1,261 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * @fileoverview CWT (CBOR Web Token) Claims Implementation (RFC 8392)
5
+ *
6
+ * Encodes and decodes CWT claims with integer keys, including
7
+ * MoQT-specific authorization scopes.
8
+ */
9
+ import { cborEncode, cborDecode } from './cbor.js';
10
+ import { CwtClaimKey, MoqtAction, } from './types.js';
11
+ /** Standard CWT claim keys that cannot be overridden via additionalClaims */
12
+ const RESERVED_CLAIM_KEYS = new Set([
13
+ CwtClaimKey.ISS, CwtClaimKey.SUB, CwtClaimKey.AUD,
14
+ CwtClaimKey.EXP, CwtClaimKey.NBF, CwtClaimKey.IAT,
15
+ CwtClaimKey.CTI, CwtClaimKey.MOQT, 65000,
16
+ ]);
17
+ // ============================================================================
18
+ // CWT Claims Encode / Decode
19
+ // ============================================================================
20
+ /**
21
+ * Encode CWT claims to a CBOR map (as bytes).
22
+ */
23
+ export function cwtClaimsEncode(claims) {
24
+ const map = new Map();
25
+ if (claims.iss !== undefined)
26
+ map.set(CwtClaimKey.ISS, claims.iss);
27
+ if (claims.sub !== undefined)
28
+ map.set(CwtClaimKey.SUB, claims.sub);
29
+ if (claims.aud !== undefined) {
30
+ if (Array.isArray(claims.aud)) {
31
+ map.set(CwtClaimKey.AUD, claims.aud);
32
+ }
33
+ else {
34
+ map.set(CwtClaimKey.AUD, claims.aud);
35
+ }
36
+ }
37
+ if (claims.exp !== undefined)
38
+ map.set(CwtClaimKey.EXP, claims.exp);
39
+ if (claims.nbf !== undefined)
40
+ map.set(CwtClaimKey.NBF, claims.nbf);
41
+ if (claims.iat !== undefined)
42
+ map.set(CwtClaimKey.IAT, claims.iat);
43
+ if (claims.cti !== undefined)
44
+ map.set(CwtClaimKey.CTI, claims.cti);
45
+ if (claims.moqt !== undefined) {
46
+ map.set(CwtClaimKey.MOQT, moqtScopesEncode(claims.moqt));
47
+ }
48
+ // M6: Additional claims — reject collisions with standard claim keys
49
+ if (claims.additionalClaims) {
50
+ for (const [key, value] of claims.additionalClaims) {
51
+ if (RESERVED_CLAIM_KEYS.has(key)) {
52
+ throw new CwtError(`Additional claim key ${key} conflicts with a reserved CWT claim key`);
53
+ }
54
+ map.set(key, value);
55
+ }
56
+ }
57
+ return cborEncode(map);
58
+ }
59
+ /**
60
+ * Decode CWT claims from CBOR map bytes.
61
+ *
62
+ * @throws {CwtError} If the data is not a valid CWT claims map
63
+ */
64
+ export function cwtClaimsDecode(data) {
65
+ const decoded = cborDecode(data);
66
+ if (!(decoded.value instanceof Map)) {
67
+ throw new CwtError('CWT claims must be a CBOR map');
68
+ }
69
+ return cwtClaimsFromMap(decoded.value);
70
+ }
71
+ /**
72
+ * Convert a decoded CBOR map to CwtClaims.
73
+ */
74
+ export function cwtClaimsFromMap(map) {
75
+ const claims = {};
76
+ const additional = new Map();
77
+ for (const [key, value] of map) {
78
+ const numKey = typeof key === 'string' ? parseInt(key, 10) : key;
79
+ if (isNaN(numKey))
80
+ continue;
81
+ switch (numKey) {
82
+ case CwtClaimKey.ISS:
83
+ if (typeof value === 'string')
84
+ claims.iss = value;
85
+ break;
86
+ case CwtClaimKey.SUB:
87
+ if (typeof value === 'string')
88
+ claims.sub = value;
89
+ break;
90
+ case CwtClaimKey.AUD:
91
+ if (typeof value === 'string') {
92
+ claims.aud = value;
93
+ }
94
+ else if (Array.isArray(value)) {
95
+ claims.aud = value.filter((v) => typeof v === 'string');
96
+ }
97
+ break;
98
+ case CwtClaimKey.EXP:
99
+ // L5: Validate that time claims are integers
100
+ if (typeof value === 'number') {
101
+ if (!Number.isInteger(value)) {
102
+ throw new CwtError(`CWT exp claim must be an integer, got ${value}`);
103
+ }
104
+ claims.exp = value;
105
+ }
106
+ break;
107
+ case CwtClaimKey.NBF:
108
+ if (typeof value === 'number') {
109
+ if (!Number.isInteger(value)) {
110
+ throw new CwtError(`CWT nbf claim must be an integer, got ${value}`);
111
+ }
112
+ claims.nbf = value;
113
+ }
114
+ break;
115
+ case CwtClaimKey.IAT:
116
+ if (typeof value === 'number') {
117
+ if (!Number.isInteger(value)) {
118
+ throw new CwtError(`CWT iat claim must be an integer, got ${value}`);
119
+ }
120
+ claims.iat = value;
121
+ }
122
+ break;
123
+ case CwtClaimKey.CTI:
124
+ if (value instanceof Uint8Array)
125
+ claims.cti = value;
126
+ break;
127
+ case CwtClaimKey.MOQT:
128
+ case 65000: // Legacy MoQT claim key
129
+ claims.moqt = decodeMoqtScopesValue(value);
130
+ break;
131
+ default:
132
+ additional.set(numKey, value);
133
+ break;
134
+ }
135
+ }
136
+ if (additional.size > 0) {
137
+ claims.additionalClaims = additional;
138
+ }
139
+ return claims;
140
+ }
141
+ // ============================================================================
142
+ // MoQT Scopes
143
+ // ============================================================================
144
+ /**
145
+ * Encode MoQT scopes to a CBOR array value.
146
+ * Each scope: [actions_array, namespace_match?, track_match?]
147
+ */
148
+ export function moqtScopesEncode(scopes) {
149
+ return scopes.map((scope) => {
150
+ const entry = [scope.actions.map((a) => a)];
151
+ if (scope.namespaceMatch !== undefined) {
152
+ entry.push(scope.namespaceMatch.map((n) => n instanceof Uint8Array ? n : n));
153
+ }
154
+ if (scope.trackMatch !== undefined) {
155
+ // Ensure namespace is present (even if null) before track
156
+ if (scope.namespaceMatch === undefined) {
157
+ entry.push(null);
158
+ }
159
+ entry.push(scope.trackMatch instanceof Uint8Array ? scope.trackMatch : scope.trackMatch);
160
+ }
161
+ return entry;
162
+ });
163
+ }
164
+ /**
165
+ * Decode MoQT scopes from a CBOR value.
166
+ * Handles both direct CBOR array and bstr-wrapped CBOR.
167
+ */
168
+ function decodeMoqtScopesValue(value) {
169
+ let scopesArray;
170
+ if (value instanceof Uint8Array) {
171
+ // bstr-wrapped CBOR
172
+ try {
173
+ const decoded = cborDecode(value);
174
+ if (!Array.isArray(decoded.value))
175
+ return [];
176
+ scopesArray = decoded.value;
177
+ }
178
+ catch {
179
+ return [];
180
+ }
181
+ }
182
+ else if (Array.isArray(value)) {
183
+ scopesArray = value;
184
+ }
185
+ else {
186
+ return [];
187
+ }
188
+ return moqtScopesDecode(scopesArray);
189
+ }
190
+ /**
191
+ * Decode MoQT scopes from a CBOR array.
192
+ */
193
+ export function moqtScopesDecode(scopesArray) {
194
+ const scopes = [];
195
+ for (const scopeValue of scopesArray) {
196
+ if (!Array.isArray(scopeValue) || scopeValue.length === 0)
197
+ continue;
198
+ const actionsValue = scopeValue[0];
199
+ if (!Array.isArray(actionsValue))
200
+ continue;
201
+ const actions = actionsValue
202
+ .filter((a) => typeof a === 'number')
203
+ .filter((a) => a >= MoqtAction.ClientSetup && a <= MoqtAction.TrackStatus);
204
+ const scope = { actions };
205
+ if (scopeValue.length > 1 && scopeValue[1] !== null) {
206
+ const nsMatch = scopeValue[1];
207
+ if (Array.isArray(nsMatch)) {
208
+ // L6: Reject non-string/non-bytes namespace elements instead of coercing
209
+ scope.namespaceMatch = nsMatch.filter((n) => typeof n === 'string' || n instanceof Uint8Array);
210
+ }
211
+ }
212
+ if (scopeValue.length > 2 && scopeValue[2] !== null) {
213
+ const trackMatch = scopeValue[2];
214
+ if (typeof trackMatch === 'string' || trackMatch instanceof Uint8Array) {
215
+ scope.trackMatch = trackMatch;
216
+ }
217
+ }
218
+ scopes.push(scope);
219
+ }
220
+ return scopes;
221
+ }
222
+ // ============================================================================
223
+ // Validation Helpers
224
+ // ============================================================================
225
+ /**
226
+ * Check if CWT claims have expired.
227
+ */
228
+ export function cwtIsExpired(claims, nowSeconds, clockSkewSeconds = 60) {
229
+ if (claims.exp === undefined)
230
+ return false;
231
+ const now = nowSeconds ?? Math.floor(Date.now() / 1000);
232
+ return now > claims.exp + clockSkewSeconds;
233
+ }
234
+ /**
235
+ * Check if CWT claims are not yet valid (before nbf).
236
+ */
237
+ export function cwtIsNotYetValid(claims, nowSeconds, clockSkewSeconds = 60) {
238
+ if (claims.nbf === undefined)
239
+ return false;
240
+ const now = nowSeconds ?? Math.floor(Date.now() / 1000);
241
+ return now < claims.nbf - clockSkewSeconds;
242
+ }
243
+ /**
244
+ * Check if CWT claims match a required audience.
245
+ */
246
+ export function cwtMatchesAudience(claims, requiredAudience) {
247
+ if (claims.aud === undefined)
248
+ return false;
249
+ if (typeof claims.aud === 'string')
250
+ return claims.aud === requiredAudience;
251
+ return claims.aud.includes(requiredAudience);
252
+ }
253
+ /**
254
+ * CWT encoding/decoding error.
255
+ */
256
+ export class CwtError extends Error {
257
+ constructor(message) {
258
+ super(message);
259
+ this.name = 'CwtError';
260
+ }
261
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview CAT/C4M Token Library for MoQ Transport
3
+ *
4
+ * Common Access Token (CAT) implementation using CBOR, COSE_Sign1, and CWT.
5
+ * Provides standards-compliant token creation, decoding, and validation
6
+ * for MoQ Transport authorization.
7
+ *
8
+ * @packageDocumentation
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import {
13
+ * CatTokenBuilder,
14
+ * CatTokenDecoder,
15
+ * CoseAlgorithm,
16
+ * MoqtAction,
17
+ * } from '@moq-web/cat';
18
+ *
19
+ * // Create a token
20
+ * const token = await new CatTokenBuilder()
21
+ * .issuer('https://auth.example.com')
22
+ * .subject('user-123')
23
+ * .audience('moq-relay')
24
+ * .expiration(Date.now() / 1000 + 3600)
25
+ * .issuedAt()
26
+ * .moqtScopes([{
27
+ * actions: [MoqtAction.Subscribe],
28
+ * namespaceMatch: ['room', 'abc'],
29
+ * }])
30
+ * .sign(privateKey);
31
+ *
32
+ * // Validate a token
33
+ * const result = await CatTokenDecoder.validate(token, publicKey);
34
+ * ```
35
+ */
36
+ export { C4M_TOKEN_TYPE, CoseAlgorithm, CoseHeaderParam, COSE_ALG_PARAMS, CwtClaimKey, MoqtAction, type CborValue, type CborTagged, type CoseSign1, type CwtClaims, type MoqtScope, type CatToken, type CatValidationResult, type CatValidationOptions, type CoseAlgParams, } from './types.js';
37
+ export { cborEncode, cborDecode, cborDecodeTagged, cborEncodeTagged, CborError, } from './cbor.js';
38
+ export { coseSign1Encode, coseSign1Decode, coseSign1Sign, coseSign1Verify, coseSign1SigStructure, coseMac0MacStructure, coseSign1GetAlgorithm, coseSign1DecodeProtectedHeader, CoseError, } from './cose.js';
39
+ export { cwtClaimsEncode, cwtClaimsDecode, cwtClaimsFromMap, moqtScopesEncode, moqtScopesDecode, cwtIsExpired, cwtIsNotYetValid, cwtMatchesAudience, CwtError, } from './cwt.js';
40
+ export { CatTokenBuilder, CatTokenDecoder, base64urlDecode, base64urlEncode, CatError, } from './cat.js';
41
+ export { generateTestKeyPair, generateTestCatToken, catTokenToBase64url, generateWrongScopeToken, generateExpiredToken, generateBadSignatureToken, generateWrongKeyToken, type TestCatTokenOptions, } from './test-utils.js';
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * @fileoverview CAT/C4M Token Library for MoQ Transport
5
+ *
6
+ * Common Access Token (CAT) implementation using CBOR, COSE_Sign1, and CWT.
7
+ * Provides standards-compliant token creation, decoding, and validation
8
+ * for MoQ Transport authorization.
9
+ *
10
+ * @packageDocumentation
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import {
15
+ * CatTokenBuilder,
16
+ * CatTokenDecoder,
17
+ * CoseAlgorithm,
18
+ * MoqtAction,
19
+ * } from '@moq-web/cat';
20
+ *
21
+ * // Create a token
22
+ * const token = await new CatTokenBuilder()
23
+ * .issuer('https://auth.example.com')
24
+ * .subject('user-123')
25
+ * .audience('moq-relay')
26
+ * .expiration(Date.now() / 1000 + 3600)
27
+ * .issuedAt()
28
+ * .moqtScopes([{
29
+ * actions: [MoqtAction.Subscribe],
30
+ * namespaceMatch: ['room', 'abc'],
31
+ * }])
32
+ * .sign(privateKey);
33
+ *
34
+ * // Validate a token
35
+ * const result = await CatTokenDecoder.validate(token, publicKey);
36
+ * ```
37
+ */
38
+ // Types
39
+ export { C4M_TOKEN_TYPE, CoseAlgorithm, CoseHeaderParam, COSE_ALG_PARAMS, CwtClaimKey, MoqtAction, } from './types.js';
40
+ // CBOR
41
+ export { cborEncode, cborDecode, cborDecodeTagged, cborEncodeTagged, CborError, } from './cbor.js';
42
+ // COSE
43
+ export { coseSign1Encode, coseSign1Decode, coseSign1Sign, coseSign1Verify, coseSign1SigStructure, coseMac0MacStructure, coseSign1GetAlgorithm, coseSign1DecodeProtectedHeader, CoseError, } from './cose.js';
44
+ // CWT
45
+ export { cwtClaimsEncode, cwtClaimsDecode, cwtClaimsFromMap, moqtScopesEncode, moqtScopesDecode, cwtIsExpired, cwtIsNotYetValid, cwtMatchesAudience, CwtError, } from './cwt.js';
46
+ // CAT
47
+ export { CatTokenBuilder, CatTokenDecoder, base64urlDecode, base64urlEncode, CatError, } from './cat.js';
48
+ // Test utilities
49
+ export { generateTestKeyPair, generateTestCatToken, catTokenToBase64url, generateWrongScopeToken, generateExpiredToken, generateBadSignatureToken, generateWrongKeyToken, } from './test-utils.js';
@@ -0,0 +1,71 @@
1
+ import { CoseAlgorithm, type CwtClaims, type MoqtScope } from './types.js';
2
+ /**
3
+ * Generate a test ECDSA key pair for CAT token signing/verification.
4
+ *
5
+ * WARNING: Keys are extractable by default for testing convenience.
6
+ * Do NOT use this function for production key generation — use
7
+ * crypto.subtle.generateKey directly with extractable=false.
8
+ *
9
+ * @param algorithm - COSE algorithm (default: ES256)
10
+ * @param extractable - Whether the private key is extractable (default: true for testing)
11
+ * @returns WebCrypto key pair
12
+ */
13
+ export declare function generateTestKeyPair(algorithm?: CoseAlgorithm, extractable?: boolean): Promise<CryptoKeyPair>;
14
+ export interface TestCatTokenOptions {
15
+ /** Signing key pair (will generate if not provided) */
16
+ keyPair?: CryptoKeyPair;
17
+ /** Partial claims to set */
18
+ claims?: Partial<CwtClaims>;
19
+ /** MoQT scopes */
20
+ scopes?: MoqtScope[];
21
+ /** Signing algorithm (default: ES256) */
22
+ algorithm?: CoseAlgorithm;
23
+ /** Generate an expired token */
24
+ expired?: boolean;
25
+ /** Token lifetime in seconds (default: 3600) */
26
+ lifetimeSeconds?: number;
27
+ }
28
+ /**
29
+ * Generate a properly-formatted test CAT token (CWT/COSE_Sign1).
30
+ * Unlike JWT fake tokens, these use proper CBOR encoding and ECDSA signatures.
31
+ *
32
+ * @returns COSE_Sign1 CBOR bytes
33
+ */
34
+ export declare function generateTestCatToken(options?: TestCatTokenOptions): Promise<{
35
+ tokenBytes: Uint8Array;
36
+ keyPair: CryptoKeyPair;
37
+ }>;
38
+ /**
39
+ * Encode CAT token bytes to base64url string for session.setAuthToken().
40
+ */
41
+ export declare function catTokenToBase64url(tokenBytes: Uint8Array): string;
42
+ /**
43
+ * Generate an invalid CAT token with a wrong namespace scope.
44
+ */
45
+ export declare function generateWrongScopeToken(_roomId: string, keyPair?: CryptoKeyPair): Promise<{
46
+ tokenBytes: Uint8Array;
47
+ keyPair: CryptoKeyPair;
48
+ }>;
49
+ /**
50
+ * Generate an expired CAT token.
51
+ */
52
+ export declare function generateExpiredToken(keyPair?: CryptoKeyPair): Promise<{
53
+ tokenBytes: Uint8Array;
54
+ keyPair: CryptoKeyPair;
55
+ }>;
56
+ /**
57
+ * Generate a CAT token with a corrupted signature.
58
+ */
59
+ export declare function generateBadSignatureToken(keyPair?: CryptoKeyPair): Promise<{
60
+ tokenBytes: Uint8Array;
61
+ keyPair: CryptoKeyPair;
62
+ }>;
63
+ /**
64
+ * Generate a CAT token signed with a different key (signature won't verify
65
+ * against the original key pair).
66
+ */
67
+ export declare function generateWrongKeyToken(algorithm?: CoseAlgorithm): Promise<{
68
+ tokenBytes: Uint8Array;
69
+ signingKeyPair: CryptoKeyPair;
70
+ verifyKeyPair: CryptoKeyPair;
71
+ }>;
@@ -0,0 +1,136 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * @fileoverview CAT Token Test Utilities
5
+ *
6
+ * Helpers for generating test key pairs and properly-formatted
7
+ * CAT (CWT/COSE) tokens for testing and demos.
8
+ */
9
+ import { CatTokenBuilder, base64urlEncode } from './cat.js';
10
+ import { coseSign1Encode, coseSign1Decode } from './cose.js';
11
+ import { CoseAlgorithm, COSE_ALG_PARAMS, MoqtAction, } from './types.js';
12
+ // ============================================================================
13
+ // Key Generation
14
+ // ============================================================================
15
+ /**
16
+ * Generate a test ECDSA key pair for CAT token signing/verification.
17
+ *
18
+ * WARNING: Keys are extractable by default for testing convenience.
19
+ * Do NOT use this function for production key generation — use
20
+ * crypto.subtle.generateKey directly with extractable=false.
21
+ *
22
+ * @param algorithm - COSE algorithm (default: ES256)
23
+ * @param extractable - Whether the private key is extractable (default: true for testing)
24
+ * @returns WebCrypto key pair
25
+ */
26
+ export async function generateTestKeyPair(algorithm = CoseAlgorithm.ES256, extractable = true) {
27
+ const params = COSE_ALG_PARAMS[algorithm];
28
+ if (params.isMac) {
29
+ // HMAC key generation
30
+ const key = await crypto.subtle.generateKey({ name: 'HMAC', hash: params.hash }, extractable, ['sign', 'verify']);
31
+ // Return same key for both sign and verify (symmetric)
32
+ return { privateKey: key, publicKey: key };
33
+ }
34
+ return crypto.subtle.generateKey({ name: params.name, namedCurve: params.namedCurve }, extractable, ['sign', 'verify']);
35
+ }
36
+ /**
37
+ * Generate a properly-formatted test CAT token (CWT/COSE_Sign1).
38
+ * Unlike JWT fake tokens, these use proper CBOR encoding and ECDSA signatures.
39
+ *
40
+ * @returns COSE_Sign1 CBOR bytes
41
+ */
42
+ export async function generateTestCatToken(options = {}) {
43
+ const algorithm = options.algorithm ?? CoseAlgorithm.ES256;
44
+ const keyPair = options.keyPair ?? await generateTestKeyPair(algorithm);
45
+ const now = Math.floor(Date.now() / 1000);
46
+ const lifetime = options.lifetimeSeconds ?? 3600;
47
+ const builder = new CatTokenBuilder().withAlgorithm(algorithm);
48
+ // Set default claims
49
+ builder
50
+ .issuer(options.claims?.iss ?? 'https://test.moq.example')
51
+ .subject(options.claims?.sub ?? 'test-user')
52
+ .audience(options.claims?.aud ?? 'moq-relay');
53
+ if (options.expired) {
54
+ builder.issuedAt(now - lifetime * 2);
55
+ builder.expiration(now - lifetime);
56
+ }
57
+ else {
58
+ builder.issuedAt(options.claims?.iat ?? now);
59
+ builder.expiration(options.claims?.exp ?? now + lifetime);
60
+ }
61
+ if (options.claims?.nbf !== undefined) {
62
+ builder.notBefore(options.claims.nbf);
63
+ }
64
+ if (options.claims?.cti) {
65
+ builder.cwtId(options.claims.cti);
66
+ }
67
+ // Set scopes
68
+ const scopes = options.scopes ?? [{
69
+ actions: [MoqtAction.Subscribe, MoqtAction.Publish, MoqtAction.SubscribeNamespace, MoqtAction.PublishNamespace],
70
+ }];
71
+ builder.moqtScopes(scopes);
72
+ const tokenBytes = await builder.sign(keyPair.privateKey);
73
+ return { tokenBytes, keyPair };
74
+ }
75
+ /**
76
+ * Encode CAT token bytes to base64url string for session.setAuthToken().
77
+ */
78
+ export function catTokenToBase64url(tokenBytes) {
79
+ return base64urlEncode(tokenBytes);
80
+ }
81
+ // ============================================================================
82
+ // Invalid Token Generators (for denial testing)
83
+ // ============================================================================
84
+ /**
85
+ * Generate an invalid CAT token with a wrong namespace scope.
86
+ */
87
+ export async function generateWrongScopeToken(_roomId, keyPair) {
88
+ const kp = keyPair ?? await generateTestKeyPair();
89
+ return generateTestCatToken({
90
+ keyPair: kp,
91
+ scopes: [{
92
+ actions: [MoqtAction.Subscribe],
93
+ namespaceMatch: ['mocha', `WRONG-ROOM-${Date.now().toString(36)}`],
94
+ }],
95
+ claims: { sub: 'wrong-scope-user' },
96
+ });
97
+ }
98
+ /**
99
+ * Generate an expired CAT token.
100
+ */
101
+ export async function generateExpiredToken(keyPair) {
102
+ const kp = keyPair ?? await generateTestKeyPair();
103
+ return generateTestCatToken({
104
+ keyPair: kp,
105
+ expired: true,
106
+ claims: { sub: 'expired-user' },
107
+ });
108
+ }
109
+ /**
110
+ * Generate a CAT token with a corrupted signature.
111
+ */
112
+ export async function generateBadSignatureToken(keyPair) {
113
+ const kp = keyPair ?? await generateTestKeyPair();
114
+ const { tokenBytes } = await generateTestCatToken({ keyPair: kp });
115
+ // Decode, corrupt signature, re-encode
116
+ const sign1 = coseSign1Decode(tokenBytes);
117
+ const corrupted = {
118
+ ...sign1,
119
+ signature: new Uint8Array(sign1.signature.length),
120
+ };
121
+ crypto.getRandomValues(corrupted.signature);
122
+ return { tokenBytes: coseSign1Encode(corrupted), keyPair: kp };
123
+ }
124
+ /**
125
+ * Generate a CAT token signed with a different key (signature won't verify
126
+ * against the original key pair).
127
+ */
128
+ export async function generateWrongKeyToken(algorithm = CoseAlgorithm.ES256) {
129
+ const signingKeyPair = await generateTestKeyPair(algorithm);
130
+ const verifyKeyPair = await generateTestKeyPair(algorithm);
131
+ const { tokenBytes } = await generateTestCatToken({
132
+ keyPair: signingKeyPair,
133
+ claims: { sub: 'wrong-key-user' },
134
+ });
135
+ return { tokenBytes, signingKeyPair, verifyKeyPair };
136
+ }