2017-04-13 00:42:44 -07:00
'use strict' ;
exports = module . exports = {
2020-12-29 17:22:32 -08:00
getFeatures ,
2020-02-14 12:20:15 +01:00
2025-07-16 22:07:54 +02:00
checkSubscription ,
2023-08-04 15:34:38 +05:30
getApiServerOrigin ,
getWebServerOrigin ,
getConsoleServerOrigin ,
downloadManifest ,
2020-12-29 17:22:32 -08:00
getApps ,
getApp ,
getAppVersion ,
2023-08-04 15:34:38 +05:30
downloadIcon ,
2019-05-04 11:45:03 -07:00
2025-05-12 21:56:32 +02:00
registerCloudron3 ,
2021-04-13 14:19:45 -07:00
updateCloudron ,
2025-09-24 21:25:31 +02:00
unlinkAccount ,
2019-05-05 13:00:45 -07:00
2020-12-29 17:22:32 -08:00
getSubscription ,
isFreePlan ,
2017-06-21 22:17:32 -07:00
2020-12-29 17:22:32 -08:00
getAppUpdate ,
getBoxUpdate ,
2017-04-13 01:23:11 -07:00
2022-04-04 13:54:57 -07:00
// exported for tests
2023-08-04 15:34:38 +05:30
_setApiServerOrigin : setApiServerOrigin ,
2022-04-04 13:54:57 -07:00
_unregister : unregister
2017-04-13 00:42:44 -07:00
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2019-10-24 17:28:59 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2025-09-24 21:25:31 +02:00
dashboard = require ( './dashboard.js' ) ,
2017-04-13 00:52:02 -07:00
debug = require ( 'debug' ) ( 'box:appstore' ) ,
2025-07-10 11:16:48 +02:00
manifestFormat = require ( '@cloudron/manifest-format' ) ,
2020-02-14 12:20:15 +01:00
paths = require ( './paths.js' ) ,
2023-08-04 15:34:38 +05:30
promiseRetry = require ( './promise-retry.js' ) ,
2018-01-25 09:35:06 -08:00
safe = require ( 'safetydance' ) ,
semver = require ( 'semver' ) ,
2017-04-13 00:42:44 -07:00
settings = require ( './settings.js' ) ,
2025-07-10 10:55:52 +02:00
superagent = require ( '@cloudron/superagent' ) ;
2017-04-13 00:42:44 -07:00
2020-02-28 15:18:16 +01:00
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
2025-05-09 09:50:01 +02:00
const DEFAULT _FEATURES = {
2025-05-06 18:49:56 +02:00
appMaxCount : 2 ,
2020-06-18 14:34:22 +02:00
userMaxCount : 5 ,
domainMaxCount : 1 ,
2025-05-06 18:49:56 +02:00
mailboxMaxCount : 5 ,
branding : false ,
2020-06-18 14:34:22 +02:00
externalLdap : false ,
2025-05-07 21:33:33 +02:00
privateDockerRegistry : false ,
2025-05-06 18:49:56 +02:00
userGroups : false ,
emailServer : false ,
2025-05-13 18:11:10 +02:00
profileConfig : false ,
2025-08-05 12:03:27 +02:00
multipleBackupTargets : false ,
encryptedBackups : false ,
2025-05-07 21:33:33 +02:00
// TODO how to go about that in the UI?
userRoles : false ,
2025-05-06 18:49:56 +02:00
appProxy : false ,
eventlogRetention : false ,
hsts : false ,
// TODO remove usage of old features below
2020-07-09 21:50:58 -07:00
support : false ,
2025-05-06 18:49:56 +02:00
emailPremium : false ,
2020-02-14 12:20:15 +01:00
} ;
2025-05-09 09:50:01 +02:00
let gFeatures = null ;
2020-02-14 12:20:15 +01:00
2025-05-09 09:50:01 +02:00
function getFeatures ( ) {
if ( gFeatures === null ) {
2025-06-10 11:08:17 +02:00
gFeatures = Object . assign ( { } , DEFAULT _FEATURES ) ;
2025-05-09 09:50:01 +02:00
const tmp = safe . JSON . parse ( safe . fs . readFileSync ( paths . FEATURES _INFO _FILE , 'utf8' ) ) ;
2025-06-10 09:34:15 +02:00
if ( ! tmp ) {
return DEFAULT _FEATURES ;
}
2025-05-09 09:50:01 +02:00
for ( const f in DEFAULT _FEATURES ) {
if ( f in tmp ) gFeatures [ f ] = tmp [ f ] ;
if ( tmp [ f ] === null ) gFeatures [ f ] = 100000 ; // null essentially means unlimited
}
2025-05-07 16:22:17 +02:00
}
2020-02-14 12:20:15 +01:00
return gFeatures ;
}
2025-07-16 22:07:54 +02:00
async function checkSubscription ( ) {
// getSubscription() also updates the feature cache
const [ error , result ] = await safe ( getSubscription ( ) ) ;
if ( error && error . reason !== BoxError . LICENSE _ERROR ) return debug ( 'Checking subscription failed:' , error . reason , error . message ) ;
debug ( ` Checking subscription: Cloudron ${ result . cloudronId } is on the ${ result . plan . name } plan. ` ) ;
}
2023-08-04 15:34:38 +05:30
async function getApiServerOrigin ( ) {
return await settings . get ( settings . API _SERVER _ORIGIN _KEY ) || 'https://api.cloudron.io' ;
}
async function setApiServerOrigin ( origin ) {
assert . strictEqual ( typeof origin , 'string' ) ;
await settings . set ( settings . API _SERVER _ORIGIN _KEY , origin ) ;
}
async function getWebServerOrigin ( ) {
return await settings . get ( settings . WEB _SERVER _ORIGIN _KEY ) || 'https://cloudron.io' ;
}
async function getConsoleServerOrigin ( ) {
return await settings . get ( settings . CONSOLE _SERVER _ORIGIN _KEY ) || 'https://console.cloudron.io' ;
}
2021-08-18 15:54:53 -07:00
async function getSubscription ( ) {
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2017-06-21 22:17:32 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/subscription ` )
2021-08-18 15:54:53 -07:00
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2017-06-21 22:17:32 -07:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status === 502 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Stripe error: ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unknown error: ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2020-02-14 12:20:15 +01:00
2021-08-18 15:54:53 -07:00
// update the features cache
2025-06-10 09:34:15 +02:00
getFeatures ( ) ;
2025-05-07 15:03:26 +02:00
for ( const f in gFeatures ) {
if ( typeof response . body . features [ f ] !== 'undefined' ) gFeatures [ f ] = response . body . features [ f ] ;
2025-05-07 15:37:31 +02:00
if ( response . body . features [ f ] === null ) gFeatures [ f ] = 100000 ; // null essentially means unlimited
2025-05-07 15:03:26 +02:00
}
2025-06-13 10:47:48 +02:00
safe . fs . writeFileSync ( paths . FEATURES _INFO _FILE , JSON . stringify ( gFeatures , null , 2 ) , 'utf8' ) ;
2021-08-18 15:54:53 -07:00
return response . body ;
2017-06-21 22:17:32 -07:00
}
2018-05-29 13:16:36 +02:00
function isFreePlan ( subscription ) {
return ! subscription || subscription . plan . id === 'free' ;
}
2021-08-18 15:54:53 -07:00
async function getBoxUpdate ( options ) {
2020-05-06 16:50:27 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2017-04-13 01:31:22 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token ,
boxVersion : constants . VERSION ,
2025-06-25 18:17:22 +02:00
stableOnly : options . stableOnly
2021-08-18 15:54:53 -07:00
} ;
2017-04-13 01:31:22 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/boxupdate ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-05-06 16:50:27 -07:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2025-06-26 15:19:28 +02:00
if ( response . status === 204 ) return null ; // no update
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 || ! response . body ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
const updateInfo = response . body ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
if ( ! semver . valid ( updateInfo . version ) || semver . gt ( constants . VERSION , updateInfo . version ) ) {
2023-07-01 13:34:58 +05:30
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Update version invalid or is a downgrade: ${ response . status } ${ response . text } ` ) ;
2021-08-18 15:54:53 -07:00
}
2017-04-13 01:31:22 -07:00
2021-08-18 15:54:53 -07:00
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
2023-06-26 18:06:37 +05:30
if ( ! updateInfo . version || typeof updateInfo . version !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad version): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . changelog || ! Array . isArray ( updateInfo . changelog ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad changelog): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . sourceTarballUrl || typeof updateInfo . sourceTarballUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad sourceTarballUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . sourceTarballSigUrl || typeof updateInfo . sourceTarballSigUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad sourceTarballSigUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . boxVersionsUrl || typeof updateInfo . boxVersionsUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad boxVersionsUrl): ${ response . status } ${ response . text } ` ) ;
if ( ! updateInfo . boxVersionsSigUrl || typeof updateInfo . boxVersionsSigUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad boxVersionsSigUrl): ${ response . status } ${ response . text } ` ) ;
2024-04-16 19:19:07 +02:00
if ( typeof updateInfo . unstable !== 'boolean' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response (bad unstable): ${ response . status } ${ response . text } ` ) ;
2019-02-20 16:18:47 -08:00
2021-08-18 15:54:53 -07:00
return updateInfo ;
2017-04-13 01:31:22 -07:00
}
2021-08-18 15:54:53 -07:00
async function getAppUpdate ( app , options ) {
2017-04-13 01:23:11 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-05-06 16:50:27 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2017-04-13 01:23:11 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token ,
boxVersion : constants . VERSION ,
appId : app . appStoreId ,
appVersion : app . manifest . version ,
2025-06-25 18:17:22 +02:00
stableOnly : options . stableOnly
2021-08-18 15:54:53 -07:00
} ;
2017-04-13 01:23:11 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/appupdate ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2017-04-13 01:23:11 -07:00
2021-08-18 15:54:53 -07:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2025-06-26 15:19:28 +02:00
if ( response . status === 204 ) return null ; // no update
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 || ! response . body ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
const updateInfo = response . body ;
2018-01-25 09:35:06 -08:00
2021-08-18 15:54:53 -07:00
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver . prerelease ( app . manifest . version ) ? app . manifest . version : ` ${ app . manifest . version } -0 ` ;
2018-03-01 11:36:39 -08:00
2021-08-18 15:54:53 -07:00
// do some sanity checks
if ( ! safe . query ( updateInfo , 'manifest.version' ) || semver . gt ( curAppVersion , safe . query ( updateInfo , 'manifest.version' ) ) ) {
debug ( 'Skipping malformed update of app %s version: %s. got %j' , app . id , curAppVersion , updateInfo ) ;
2023-06-26 18:06:37 +05:30
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Malformed update: ${ response . status } ${ response . text } ` ) ;
2021-08-18 15:54:53 -07:00
}
2017-04-13 01:23:11 -07:00
2021-08-18 15:54:53 -07:00
updateInfo . unstable = ! ! updateInfo . unstable ;
2020-06-05 16:09:12 -07:00
2021-08-18 15:54:53 -07:00
// { id, creationDate, manifest, unstable }
return updateInfo ;
2017-04-13 01:23:11 -07:00
}
2017-09-19 21:07:59 +02:00
2025-09-24 21:25:31 +02:00
async function registerCloudron3 ( ) {
const { domain } = await dashboard . getLocation ( ) ;
const version = constants . VERSION ;
2025-05-12 21:56:32 +02:00
2025-08-20 17:50:43 +02:00
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2025-09-24 20:56:48 +02:00
if ( token ) { // when installed using setupToken, this updates the domain record when called during provisioning
2025-08-20 17:50:43 +02:00
debug ( 'registerCloudron3: already registered. Just updating the record.' ) ;
return await updateCloudron ( { domain , version } ) ;
}
2025-05-12 21:56:32 +02:00
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/register_cloudron3 ` )
. send ( { domain , version } )
. timeout ( 60 * 1000 )
. ok ( ( ) => true ) ) ;
2025-06-06 13:45:23 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , ` Network error reaching appstore: ${ error . message } ` ) ;
2025-06-06 11:25:57 +02:00
if ( response . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unable to register cloudron: ${ response . status } ${ response . text } ` ) ;
2025-05-12 21:56:32 +02:00
2025-06-06 10:19:37 +02:00
if ( ! response . body . cloudronId ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response - no cloudron id' ) ;
if ( ! response . body . cloudronToken ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Invalid response - no token' ) ;
2025-05-12 21:56:32 +02:00
2025-06-06 10:19:37 +02:00
await settings . set ( settings . CLOUDRON _ID _KEY , response . body . cloudronId ) ;
await settings . set ( settings . APPSTORE _API _TOKEN _KEY , response . body . cloudronToken ) ;
2025-05-12 21:56:32 +02:00
2025-06-06 10:19:37 +02:00
debug ( ` registerCloudron3: Cloudron registered with id ${ response . body . cloudronId } ` ) ;
2025-05-12 21:56:32 +02:00
}
2025-09-24 21:25:31 +02:00
async function unlinkAccount ( ) {
debug ( 'unlinkAccount: Unlinking existing account.' ) ;
await unregister ( ) ;
return await registerCloudron3 ( ) ;
}
2021-08-18 15:54:53 -07:00
async function updateCloudron ( data ) {
2021-04-13 14:19:45 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2025-09-13 14:51:30 +02:00
const { domain , version } = data ;
2022-03-31 23:27:00 -07:00
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2021-08-18 15:54:53 -07:00
const query = {
accessToken : token
} ;
2021-04-13 14:19:45 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/update_cloudron ` )
2021-08-18 15:54:53 -07:00
. query ( query )
2025-09-13 14:51:30 +02:00
. send ( { domain , version } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2021-04-13 14:19:45 -07:00
2021-08-18 15:54:53 -07:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2021-04-13 14:19:45 -07:00
2021-08-18 15:54:53 -07:00
debug ( ` updateCloudron: Cloudron updated with data ${ JSON . stringify ( data ) } ` ) ;
2021-04-13 14:19:45 -07:00
}
2022-04-04 13:54:57 -07:00
async function unregister ( ) {
2023-08-02 21:49:29 +05:30
await settings . set ( settings . CLOUDRON _ID _KEY , '' ) ;
2023-08-02 20:07:03 +05:30
await settings . set ( settings . APPSTORE _API _TOKEN _KEY , '' ) ;
2022-04-04 13:54:57 -07:00
}
2023-08-04 15:34:38 +05:30
async function downloadManifest ( appStoreId , manifest ) {
if ( ! appStoreId && ! manifest ) throw new BoxError ( BoxError . BAD _FIELD , 'Neither manifest nor appStoreId provided' ) ;
if ( ! appStoreId ) return { appStoreId : '' , manifest } ;
2024-06-15 17:08:42 +02:00
const [ id , version ] = appStoreId . split ( '@' ) ;
if ( ! manifestFormat . isId ( id ) ) throw new BoxError ( BoxError . BAD _FIELD , 'appStoreId is not valid' ) ;
if ( version && ! semver . valid ( version ) ) throw new BoxError ( BoxError . BAD _FIELD , 'package version is not valid semver' ) ;
2023-08-04 15:34:38 +05:30
2024-06-15 17:08:42 +02:00
const url = await getApiServerOrigin ( ) + '/api/v1/apps/' + id + ( version ? '/versions/' + version : '' ) ;
2023-08-04 15:34:38 +05:30
debug ( ` downloading manifest from ${ url } ` ) ;
2024-04-23 11:41:49 +02:00
const [ error , response ] = await safe ( superagent . get ( url ) . timeout ( 60 * 1000 ) . ok ( ( ) => true ) ) ;
2023-08-04 15:34:38 +05:30
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Network error downloading manifest:' + error . message ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . NOT _FOUND , ` Failed to get app info from store. status: ${ response . status } text: ${ response . text } ` ) ;
if ( ! response . body . manifest || typeof response . body . manifest !== 'object' ) throw new BoxError ( BoxError . NOT _FOUND , ` Missing manifest. Failed to get app info from store. status: ${ response . status } text: ${ response . text } ` ) ;
2024-06-15 17:08:42 +02:00
return { appStoreId : id , manifest : response . body . manifest } ;
2023-08-04 15:34:38 +05:30
}
2023-04-02 18:03:41 +02:00
async function getApps ( ) {
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2019-05-04 11:45:03 -07:00
2023-08-04 15:34:38 +05:30
const [ error , response ] = await safe ( superagent . get ( ` ${ await getApiServerOrigin ( ) } /api/v1/apps ` )
2023-08-02 19:28:14 +05:30
. query ( { accessToken : token , boxVersion : constants . VERSION , unstable : true } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-02-05 11:58:10 -08:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 403 || response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App listing failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
if ( ! response . body . apps ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2020-02-05 11:58:10 -08:00
2023-08-02 20:18:00 +05:30
return response . body . apps ;
2019-05-04 11:45:03 -07:00
}
2021-08-18 15:54:53 -07:00
async function getAppVersion ( appId , version ) {
2019-05-04 11:45:03 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof version , 'string' ) ;
2023-08-02 20:07:03 +05:30
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
2021-08-25 15:18:58 -07:00
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
2020-02-05 11:58:10 -08:00
2023-08-04 15:34:38 +05:30
let url = ` ${ await getApiServerOrigin ( ) } /api/v1/apps/ ${ appId } ` ;
2021-08-18 15:54:53 -07:00
if ( version !== 'latest' ) url += ` /versions/ ${ version } ` ;
2019-05-04 11:45:03 -07:00
2021-08-18 15:54:53 -07:00
const [ error , response ] = await safe ( superagent . get ( url )
. query ( { accessToken : token } )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2021-08-18 15:54:53 -07:00
. ok ( ( ) => true ) ) ;
2020-02-05 11:58:10 -08:00
2024-09-19 11:44:47 +02:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , error ) ;
2025-09-11 12:37:30 +02:00
if ( response . status === 403 || response . status === 401 ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Invalid appstore token' ) ;
2024-10-30 16:21:21 +01:00
if ( response . status === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Could not find app ${ appId } ` ) ;
2023-06-26 18:06:37 +05:30
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` App fetch failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2021-08-18 15:54:53 -07:00
return response . body ;
2019-05-04 11:45:03 -07:00
}
2021-08-18 15:54:53 -07:00
async function getApp ( appId ) {
2019-05-04 11:45:03 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2021-08-18 15:54:53 -07:00
return await getAppVersion ( appId , 'latest' ) ;
2019-05-04 11:45:03 -07:00
}
2023-08-04 15:34:38 +05:30
async function downloadIcon ( appStoreId , version ) {
const iconUrl = ` ${ await getApiServerOrigin ( ) } /api/v1/apps/ ${ appStoreId } /versions/ ${ version } /icon ` ;
return await promiseRetry ( { times : 10 , interval : 5000 , debug } , async function ( ) {
const [ networkError , response ] = await safe ( superagent . get ( iconUrl )
2024-04-23 11:41:49 +02:00
. timeout ( 60 * 1000 )
2023-08-04 15:34:38 +05:30
. ok ( ( ) => true ) ) ;
2025-07-21 12:11:26 +02:00
if ( networkError ) throw new BoxError ( BoxError . NETWORK _ERROR , ` Network error downloading icon: ${ networkError . message } ` ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Icon download failed. ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2023-08-04 15:34:38 +05:30
2024-06-15 17:23:20 +02:00
const contentType = response . headers [ 'content-type' ] ;
if ( ! contentType || contentType . indexOf ( 'image' ) === - 1 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'AppStore returned invalid icon for app' ) ;
2023-08-04 15:34:38 +05:30
return response . body ;
} ) ;
}