2026-02-05 13:20:00 +01:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
2026-02-05 17:29:00 +01:00
|
|
|
getAppVersion,
|
|
|
|
|
downloadManifest,
|
|
|
|
|
getAppUpdate
|
2026-02-05 13:20:00 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const assert = require('node:assert'),
|
|
|
|
|
BoxError = require('./boxerror.js'),
|
2026-02-05 17:29:00 +01:00
|
|
|
debug = require('debug')('box:community'),
|
2026-02-05 13:20:00 +01:00
|
|
|
manifestFormat = require('@cloudron/manifest-format'),
|
|
|
|
|
safe = require('safetydance'),
|
|
|
|
|
superagent = require('@cloudron/superagent');
|
|
|
|
|
|
|
|
|
|
async function getAppVersion(url, version) {
|
|
|
|
|
assert.strictEqual(typeof url, 'string');
|
|
|
|
|
assert.strictEqual(typeof version, 'string');
|
|
|
|
|
|
|
|
|
|
if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'URL must use HTTPS');
|
|
|
|
|
|
|
|
|
|
const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));
|
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
|
|
|
|
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
|
|
|
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
|
|
|
|
|
|
|
|
|
|
const versions = response.body;
|
|
|
|
|
if (!versions || typeof versions !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid CloudronVersions.json format');
|
|
|
|
|
|
|
|
|
|
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
|
|
|
const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];
|
|
|
|
|
if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);
|
|
|
|
|
|
|
|
|
|
const manifestError = manifestFormat.checkVersionsRequirements(versionData.manifest);
|
|
|
|
|
if (manifestError) throw new BoxError(BoxError.BAD_FIELD, `Invalid manifest: ${manifestError.message}`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: versionData.manifest.id,
|
|
|
|
|
iconUrl: versionData.manifest.iconUrl,
|
|
|
|
|
...versionData // { manifest, publishState, creationDate, ts }
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-05 17:29:00 +01:00
|
|
|
|
|
|
|
|
async function downloadManifest(versionsUrl) {
|
|
|
|
|
assert.strictEqual(typeof versionsUrl, 'string');
|
|
|
|
|
|
|
|
|
|
const atIndex = versionsUrl.lastIndexOf('@');
|
|
|
|
|
if (atIndex === -1) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');
|
|
|
|
|
|
|
|
|
|
const url = versionsUrl.substring(0, atIndex);
|
|
|
|
|
const version = versionsUrl.substring(atIndex + 1);
|
|
|
|
|
|
|
|
|
|
if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl must use https');
|
|
|
|
|
if (!version) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');
|
|
|
|
|
|
|
|
|
|
debug(`downloading manifest from ${url} version ${version}`);
|
|
|
|
|
|
|
|
|
|
const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));
|
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
|
|
|
|
|
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
|
|
|
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
|
|
|
|
|
|
|
|
|
|
const versions = response.body;
|
|
|
|
|
if (!versions || typeof versions !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid CloudronVersions.json format');
|
|
|
|
|
|
|
|
|
|
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
|
|
|
const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];
|
|
|
|
|
|
|
|
|
|
if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);
|
|
|
|
|
if (!versionData.manifest || typeof versionData.manifest !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Missing manifest in version data');
|
|
|
|
|
|
|
|
|
|
return { versionsUrl: url, manifest: versionData.manifest };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getAppUpdate(app, options) {
|
|
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
|
|
|
|
|
const [error, response] = await safe(superagent.get(app.versionsUrl).timeout(60 * 1000).ok(() => true));
|
|
|
|
|
if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
|
|
|
|
|
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
|
|
|
|
|
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
|
|
|
|
|
|
|
|
|
|
const versions = response.body;
|
|
|
|
|
if (!versions || typeof versions !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid CloudronVersions.json format');
|
|
|
|
|
|
|
|
|
|
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
|
|
|
const idx = sortedVersions.findIndex(v => v === app.manifest.version);
|
|
|
|
|
if (idx === -1) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No such version')
|
|
|
|
|
if (idx === sortedVersions.length-1) return null; // no update
|
|
|
|
|
const nextVersion = versions[sortedVersions[idx+1]];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: app.manifest.id,
|
|
|
|
|
creationDate: nextVersion.creationDate,
|
|
|
|
|
manifest: nextVersion.manifest,
|
|
|
|
|
unstable: nextVersion.publishState === 'approved'
|
|
|
|
|
};
|
|
|
|
|
}
|