Files
cloudron-box/src/appstore.js
T

426 lines
18 KiB
JavaScript
Raw Normal View History

2017-04-13 00:42:44 -07:00
'use strict';
exports = module.exports = {
2020-12-29 17:22:32 -08:00
getFeatures,
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,
registerCloudronWithSetupToken,
registerCloudronWithLogin,
updateCloudron,
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-06-06 09:51:07 +02:00
const assert = require('assert'),
2019-10-24 17:28:59 -07:00
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
2023-08-11 19:41:05 +05:30
dashboard = require('./dashboard.js'),
2017-04-13 00:52:02 -07:00
debug = require('debug')('box:appstore'),
2024-06-15 17:08:42 +02:00
manifestFormat = require('cloudron-manifestformat'),
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-06-06 09:51:07 +02:00
superagent = require('./superagent.js');
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,
userMaxCount: 5,
domainMaxCount: 1,
2025-05-06 18:49:56 +02:00
mailboxMaxCount: 5,
branding: false,
externalLdap: false,
2025-05-07 21:33:33 +02:00
privateDockerRegistry: false,
2025-05-06 18:49:56 +02:00
userGroups: false,
emailServer: false,
profileConfig: false,
2025-05-07 21:33:33 +02:00
// TODO how to go about that in the UI?
backupSchedules: false,
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,
};
2025-05-09 09:50:01 +02:00
let gFeatures = null;
2025-05-09 09:50:01 +02:00
function getFeatures() {
if (gFeatures === null) {
const tmp = safe.JSON.parse(safe.fs.readFileSync(paths.FEATURES_INFO_FILE, 'utf8'));
if (!tmp) return DEFAULT_FEATURES;
gFeatures = Object.assign({}, DEFAULT_FEATURES);
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
}
}
return gFeatures;
}
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 login(email, password, totpToken) {
2019-05-03 16:27:47 -07:00
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof totpToken, 'string');
2023-08-04 15:34:38 +05:30
const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/login`)
2022-03-31 22:41:48 -07:00
.send({ email, password, totpToken })
.timeout(60 * 1000)
2021-08-18 15:54:53 -07:00
.ok(() => true));
2024-09-19 11:44:47 +02:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Incorrect username or password');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Login error. status code: ${response.status}`);
if (!response.body.accessToken) throw new BoxError(BoxError.EXTERNAL_ERROR, `Login error. invalid response: ${response.text}`);
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07:00
return response.body; // { userId, accessToken }
2019-05-03 16:27:47 -07:00
}
2021-08-18 15:54:53 -07:00
async function registerUser(email, password) {
2019-05-03 16:27:47 -07:00
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
2023-08-04 15:34:38 +05:30
const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/register_user`)
.send({ email, password, utmSource: 'cloudron-dashboard' })
.timeout(60 * 1000)
2021-08-18 15:54:53 -07:00
.ok(() => true));
2019-05-03 16:27:47 -07:00
2024-09-19 11:44:47 +02:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, 'Registration error: account already exists');
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Registration error. invalid response: ${response.status}`);
2021-08-18 15:54:53 -07:00
}
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 })
.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);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
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)}`);
2021-08-18 15:54:53 -07:00
// update the features cache
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
}
2021-08-18 15:54:53 -07:00
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
return response.body;
2017-06-21 22:17:32 -07: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,
automatic: options.automatic
};
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)
.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);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
2021-08-18 15:54:53 -07:00
if (response.status === 204) return; // no update
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 }
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,
automatic: options.automatic
};
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)
.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);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
2021-08-18 15:54:53 -07:00
if (response.status === 204) return; // no update
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`;
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);
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;
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-05-12 21:56:32 +02:00
async function registerCloudron3(domain, version) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof version, 'string');
const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/register_cloudron3`)
.send({ domain, version })
.timeout(60 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Setup token invalid');
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.status} ${error.message}`);
if (!response.body.id) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id');
if (!response.body.token) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token');
await settings.set(settings.CLOUDRON_ID_KEY, response.body.id);
await settings.set(settings.APPSTORE_API_TOKEN_KEY, response.body.token);
debug(`registerCloudron3: Cloudron registered with id ${response.body.id}`);
2025-05-12 21:56:32 +02:00
}
2021-08-18 15:54:53 -07:00
async function registerCloudron(data) {
2019-05-07 14:25:27 -07:00
assert.strictEqual(typeof data, 'object');
2019-02-25 17:29:42 -08:00
const { domain, setupToken, accessToken, version, existingApps } = data;
2019-02-25 17:29:42 -08:00
2023-08-04 15:34:38 +05:30
const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/register_cloudron`)
.send({ domain, setupToken, accessToken, version, existingApps })
.timeout(60 * 1000)
2021-08-18 15:54:53 -07:00
.ok(() => true));
2019-02-25 17:29:42 -08:00
2024-09-19 11:44:47 +02:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
2023-12-02 16:42:03 +01:00
if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Setup token invalid');
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.status} ${error.message}`);
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07: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');
2019-05-03 16:27:47 -07:00
2023-08-02 21:49:29 +05:30
await settings.set(settings.CLOUDRON_ID_KEY, response.body.cloudronId);
2023-08-02 20:07:03 +05:30
await settings.set(settings.APPSTORE_API_TOKEN_KEY, response.body.cloudronToken);
2019-05-03 16:27:47 -07:00
2021-08-18 15:54:53 -07:00
debug(`registerCloudron: Cloudron registered with id ${response.body.cloudronId}`);
2019-05-03 16:27:47 -07:00
}
2021-08-18 15:54:53 -07:00
async function updateCloudron(data) {
assert.strictEqual(typeof data, 'object');
const { domain } = data;
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
};
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)
.send({ domain })
.timeout(60 * 1000)
2021-08-18 15:54:53 -07:00
.ok(() => true));
2021-08-18 15:54:53 -07:00
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`);
2021-08-18 15:54:53 -07:00
debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
}
async function registerCloudronWithSetupToken(options) {
2019-05-03 16:27:47 -07:00
assert.strictEqual(typeof options, 'object');
const { domain } = await dashboard.getLocation();
2021-08-25 15:18:58 -07:00
await registerCloudron({ domain, setupToken: options.setupToken, version: constants.VERSION });
}
async function registerCloudronWithLogin(options) {
assert.strictEqual(typeof options, 'object');
if (options.signup) await registerUser(options.email, options.password);
2021-08-18 15:54:53 -07:00
const result = await login(options.email, options.password, options.totpToken || '');
2023-08-11 19:41:05 +05:30
const { domain } = await dashboard.getLocation();
2023-08-11 19:41:05 +05:30
await registerCloudron({ domain, accessToken: result.accessToken, version: constants.VERSION });
2019-02-25 17:29:42 -08: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}`);
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
}
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 })
.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);
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
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 })
.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);
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, '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}`);
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)
.timeout(60 * 1000)
2023-08-04 15:34:38 +05:30
.ok(() => true));
2024-10-30 18:30:47 +01:00
if (networkError) throw new BoxError(BoxError.NETWORK_ERROR, Object.assign(networkError, { message: 'Downloading icon' }));
2023-08-04 15:34:38 +05:30
if (response.status !== 200) return; // ignore error. this can also happen for apps installed with cloudron-cli
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;
});
}