1+ import jwt , { JwtPayload } from 'jsonwebtoken' ;
2+ import NodeCache from 'node-cache' ;
3+ import { fetch } from 'undici' ;
4+ import { Static } from '@sinclair/typebox' ;
5+ import type { Config } from './config.js' ;
6+ import { MediaMTXAuthRequest , StandardResponse } from './types.js' ;
7+
8+ export const MEDIA_MTX_AUTH_CACHE_TTL_SECONDS = 5 * 60 ;
9+
10+ type FetchResponse = {
11+ ok : boolean ;
12+ status : number ;
13+ text ( ) : Promise < string > ;
14+ }
15+
16+ type FetchLike = ( input : URL , init ?: RequestInit ) => Promise < FetchResponse > ;
17+
18+ export function getMediaMTXAuthCacheKey ( auth : Static < typeof MediaMTXAuthRequest > ) : string {
19+ return JSON . stringify ( {
20+ user : auth . user ,
21+ password : auth . password ,
22+ token : auth . token || '' ,
23+ ip : auth . ip ,
24+ action : auth . action ,
25+ path : auth . path ,
26+ protocol : auth . protocol ,
27+ id : auth . id ,
28+ query : auth . query
29+ } ) ;
30+ }
31+
32+ export function getMediaMTXAuthCacheTTL (
33+ config : Config ,
34+ auth : Static < typeof MediaMTXAuthRequest >
35+ ) : number {
36+ const ttl = MEDIA_MTX_AUTH_CACHE_TTL_SECONDS ;
37+
38+ if ( auth . user !== 'management' || ! auth . password ) {
39+ return ttl ;
40+ }
41+
42+ try {
43+ const decoded = jwt . verify ( auth . password , config . SigningSecret ) as JwtPayload | string ;
44+
45+ if ( typeof decoded === 'string' || typeof decoded . exp !== 'number' ) {
46+ return ttl ;
47+ }
48+
49+ const remaining = Math . floor ( decoded . exp - ( Date . now ( ) / 1000 ) ) ;
50+ return Math . max ( 1 , Math . min ( ttl , remaining ) ) ;
51+ } catch {
52+ return ttl ;
53+ }
54+ }
55+
56+ export async function authenticateMediaMTXRequest (
57+ config : Config ,
58+ cache : NodeCache ,
59+ auth : Static < typeof MediaMTXAuthRequest > ,
60+ fetchImpl : FetchLike = fetch as FetchLike
61+ ) : Promise < { status : number ; body : Static < typeof StandardResponse > ; cached : boolean } > {
62+ const cacheKey = getMediaMTXAuthCacheKey ( auth ) ;
63+ const cached = cache . get < Static < typeof StandardResponse > > ( cacheKey ) ;
64+
65+ if ( cached ) {
66+ return {
67+ status : 200 ,
68+ body : cached ,
69+ cached : true
70+ } ;
71+ }
72+
73+ const url = new URL ( '/api/video/auth' , config . API_URL ) ;
74+ const resp = await fetchImpl ( url , {
75+ method : 'POST' ,
76+ headers : {
77+ 'Content-Type' : 'application/json'
78+ } ,
79+ body : JSON . stringify ( auth )
80+ } ) ;
81+
82+ const text = await resp . text ( ) ;
83+
84+ let body : Static < typeof StandardResponse > = {
85+ status : resp . status ,
86+ message : text || ( resp . ok ? 'Authorized' : 'Unauthorized' )
87+ } ;
88+
89+ if ( text ) {
90+ try {
91+ const parsed = JSON . parse ( text ) as Partial < Static < typeof StandardResponse > > ;
92+
93+ if ( typeof parsed . status === 'number' && typeof parsed . message === 'string' ) {
94+ body = {
95+ status : parsed . status ,
96+ message : parsed . message
97+ } ;
98+ }
99+ } catch {
100+ // Non-JSON responses are normalized above.
101+ }
102+ }
103+
104+ if ( resp . ok ) {
105+ cache . set ( cacheKey , body , getMediaMTXAuthCacheTTL ( config , auth ) ) ;
106+ }
107+
108+ return {
109+ status : resp . status ,
110+ body,
111+ cached : false
112+ } ;
113+ }
0 commit comments