@@ -8,6 +8,24 @@ const headers = {
88} ;
99
1010const base64Url = ( value ) => Buffer . from ( value ) . toString ( 'base64url' ) ;
11+ const REMOTE_SCOPE = 'notifycomp.remote' ;
12+ const PUSH_SCOPE = 'assignment_notifications' ;
13+ const REMOTE_TOKEN_TTL_SECONDS = 12 * 60 * 60 ;
14+ const PUSH_TOKEN_TTL_SECONDS = 10 * 60 ;
15+
16+ const getCompetitionManagers = ( competition ) => {
17+ const data = competition ?. competition || competition ;
18+ return [ ...( data ?. organizers || [ ] ) , ...( data ?. delegates || [ ] ) ] ;
19+ } ;
20+
21+ const isListedCompetitionManager = ( competition , userId ) =>
22+ getCompetitionManagers ( competition ) . some ( ( user ) => Number ( user ?. id ) === Number ( userId ) ) ;
23+
24+ const getManagedCompetitionIds = ( competitions , userId ) =>
25+ competitions
26+ . filter ( ( competition ) => isListedCompetitionManager ( competition , userId ) )
27+ . map ( ( competition ) => competition . id )
28+ . filter ( Boolean ) ;
1129
1230const signJwt = ( claims , secret ) => {
1331 const encodedHeader = base64Url ( JSON . stringify ( { alg : 'HS256' , typ : 'JWT' } ) ) ;
@@ -42,7 +60,19 @@ exports.handler = async (event) => {
4260 } ;
4361 }
4462
45- const { accessToken } = JSON . parse ( event . body || '{}' ) ;
63+ let body ;
64+ try {
65+ body = JSON . parse ( event . body || '{}' ) ;
66+ } catch {
67+ return {
68+ statusCode : 400 ,
69+ headers,
70+ body : JSON . stringify ( { message : 'Invalid JSON body' } ) ,
71+ } ;
72+ }
73+
74+ const { accessToken, competitionId, scope } = body ;
75+ const tokenScope = scope === REMOTE_SCOPE ? REMOTE_SCOPE : PUSH_SCOPE ;
4676 if ( ! accessToken ) {
4777 return {
4878 statusCode : 400 ,
@@ -52,7 +82,9 @@ exports.handler = async (event) => {
5282 }
5383
5484 const wcaOrigin = process . env . WCA_ORIGIN || 'https://www.worldcubeassociation.org' ;
55- const meResponse = await fetch ( `${ wcaOrigin } /api/v0/me` , {
85+ const meParams =
86+ tokenScope === REMOTE_SCOPE ? '?upcoming_competitions=true&ongoing_competitions=true' : '' ;
87+ const meResponse = await fetch ( `${ wcaOrigin } /api/v0/me${ meParams } ` , {
5688 headers : {
5789 Authorization : `Bearer ${ accessToken } ` ,
5890 } ,
@@ -66,7 +98,7 @@ exports.handler = async (event) => {
6698 } ;
6799 }
68100
69- const { me } = await meResponse . json ( ) ;
101+ const { me, ongoing_competitions = [ ] , upcoming_competitions = [ ] } = await meResponse . json ( ) ;
70102 if ( ! me ?. id ) {
71103 return {
72104 statusCode : 401 ,
@@ -75,14 +107,68 @@ exports.handler = async (event) => {
75107 } ;
76108 }
77109
110+ const tokenTtlSeconds =
111+ tokenScope === REMOTE_SCOPE ? REMOTE_TOKEN_TTL_SECONDS : PUSH_TOKEN_TTL_SECONDS ;
112+ if ( tokenScope === REMOTE_SCOPE && ! competitionId ) {
113+ return {
114+ statusCode : 400 ,
115+ headers,
116+ body : JSON . stringify ( { message : 'Missing competition ID for remote token' } ) ,
117+ } ;
118+ }
119+
120+ let remoteCompetitionIds = [ ] ;
121+
122+ if ( tokenScope === REMOTE_SCOPE ) {
123+ const competitionResponse = await fetch ( `${ wcaOrigin } /api/v0/competitions/${ competitionId } ` , {
124+ headers : {
125+ Authorization : `Bearer ${ accessToken } ` ,
126+ } ,
127+ } ) ;
128+
129+ if ( ! competitionResponse . ok ) {
130+ return {
131+ statusCode : competitionResponse . status === 404 ? 404 : 502 ,
132+ headers,
133+ body : JSON . stringify ( { message : 'Unable to verify competition remote access' } ) ,
134+ } ;
135+ }
136+
137+ const competition = await competitionResponse . json ( ) ;
138+ if ( ! isListedCompetitionManager ( competition , me . id ) ) {
139+ return {
140+ statusCode : 403 ,
141+ headers,
142+ body : JSON . stringify ( {
143+ message :
144+ 'Only listed competition delegates and organizers can use Live Activities Remote' ,
145+ } ) ,
146+ } ;
147+ }
148+
149+ // Remote tokens are scoped to competitions the WCA API says this user manages. The
150+ // NotifyComp API must reject remote mutations for competition IDs outside this claim.
151+ remoteCompetitionIds = [
152+ ...new Set ( [
153+ competitionId ,
154+ ...getManagedCompetitionIds ( [ ...ongoing_competitions , ...upcoming_competitions ] , me . id ) ,
155+ ] ) ,
156+ ] ;
157+ }
158+
78159 const now = Math . floor ( Date . now ( ) / 1000 ) ;
79160 const token = signJwt (
80161 {
81162 aud : process . env . COMPETITION_GROUPS_JWT_AUDIENCE || 'notifycomp' ,
82- exp : now + 10 * 60 ,
163+ competitionIds : tokenScope === REMOTE_SCOPE ? remoteCompetitionIds : undefined ,
164+ exp : now + tokenTtlSeconds ,
83165 iat : now ,
84166 iss : process . env . COMPETITION_GROUPS_JWT_ISSUER || 'competitiongroups.com' ,
167+ name : me . name ,
168+ scope : tokenScope ,
169+ scopes : [ tokenScope ] ,
85170 sub : `wca:${ me . id } ` ,
171+ wcaUserId : me . id ,
86172 wcaUserIds : [ me . id ] ,
87173 } ,
88174 secret ,
0 commit comments