@@ -225,13 +225,17 @@ export const validateDubServiceAccount = tokenCheckValidator({
225225 forbiddenMeansScoped : true ,
226226} )
227227
228- /** ElevenLabs returns the key owner's account for a valid key and 401 otherwise. */
228+ /**
229+ * ElevenLabs returns the key owner's account for a valid key and 400/401 for a bad one. A key
230+ * restricted to other endpoints gets 403, which still proves it authenticated.
231+ */
229232export const validateElevenLabsServiceAccount = tokenCheckValidator ( {
230233 serviceLabel : 'ElevenLabs' ,
231234 request : ( { apiToken } ) => ( {
232235 url : 'https://api.elevenlabs.io/v1/user' ,
233236 headers : { 'xi-api-key' : apiToken , Accept : 'application/json' } ,
234237 } ) ,
238+ forbiddenMeansScoped : true ,
235239} )
236240
237241/** Enrich's credit-balance endpoint costs no credits and succeeds only for a valid key. */
@@ -355,13 +359,17 @@ export const validateIncidentioServiceAccount = tokenCheckValidator({
355359 } ) ,
356360} )
357361
358- /** Instantly's campaign list (the one the Instantly tools call) answers 200 for a valid key and 401 otherwise. */
362+ /**
363+ * Instantly's campaign list (the one the Instantly tools call) answers 200 for a valid key and
364+ * 401 for a bad one; a valid key without the campaigns scope gets 403.
365+ */
359366export const validateInstantlyServiceAccount = tokenCheckValidator ( {
360367 serviceLabel : 'Instantly' ,
361368 request : ( { apiToken } ) => ( {
362369 url : 'https://api.instantly.ai/api/v2/campaigns?limit=1' ,
363370 headers : { Authorization : `Bearer ${ apiToken } ` , Accept : 'application/json' } ,
364371 } ) ,
372+ forbiddenMeansScoped : true ,
365373} )
366374
367375/** Intercom's identify-admin endpoint answers 200 with the token's admin and 401 to an unknown token. */
@@ -597,22 +605,30 @@ export const validateSportmonksServiceAccount = tokenCheckValidator({
597605 } ) ,
598606} )
599607
600- /** Square's list-locations read answers 200 for a valid production access token and 401 otherwise. */
608+ /**
609+ * Square's list-locations read answers 200 for a valid production access token and 401 for a bad
610+ * one; a token without the merchant-profile scope gets 403.
611+ */
601612export const validateSquareServiceAccount = tokenCheckValidator ( {
602613 serviceLabel : 'Square' ,
603614 request : ( { apiToken } ) => ( {
604615 url : 'https://connect.squareup.com/v2/locations' ,
605616 headers : { Authorization : `Bearer ${ apiToken } ` , Accept : 'application/json' } ,
606617 } ) ,
618+ forbiddenMeansScoped : true ,
607619} )
608620
609- /** Listing devices in the token's default tailnet (`-`) answers 200 for a valid token and 401 otherwise. */
621+ /**
622+ * Listing devices in the token's default tailnet (`-`) answers 200 for a valid token and 401 for a
623+ * bad one; an OAuth token without the devices scope gets 403.
624+ */
610625export const validateTailscaleServiceAccount = tokenCheckValidator ( {
611626 serviceLabel : 'Tailscale' ,
612627 request : ( { apiToken } ) => ( {
613628 url : 'https://api.tailscale.com/api/v2/tailnet/-/devices' ,
614629 headers : { Authorization : `Bearer ${ apiToken } ` , Accept : 'application/json' } ,
615630 } ) ,
631+ forbiddenMeansScoped : true ,
616632} )
617633
618634/** Tavily's usage endpoint answers 200 for a valid key and 401 otherwise. */
@@ -651,13 +667,28 @@ export const validateYouComServiceAccount = tokenCheckValidator({
651667 } ) ,
652668} )
653669
654- /** Listing video categories costs one quota unit; Google answers 400/403 for an invalid or restricted key. */
670+ /** Google's 403 reasons that a valid key gets when it is only out of quota for now. */
671+ const YOUTUBE_QUOTA_REASONS = new Set ( [ 'quotaExceeded' , 'dailyLimitExceeded' , 'rateLimitExceeded' ] )
672+
673+ /**
674+ * Listing video categories costs one quota unit. Google answers 400 for an invalid key and 403
675+ * for a restricted one or a project without the YouTube API; a 403 for spent quota still proves
676+ * the key, so only that one is accepted.
677+ */
655678export const validateYouTubeServiceAccount = tokenCheckValidator ( {
656679 serviceLabel : 'YouTube' ,
657680 request : ( { apiToken } ) => ( {
658681 url : `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet®ionCode=US&key=${ encodeURIComponent ( apiToken ) } ` ,
659682 headers : { Accept : 'application/json' } ,
660683 } ) ,
684+ forbiddenMeansScoped : true ,
685+ rejectsKey : ( errorBody ) => {
686+ const error = toRecord ( toRecord ( errorBody ) . error )
687+ if ( error . code !== 400 && error . code !== 403 ) return false
688+ return ! toArray ( error . errors ) . some ( ( entry ) =>
689+ YOUTUBE_QUOTA_REASONS . has ( String ( toRecord ( entry ) . reason ) )
690+ )
691+ } ,
661692} )
662693
663694/** Listing one thread needs a valid project key; Zep answers 401 for any other. */
0 commit comments