Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import apps from './apps.js' ;
import assert from 'node:assert' ;
2026-02-14 15:43:24 +01:00
import backupSites from './backupsites.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import BoxError from './boxerror.js' ;
import constants from './constants.js' ;
2026-02-14 15:43:24 +01:00
import dashboard from './dashboard.js' ;
2026-03-12 22:55:28 +05:30
import logger from './logger.js' ;
2026-02-14 15:43:24 +01:00
import domains from './domains.js' ;
import dockerRegistries from './dockerregistries.js' ;
2026-02-21 12:05:56 +01:00
import directoryServer from './directoryserver.js' ;
2026-02-14 15:43:24 +01:00
import externalLdap from './externalldap.js' ;
import groups from './groups.js' ;
import mail from './mail.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import manifestFormat from '@cloudron/manifest-format' ;
2026-02-21 12:05:56 +01:00
import oidcClients from './oidcclients.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import paths from './paths.js' ;
import promiseRetry from './promise-retry.js' ;
import safe from 'safetydance' ;
import semver from 'semver' ;
2026-02-14 15:43:24 +01:00
import settings from './settings.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import superagent from '@cloudron/superagent' ;
2026-02-14 15:43:24 +01:00
import system from './system.js' ;
import users from './users.js' ;
import volumes from './volumes.js' ;
2017-04-13 00:42:44 -07:00
2026-03-12 22:55:28 +05:30
const { log , trace } = logger ( 'appstore' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01: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-10-01 14:35:14 +02:00
appUpdates : false ,
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-10-01 16:44:53 +02:00
async function getState ( ) {
2025-12-05 12:54:48 +01:00
const mailDomains = await mail . listDomains ( ) ;
const mailStats = await Promise . all ( mailDomains . map ( d => mail . getStats ( d . domain ) ) ) ;
2026-02-21 12:05:56 +01:00
const allUsers = await users . list ( ) ;
const roleCounts = allUsers . reduce ( ( acc , u ) => {
acc [ u . role ] = ( acc [ u . role ] || 0 ) + 1 ;
return acc ;
} , { } ) ;
2025-12-05 12:54:48 +01:00
2025-10-01 16:44:53 +02:00
const state = {
2026-01-29 11:46:57 +01:00
provider : system . getProvider ( ) ,
2026-02-21 12:05:56 +01:00
users : { count : allUsers . length , roleCounts } ,
2025-12-05 12:54:48 +01:00
groupCount : ( await groups . list ( ) ) . length ,
domains : ( await domains . list ( ) ) . map ( d => d . provider ) ,
mail : {
incomingCount : mailDomains . filter ( md => md . enabled ) . length ,
catchAllCount : mailDomains . filter ( md => md . catchAll . length ) . length ,
bannerCount : mailDomains . filter ( md => md . banner . text || md . banner . html ) . length ,
mailboxCount : mailStats . reduce ( ( acc , cur ) => acc + cur . mailboxCount , 0 ) ,
mailingListCount : mailStats . reduce ( ( acc , cur ) => acc + cur . mailingListCount , 0 ) ,
pop3Count : mailStats . reduce ( ( acc , cur ) => acc + cur . pop3Count , 0 ) ,
aliasCount : mailStats . reduce ( ( acc , cur ) => acc + cur . aliasCount , 0 )
} ,
2026-02-21 12:05:56 +01:00
apps : ( await apps . list ( ) ) . map ( a => { return { id : a . manifest . id , community : ! ! a . versionsUrl } ; } ) ,
2025-12-05 12:54:48 +01:00
dockerRegistries : ( await dockerRegistries . list ( ) ) . map ( r => r . provider ) ,
backupSites : ( await backupSites . list ( ) ) . map ( s => { return { provider : s . provider , format : s . format , encryption : ! ! s . encryption } ; } ) ,
externalLdap : ( await externalLdap . getConfig ( ) ) . provider ,
volumes : ( await volumes . list ( ) ) . map ( v => v . mountType ) ,
2026-02-21 12:05:56 +01:00
directoryServer : ( await directoryServer . getConfig ( ) ) . enabled ,
oidcClientCount : ( await oidcClients . list ( ) ) . length
2025-09-29 14:44:42 +02:00
} ;
2025-10-01 16:44:53 +02:00
return state ;
2025-09-29 14:44:42 +02:00
}
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
2025-12-05 12:54:48 +01:00
const [ stateError , state ] = await safe ( getState ( ) ) ;
2026-03-12 22:55:28 +05:30
if ( stateError ) log ( 'getSubscription: error getting current state' , stateError ) ;
2025-10-01 16:44:53 +02:00
2025-10-01 17:16:21 +02:00
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/subscription3 ` )
2021-08-18 15:54:53 -07:00
. query ( { accessToken : token } )
2025-10-01 17:16:21 +02:00
. send ( { state } )
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
2025-09-24 22:11:43 +02:00
// { email, emailVerified, cloudronId, cloudronCreatedAt, plan: { id, name }, canceled_at, status, externalCustomer, features: {} }
2021-08-18 15:54:53 -07:00
return response . body ;
2017-06-21 22:17:32 -07:00
}
2025-10-02 11:18:42 +02:00
// cron hook
async function checkSubscription ( ) {
const [ error , result ] = await safe ( getSubscription ( ) ) ;
2026-03-12 22:55:28 +05:30
if ( error ) log ( 'checkSubscription error:' , error ) ;
else log ( ` checkSubscription: Cloudron ${ result . cloudronId } is on the ${ result . plan . name } plan. ` ) ;
2025-10-02 11:18:42 +02: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
2026-01-06 15:10:15 +01:00
if ( ! semver . valid ( updateInfo . version ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Offered version ${ updateInfo . version } is invalid ` ) ;
if ( semver . gt ( constants . VERSION , updateInfo . version ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Offered version ${ updateInfo . version } would be a downgrade ` ) ;
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' ) ) ) {
2026-03-12 22:55:28 +05:30
log ( '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
2026-02-14 16:34:34 +01:00
async function updateCloudron ( data ) {
assert . strictEqual ( typeof data , 'object' ) ;
const { domain , version } = data ;
const token = await settings . get ( settings . APPSTORE _API _TOKEN _KEY ) ;
if ( ! token ) throw new BoxError ( BoxError . LICENSE _ERROR , 'Missing token' ) ;
const query = {
accessToken : token
} ;
const [ error , response ] = await safe ( superagent . post ( ` ${ await getApiServerOrigin ( ) } /api/v1/update_cloudron ` )
. query ( query )
. 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 , 'Invalid appstore token' ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response: ${ response . status } ${ response . text } ` ) ;
2026-03-12 22:55:28 +05:30
log ( ` updateCloudron: Cloudron updated with data ${ JSON . stringify ( data ) } ` ) ;
2026-02-14 16:34:34 +01: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
2026-03-12 22:55:28 +05:30
log ( 'registerCloudron3: already registered. Just updating the record.' ) ;
2025-10-02 11:18:42 +02:00
await getSubscription ( ) ;
2025-08-20 17:50:43 +02:00
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
2026-03-12 22:55:28 +05:30
log ( ` registerCloudron3: Cloudron registered with id ${ response . body . cloudronId } ` ) ;
2025-10-02 11:18:42 +02:00
await getSubscription ( ) ;
2025-05-12 21:56:32 +02:00
}
2026-02-14 16:34:34 +01:00
async function unregister ( ) {
await settings . set ( settings . CLOUDRON _ID _KEY , '' ) ;
await settings . set ( settings . APPSTORE _API _TOKEN _KEY , '' ) ;
}
2025-09-24 21:25:31 +02:00
async function unlinkAccount ( ) {
2026-03-12 22:55:28 +05:30
log ( 'unlinkAccount: Unlinking existing account.' ) ;
2025-09-24 21:25:31 +02:00
2025-10-31 08:47:05 +01:00
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
2025-09-24 21:25:31 +02:00
await unregister ( ) ;
return await registerCloudron3 ( ) ;
}
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
2026-03-12 22:55:28 +05:30
log ( ` downloading manifest from ${ url } ` ) ;
2023-08-04 15:34:38 +05:30
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
2026-02-06 18:04:47 +01:00
return response . body ; // { id, creationDate, publishState, manifest, iconUrl }
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 ` ;
2026-03-12 22:55:28 +05:30
return await promiseRetry ( { times : 10 , interval : 5000 , debug : log } , async function ( ) {
2023-08-04 15:34:38 +05:30
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 ;
} ) ;
}
2026-02-14 15:43:24 +01:00
2026-02-14 16:34:34 +01:00
const _setApiServerOrigin = setApiServerOrigin ;
2026-02-14 15:43:24 +01:00
export default {
getFeatures ,
getApiServerOrigin ,
getWebServerOrigin ,
getConsoleServerOrigin ,
downloadManifest ,
getApps ,
getApp ,
getAppVersion ,
downloadIcon ,
registerCloudron3 ,
updateCloudron ,
unlinkAccount ,
getSubscription ,
checkSubscription , // cron hook,
isFreePlan ,
getAppUpdate ,
getBoxUpdate ,
_setApiServerOrigin ,
2026-02-14 16:34:34 +01:00
_unregister : unregister ,
2026-02-14 15:43:24 +01:00
} ;