2026-02-14 15:43:24 +01:00
import appTaskManager from './apptaskmanager.js' ;
import appstore from './appstore.js' ;
import archives from './archives.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 assert from 'node:assert' ;
import backups from './backups.js' ;
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' ;
import crypto from 'node:crypto' ;
import { CronTime } from 'cron' ;
2026-02-14 15:43:24 +01:00
import dashboard from './dashboard.js' ;
import database from './database.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 debugModule from 'debug' ;
2026-02-14 15:43:24 +01:00
import dns from './dns.js' ;
import docker from './docker.js' ;
import domains from './domains.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 eventlog from './eventlog.js' ;
import fs from 'node:fs' ;
2026-02-14 19:01:00 +01:00
import fileUtils from './file-utils.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 Location from './location.js' ;
import locks from './locks.js' ;
2026-02-14 15:43:24 +01:00
import logs from './logs.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' ;
import mysql from 'mysql2' ;
2026-02-14 15:43:24 +01:00
import notifications from './notifications.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 once from './once.js' ;
import path from 'node:path' ;
import paths from './paths.js' ;
import { PassThrough } from 'node:stream' ;
2026-02-14 15:43:24 +01:00
import reverseProxy from './reverseproxy.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 safe from 'safetydance' ;
import semver from 'semver' ;
import services from './services.js' ;
import shellModule from './shell.js' ;
import tasks from './tasks.js' ;
import { Transform as TransformStream } from 'node:stream' ;
2026-02-14 15:43:24 +01:00
import users from './users.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 util from 'node:util' ;
2026-02-14 15:43:24 +01:00
import volumes from './volumes.js' ;
import _ from './underscore.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
const debug = debugModule ( 'box:apps' ) ;
const shell = shellModule ( 'apps' ) ;
const PORT _TYPE _TCP = 'tcp' ;
const PORT _TYPE _UDP = 'udp' ;
const ISTATE _PENDING _INSTALL = 'pending_install' ;
const ISTATE _PENDING _CLONE = 'pending_clone' ;
const ISTATE _PENDING _CONFIGURE = 'pending_configure' ;
const ISTATE _PENDING _RECREATE _CONTAINER = 'pending_recreate_container' ;
const ISTATE _PENDING _LOCATION _CHANGE = 'pending_location_change' ;
const ISTATE _PENDING _SERVICES _CHANGE = 'pending_services_change' ;
const ISTATE _PENDING _DATA _DIR _MIGRATION = 'pending_data_dir_migration' ;
const ISTATE _PENDING _RESIZE = 'pending_resize' ;
const ISTATE _PENDING _DEBUG = 'pending_debug' ;
const ISTATE _PENDING _UNINSTALL = 'pending_uninstall' ;
const ISTATE _PENDING _RESTORE = 'pending_restore' ;
const ISTATE _PENDING _IMPORT = 'pending_import' ;
const ISTATE _PENDING _UPDATE = 'pending_update' ;
const ISTATE _PENDING _START = 'pending_start' ;
const ISTATE _PENDING _STOP = 'pending_stop' ;
const ISTATE _PENDING _RESTART = 'pending_restart' ;
const ISTATE _ERROR = 'error' ;
const ISTATE _INSTALLED = 'installed' ;
const RSTATE _RUNNING = 'running' ;
const RSTATE _STOPPED = 'stopped' ;
const ACCESS _LEVEL _ADMIN = 'admin' ;
const ACCESS _LEVEL _OPERATOR = 'operator' ;
const ACCESS _LEVEL _USER = 'user' ;
const ACCESS _LEVEL _NONE = '' ;
2015-07-20 00:09:47 -07:00
2024-12-10 17:16:06 +01:00
// NOTE: when adding fields here, update the clone and unarchive logic as well
2026-02-05 17:29:00 +01:00
const APPS _FIELDS _PREFIXED = [ 'apps.id' , 'apps.appStoreId' , 'apps.versionsUrl' , 'apps.installationState' , 'apps.errorJson' , 'apps.runState' ,
2024-04-10 17:38:49 +02:00
'apps.health' , 'apps.containerId' , 'apps.manifestJson' , 'apps.accessRestrictionJson' , 'apps.memoryLimit' , 'apps.cpuQuota' ,
2024-04-10 17:02:32 +02:00
'apps.label' , 'apps.notes' , 'apps.tagsJson' , 'apps.taskId' , 'apps.reverseProxyConfigJson' , 'apps.servicesConfigJson' , 'apps.operatorsJson' ,
2024-12-05 13:47:59 +01:00
'apps.sso' , 'apps.devicesJson' , 'apps.debugModeJson' , 'apps.enableBackup' , 'apps.proxyAuth' , 'apps.containerIp' , 'apps.crontab' ,
2025-06-26 15:19:28 +02:00
'apps.creationTime' , 'apps.updateTime' , 'apps.enableAutomaticUpdate' , 'apps.upstreamUri' , 'apps.checklistJson' , 'apps.updateInfoJson' ,
2022-05-31 17:53:09 -07:00
'apps.enableMailbox' , 'apps.mailboxDisplayName' , 'apps.mailboxName' , 'apps.mailboxDomain' , 'apps.enableInbox' , 'apps.inboxName' , 'apps.inboxDomain' ,
2026-02-06 18:45:40 +01:00
'apps.enableTurn' , 'apps.enableRedis' , 'apps.storageVolumeId' , 'apps.storageVolumePrefix' , 'apps.ts' , 'apps.healthTime' , '(apps.icon IS NOT NULL) AS hasIcon' , '(apps.packageIcon IS NOT NULL) AS hasPackageIcon' ] . join ( ',' ) ;
2021-08-20 09:19:44 -07:00
2024-07-15 22:59:16 +02:00
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId', 'count' ].join(',');
2022-11-28 22:32:34 +01:00
const LOCATION _FIELDS = [ 'appId' , 'subdomain' , 'domain' , 'type' , 'certificateJson' ] ;
2019-08-27 22:39:59 -07:00
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
const CHECKVOLUME _CMD = path . join ( import . meta . dirname , 'scripts/checkvolume.sh' ) ;
2022-06-03 10:56:50 -07:00
2024-07-16 22:21:36 +02:00
// ports is a map of envvar -> hostPort
function translateToPortBindings ( ports , manifest ) {
assert . strictEqual ( typeof ports , 'object' ) ;
2018-08-13 08:33:09 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2024-07-16 22:21:36 +02:00
const portBindings = { } ;
2018-08-12 19:33:11 -07:00
2024-07-16 22:21:36 +02:00
if ( ! ports ) return portBindings ;
2018-08-13 08:33:09 -07:00
2024-07-16 22:21:36 +02:00
const tcpPorts = manifest . tcpPorts || { } ;
for ( const portName in ports ) {
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
const portType = portName in tcpPorts ? PORT _TYPE _TCP : PORT _TYPE _UDP ;
2024-07-16 22:21:36 +02:00
const portCount = portName in tcpPorts ? tcpPorts [ portName ] . portCount : manifest . udpPorts [ portName ] . portCount ; // since count is optional, this can be undefined
portBindings [ portName ] = { hostPort : ports [ portName ] , type : portType , count : portCount || 1 } ;
2018-08-12 19:33:11 -07:00
}
2024-02-25 14:33:57 +01:00
2024-07-16 22:21:36 +02:00
return portBindings ;
2018-08-12 19:33:11 -07:00
}
2026-02-14 16:34:34 +01:00
// translates the REST API ports (envvar -> hostPort) to database portBindings (envvar -> { hostPort, count, type })
2022-01-14 22:40:51 -08:00
function validateSecondaryDomains ( secondaryDomains , manifest ) {
assert . strictEqual ( typeof secondaryDomains , 'object' ) ;
assert . strictEqual ( typeof manifest , 'object' ) ;
const httpPorts = manifest . httpPorts || { } ;
2022-01-20 16:57:30 -08:00
for ( const envName in httpPorts ) { // each httpPort is required
if ( ! ( envName in secondaryDomains ) ) return new BoxError ( BoxError . BAD _FIELD , ` secondaryDomain ${ envName } is required ` ) ;
}
2022-01-14 22:40:51 -08:00
for ( const envName in secondaryDomains ) {
2022-01-25 16:41:29 -08:00
if ( ! ( envName in httpPorts ) ) return new BoxError ( BoxError . BAD _FIELD , ` secondaryDomain ${ envName } is not listed in manifest ` ) ;
2022-01-14 22:40:51 -08:00
}
return null ;
}
function translateSecondaryDomains ( secondaryDomains ) {
2022-01-20 16:57:30 -08:00
assert ( secondaryDomains && typeof secondaryDomains === 'object' ) ;
2022-01-14 22:40:51 -08:00
const result = [ ] ;
for ( const envName in secondaryDomains ) {
2022-01-20 16:57:30 -08:00
result . push ( { domain : secondaryDomains [ envName ] . domain , subdomain : secondaryDomains [ envName ] . subdomain , environmentVariable : envName } ) ;
2022-01-14 22:40:51 -08:00
}
return result ;
}
2021-09-27 14:21:42 -07:00
function parseCrontab ( crontab ) {
assert ( crontab === null || typeof crontab === 'string' ) ;
2022-05-20 09:31:58 -07:00
// https://www.man7.org/linux/man-pages/man5/crontab.5.html#EXTENSIONS
const KNOWN _EXTENSIONS = {
2022-05-20 10:57:44 -07:00
'@service' : '@service' , // runs once
'@reboot' : '@service' ,
2022-05-20 09:31:58 -07:00
'@yearly' : '0 0 1 1 *' ,
'@annually' : '0 0 1 1 *' ,
'@monthly' : '0 0 1 * *' ,
'@weekly' : '0 0 * * 0' ,
'@daily' : '0 0 * * *' ,
'@hourly' : '0 * * * *' ,
} ;
2021-09-27 14:21:42 -07:00
const result = [ ] ;
if ( ! crontab ) return result ;
const lines = crontab . split ( '\n' ) ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] . trim ( ) ;
if ( ! line || line . startsWith ( '#' ) ) continue ;
2022-05-20 09:31:58 -07:00
if ( line . startsWith ( '@' ) ) {
const parts = /^(@\S+)\s+(.+)$/ . exec ( line ) ;
if ( ! parts ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron configuration at line ${ i + 1 } ` ) ;
const [ , extension , command ] = parts ;
if ( ! KNOWN _EXTENSIONS [ extension ] ) throw new BoxError ( BoxError . BAD _FIELD , ` Unknown extension pattern at line ${ i + 1 } ` ) ;
result . push ( { schedule : KNOWN _EXTENSIONS [ extension ] , command } ) ;
} else {
const parts = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/ . exec ( line ) ;
if ( ! parts ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron configuration at line ${ i + 1 } ` ) ;
const schedule = parts . slice ( 1 , 6 ) . join ( ' ' ) ;
const command = parts [ 6 ] ;
try {
2024-04-19 18:19:41 +02:00
new CronTime ( '00 ' + schedule ) ; // second is disallowed
2024-05-13 08:43:28 +02:00
} catch ( error ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron pattern at line ${ i + 1 } : ${ error . message } ` ) ;
2022-05-20 09:31:58 -07:00
}
if ( command . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron pattern. Command must not be empty at line ${ i + 1 } ` ) ; // not possible with the regexp we have
result . push ( { schedule , command } ) ;
2021-09-27 14:21:42 -07:00
}
}
return result ;
}
function getSchedulerConfig ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
let schedulerConfig = app . manifest . addons ? . scheduler || null ;
const crontab = parseCrontab ( app . crontab ) ;
if ( crontab . length === 0 ) return schedulerConfig ;
schedulerConfig = schedulerConfig || { } ;
// put a '.' because it is not a valid name for schedule name in manifestformat
crontab . forEach ( ( c , idx ) => schedulerConfig [ ` crontab. ${ idx } ` ] = c ) ;
return schedulerConfig ;
}
2015-10-15 12:26:48 +02:00
function validateAccessRestriction ( accessRestriction ) {
2015-10-16 15:11:54 +02:00
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
2015-10-15 12:26:48 +02:00
2015-10-16 15:11:54 +02:00
if ( accessRestriction === null ) return null ;
2015-10-15 12:26:48 +02:00
2016-02-09 13:03:52 -08:00
if ( accessRestriction . users ) {
2019-10-24 10:39:47 -07:00
if ( ! Array . isArray ( accessRestriction . users ) ) return new BoxError ( BoxError . BAD _FIELD , 'users array property required' ) ;
if ( ! accessRestriction . users . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new BoxError ( BoxError . BAD _FIELD , 'All users have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
if ( accessRestriction . groups ) {
2019-10-24 10:39:47 -07:00
if ( ! Array . isArray ( accessRestriction . groups ) ) return new BoxError ( BoxError . BAD _FIELD , 'groups array property required' ) ;
if ( ! accessRestriction . groups . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new BoxError ( BoxError . BAD _FIELD , 'All groups have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
2016-06-04 13:20:10 -07:00
// TODO: maybe validate if the users and groups actually exist
2015-10-15 12:26:48 +02:00
return null ;
}
2026-02-14 16:34:34 +01:00
// also validates operators
2024-04-10 17:38:49 +02:00
function validateCpuQuota ( cpuQuota ) {
assert . strictEqual ( typeof cpuQuota , 'number' ) ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
if ( cpuQuota < 1 || cpuQuota > 100 ) return new BoxError ( BoxError . BAD _FIELD , 'cpuQuota has to be between 1 and 100' ) ;
2020-01-28 21:30:35 -08:00
return null ;
}
2017-01-20 05:48:25 -08:00
function validateDebugMode ( debugMode ) {
assert . strictEqual ( typeof debugMode , 'object' ) ;
if ( debugMode === null ) return null ;
2019-10-24 10:39:47 -07:00
if ( 'cmd' in debugMode && debugMode . cmd !== null && ! Array . isArray ( debugMode . cmd ) ) return new BoxError ( BoxError . BAD _FIELD , 'debugMode.cmd must be an array or null' ) ;
if ( 'readonlyRootfs' in debugMode && typeof debugMode . readonlyRootfs !== 'boolean' ) return new BoxError ( BoxError . BAD _FIELD , 'debugMode.readonlyRootfs must be a boolean' ) ;
2017-01-20 05:48:25 -08:00
return null ;
}
2017-07-14 12:19:27 -05:00
function validateRobotsTxt ( robotsTxt ) {
if ( robotsTxt === null ) return null ;
2017-07-23 18:55:31 -07:00
// this is the nginx limit on inline strings. if we really hit this, we have to generate a file
2022-02-07 13:19:59 -08:00
if ( robotsTxt . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'robotsTxt must be less than 4096' ) ;
2017-07-23 18:55:31 -07:00
// TODO: validate the robots file? we escape the string when templating the nginx config right now
2017-07-14 12:19:27 -05:00
return null ;
}
2019-10-14 16:59:22 -07:00
function validateCsp ( csp ) {
if ( csp === null ) return null ;
2019-10-13 18:22:03 -07:00
2022-02-07 13:19:59 -08:00
if ( csp . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'CSP must be less than 4096' ) ;
if ( csp . includes ( '"' ) ) return new BoxError ( BoxError . BAD _FIELD , 'CSP cannot contains double quotes' ) ;
2019-10-13 18:22:03 -07:00
return null ;
}
2022-06-08 11:21:09 +02:00
function validateUpstreamUri ( upstreamUri ) {
2022-06-10 11:23:58 -07:00
assert . strictEqual ( typeof upstreamUri , 'string' ) ;
2022-06-08 11:21:09 +02:00
2022-11-23 12:53:21 +01:00
if ( ! upstreamUri ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot be empty' ) ;
2022-09-29 18:39:58 +02:00
const uri = safe ( ( ) => new URL ( upstreamUri ) ) ;
if ( ! uri ) return new BoxError ( BoxError . BAD _FIELD , ` upstreamUri is invalid: ${ safe . error . message } ` ) ;
if ( uri . protocol !== 'http:' && uri . protocol !== 'https:' ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri has an unsupported scheme' ) ;
if ( uri . search || uri . hash ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have search or hash' ) ;
if ( uri . pathname !== '/' ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have a path' ) ;
// we use the uri in a named location @wellknown-upstream. nginx does not support having paths in it
if ( upstreamUri . endsWith ( '/' ) ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have a path' ) ;
2022-06-08 11:21:09 +02:00
return null ;
}
2019-03-22 07:48:31 -07:00
function validateLabel ( label ) {
if ( label === null ) return null ;
2022-02-07 13:19:59 -08:00
if ( label . length > 128 ) return new BoxError ( BoxError . BAD _FIELD , 'label must be less than 128' ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
function validateTags ( tags ) {
2022-02-07 13:19:59 -08:00
if ( tags . length > 64 ) return new BoxError ( BoxError . BAD _FIELD , 'Can only set up to 64 tags' ) ;
2019-03-22 07:48:31 -07:00
2022-02-07 13:19:59 -08:00
if ( tags . some ( tag => tag . length == 0 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag cannot be empty' ) ;
if ( tags . some ( tag => tag . length > 128 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag must be less than 128' ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
2018-10-18 11:19:32 -07:00
function validateEnv ( env ) {
2024-05-13 08:43:28 +02:00
for ( const key in env ) {
2022-02-07 13:19:59 -08:00
if ( key . length > 512 ) return new BoxError ( BoxError . BAD _FIELD , 'Max env var key length is 512' ) ;
2018-10-18 11:19:32 -07:00
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
2022-02-07 13:19:59 -08:00
if ( ! /^[a-zA-Z_][a-zA-Z0-9_]*$/ . test ( key ) ) return new BoxError ( BoxError . BAD _FIELD , ` " ${ key } " is not a valid environment variable ` ) ;
2018-10-18 11:19:32 -07:00
}
return null ;
}
2022-11-28 22:14:10 +01:00
function getDuplicateErrorDetails ( errorMessage , locations , portBindings ) {
2019-08-29 12:14:42 -07:00
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2019-09-27 10:25:26 -07:00
assert ( Array . isArray ( locations ) ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2025-09-24 10:50:25 +02:00
const match = errorMessage . match ( /Duplicate entry '(.*)' for key '(.*)'/ ) ;
2015-07-20 00:09:47 -07:00
if ( ! match ) {
2019-08-29 12:14:42 -07:00
debug ( 'Unexpected SQL error message.' , errorMessage ) ;
2019-10-24 18:32:33 -07:00
return new BoxError ( BoxError . DATABASE _ERROR , errorMessage ) ;
2015-07-20 00:09:47 -07:00
}
2019-09-27 10:25:26 -07:00
// check if a location conflicts
2022-02-07 16:09:43 -08:00
if ( match [ 2 ] === 'locations.subdomain' ) {
2019-09-27 10:25:26 -07:00
for ( let i = 0 ; i < locations . length ; i ++ ) {
2022-02-07 16:09:43 -08:00
const { subdomain , domain , type } = locations [ i ] ;
2023-01-09 13:27:02 +01:00
if ( match [ 1 ] !== ( subdomain ? ` ${ subdomain } - ${ domain } ` : domain ) ) continue ;
2019-06-05 16:01:44 +02:00
2022-11-28 21:23:06 +01:00
return new BoxError ( BoxError . ALREADY _EXISTS , ` ${ type } location ' ${ dns . fqdn ( subdomain , domain ) } ' is in use ` ) ;
2019-09-01 21:34:27 -07:00
}
2019-03-19 20:43:42 -07:00
}
2015-07-20 00:09:47 -07:00
2022-02-07 16:09:43 -08:00
for ( const portName in portBindings ) {
2024-07-16 22:21:36 +02:00
if ( portBindings [ portName ] . hostPort === parseInt ( match [ 1 ] ) ) return new BoxError ( BoxError . ALREADY _EXISTS , ` Port ${ match [ 1 ] } is in use ` ) ;
2015-07-20 00:09:47 -07:00
}
2022-06-01 22:44:52 -07:00
if ( match [ 2 ] === 'apps_storageVolume' ) {
return new BoxError ( BoxError . BAD _FIELD , ` Storage directory ${ match [ 1 ] } is in use ` ) ;
2019-09-03 15:17:48 -07:00
}
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . ALREADY _EXISTS , ` ${ match [ 2 ] } ' ${ match [ 1 ] } ' is in use ` ) ;
2015-07-20 00:09:47 -07:00
}
2021-01-20 12:12:14 -08:00
function getMemoryLimit ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
let memoryLimit = app . memoryLimit || app . manifest . memoryLimit || 0 ;
if ( memoryLimit === - 1 ) { // unrestricted
memoryLimit = 0 ;
} else if ( memoryLimit === 0 || memoryLimit < constants . DEFAULT _MEMORY _LIMIT ) { // ensure we never go below minimum (in case we change the default)
memoryLimit = constants . DEFAULT _MEMORY _LIMIT ;
}
return memoryLimit ;
}
2021-08-20 09:19:44 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
assert ( result . manifestJson === null || typeof result . manifestJson === 'string' ) ;
result . manifest = safe . JSON . parse ( result . manifestJson ) ;
delete result . manifestJson ;
assert ( result . tagsJson === null || typeof result . tagsJson === 'string' ) ;
result . tags = safe . JSON . parse ( result . tagsJson ) || [ ] ;
delete result . tagsJson ;
2024-04-17 16:54:54 +02:00
assert ( result . checklistJson === null || typeof result . checklistJson === 'string' ) ;
result . checklist = safe . JSON . parse ( result . checklistJson ) || { } ;
delete result . checklistJson ;
2025-06-26 15:19:28 +02:00
assert ( result . updateInfoJson === null || typeof result . updateInfoJson === 'string' ) ;
result . updateInfo = safe . JSON . parse ( result . updateInfoJson ) || null ;
delete result . updateInfoJson ;
2021-08-20 09:19:44 -07:00
assert ( result . reverseProxyConfigJson === null || typeof result . reverseProxyConfigJson === 'string' ) ;
result . reverseProxyConfig = safe . JSON . parse ( result . reverseProxyConfigJson ) || { } ;
delete result . reverseProxyConfigJson ;
assert ( result . hostPorts === null || typeof result . hostPorts === 'string' ) ;
assert ( result . environmentVariables === null || typeof result . environmentVariables === 'string' ) ;
2024-07-16 22:21:36 +02:00
result . portBindings = { } ;
2024-05-13 08:43:28 +02:00
const hostPorts = result . hostPorts === null ? [ ] : result . hostPorts . split ( ',' ) ;
const environmentVariables = result . environmentVariables === null ? [ ] : result . environmentVariables . split ( ',' ) ;
const portTypes = result . portTypes === null ? [ ] : result . portTypes . split ( ',' ) ;
2024-07-15 22:59:16 +02:00
const portCounts = result . portCounts === null ? [ ] : result . portCounts . split ( ',' ) ;
2021-08-20 09:19:44 -07:00
delete result . hostPorts ;
delete result . environmentVariables ;
delete result . portTypes ;
2024-07-15 22:59:16 +02:00
delete result . portCounts ;
2021-08-20 09:19:44 -07:00
for ( let i = 0 ; i < environmentVariables . length ; i ++ ) {
2024-07-16 22:21:36 +02:00
result . portBindings [ environmentVariables [ i ] ] = { hostPort : parseInt ( hostPorts [ i ] , 10 ) , type : portTypes [ i ] , count : parseInt ( portCounts [ i ] , 10 ) } ;
2021-08-20 09:19:44 -07:00
}
assert ( result . accessRestrictionJson === null || typeof result . accessRestrictionJson === 'string' ) ;
result . accessRestriction = safe . JSON . parse ( result . accessRestrictionJson ) ;
if ( result . accessRestriction && ! result . accessRestriction . users ) result . accessRestriction . users = [ ] ;
delete result . accessRestrictionJson ;
2021-09-21 10:11:27 -07:00
result . operators = safe . JSON . parse ( result . operatorsJson ) ;
if ( result . operators && ! result . operators . users ) result . operators . users = [ ] ;
delete result . operatorsJson ;
2021-08-20 09:19:44 -07:00
result . sso = ! ! result . sso ;
result . enableBackup = ! ! result . enableBackup ;
result . enableAutomaticUpdate = ! ! result . enableAutomaticUpdate ;
result . enableMailbox = ! ! result . enableMailbox ;
2021-10-01 12:09:13 -07:00
result . enableInbox = ! ! result . enableInbox ;
2021-08-20 09:19:44 -07:00
result . proxyAuth = ! ! result . proxyAuth ;
result . hasIcon = ! ! result . hasIcon ;
2026-02-06 18:45:40 +01:00
result . hasPackageIcon = ! ! result . hasPackageIcon ;
2021-08-20 09:19:44 -07:00
assert ( result . debugModeJson === null || typeof result . debugModeJson === 'string' ) ;
result . debugMode = safe . JSON . parse ( result . debugModeJson ) ;
delete result . debugModeJson ;
assert ( result . servicesConfigJson === null || typeof result . servicesConfigJson === 'string' ) ;
result . servicesConfig = safe . JSON . parse ( result . servicesConfigJson ) || { } ;
delete result . servicesConfigJson ;
2022-01-14 22:40:51 -08:00
const subdomains = JSON . parse ( result . subdomains ) ,
domains = JSON . parse ( result . domains ) ,
subdomainTypes = JSON . parse ( result . subdomainTypes ) ,
2022-07-14 11:57:04 +05:30
subdomainEnvironmentVariables = JSON . parse ( result . subdomainEnvironmentVariables ) ,
subdomainCertificateJsons = JSON . parse ( result . subdomainCertificateJsons ) ;
2022-01-14 22:40:51 -08:00
2021-08-20 09:19:44 -07:00
delete result . subdomains ;
delete result . domains ;
delete result . subdomainTypes ;
2022-01-14 22:40:51 -08:00
delete result . subdomainEnvironmentVariables ;
2022-07-14 11:57:04 +05:30
delete result . subdomainCertificateJsons ;
2021-08-20 09:19:44 -07:00
2022-01-14 22:40:51 -08:00
result . secondaryDomains = [ ] ;
2022-01-14 22:29:47 -08:00
result . redirectDomains = [ ] ;
2021-08-20 09:19:44 -07:00
result . aliasDomains = [ ] ;
for ( let i = 0 ; i < subdomainTypes . length ; i ++ ) {
2022-07-14 11:57:04 +05:30
const subdomain = subdomains [ i ] , domain = domains [ i ] , certificate = safe . JSON . parse ( subdomainCertificateJsons [ i ] ) ;
2023-08-17 16:05:19 +05:30
if ( subdomainTypes [ i ] === Location . TYPE _PRIMARY ) {
2022-07-14 11:57:04 +05:30
result . subdomain = subdomain ;
result . domain = domain ;
result . certificate = certificate ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _SECONDARY ) {
2022-07-14 11:57:04 +05:30
result . secondaryDomains . push ( { domain , subdomain , certificate , environmentVariable : subdomainEnvironmentVariables [ i ] } ) ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _REDIRECT ) {
2022-07-14 11:57:04 +05:30
result . redirectDomains . push ( { domain , subdomain , certificate } ) ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _ALIAS ) {
2022-07-14 11:57:04 +05:30
result . aliasDomains . push ( { domain , subdomain , certificate } ) ;
2021-08-20 09:19:44 -07:00
}
}
2022-07-14 11:57:04 +05:30
const envNames = JSON . parse ( result . envNames ) , envValues = JSON . parse ( result . envValues ) ;
2021-08-20 09:19:44 -07:00
delete result . envNames ;
delete result . envValues ;
result . env = { } ;
for ( let i = 0 ; i < envNames . length ; i ++ ) { // NOTE: envNames is [ null ] when env of an app is empty
if ( envNames [ i ] ) result . env [ envNames [ i ] ] = envValues [ i ] ;
}
2022-07-14 11:57:04 +05:30
const volumeIds = JSON . parse ( result . volumeIds ) ;
2021-08-20 09:19:44 -07:00
delete result . volumeIds ;
2024-05-13 08:43:28 +02:00
const volumeReadOnlys = JSON . parse ( result . volumeReadOnlys ) ;
2021-08-20 09:19:44 -07:00
delete result . volumeReadOnlys ;
result . mounts = volumeIds [ 0 ] === null ? [ ] : volumeIds . map ( ( v , idx ) => { return { volumeId : v , readOnly : ! ! volumeReadOnlys [ idx ] } ; } ) ; // NOTE: volumeIds is [null] when volumes of an app is empty
result . error = safe . JSON . parse ( result . errorJson ) ;
delete result . errorJson ;
result . taskId = result . taskId ? String ( result . taskId ) : null ;
2024-12-05 13:47:59 +01:00
result . devices = result . devicesJson ? JSON . parse ( result . devicesJson ) : { } ;
delete result . devicesJson ;
2021-08-20 09:19:44 -07:00
}
2021-09-21 10:11:27 -07:00
function isAdmin ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
return users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ;
}
function isOperator ( app , user ) {
2022-10-06 11:24:39 +02:00
assert . strictEqual ( typeof app , 'object' ) ; // IMPORTANT: can also be applink
2015-10-15 15:06:34 +02:00
assert . strictEqual ( typeof user , 'object' ) ;
2021-09-21 10:11:27 -07:00
if ( ! app . operators ) return isAdmin ( user ) ;
2016-02-09 12:48:21 -08:00
2025-02-12 12:36:50 +01:00
if ( app . operators . users . includes ( user . id ) ) return true ;
2021-09-21 10:11:27 -07:00
if ( ! app . operators . groups ) return isAdmin ( user ) ;
2025-02-12 12:36:50 +01:00
if ( app . operators . groups . some ( function ( gid ) { return Array . isArray ( user . groupIds ) && user . groupIds . includes ( gid ) ; } ) ) return true ;
2016-02-09 13:03:52 -08:00
2021-09-21 10:11:27 -07:00
return isAdmin ( user ) ;
}
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
function canAccess ( app , user ) {
2022-10-06 11:24:39 +02:00
assert . strictEqual ( typeof app , 'object' ) ; // IMPORTANT: can also be applink
2021-09-21 10:11:27 -07:00
assert . strictEqual ( typeof user , 'object' ) ;
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
if ( app . accessRestriction === null ) return true ;
2025-02-12 12:36:50 +01:00
if ( app . accessRestriction . users . includes ( user . id ) ) return true ;
2021-09-21 10:11:27 -07:00
if ( ! app . accessRestriction . groups ) return isOperator ( app , user ) ;
2025-02-12 12:36:50 +01:00
if ( app . accessRestriction . groups . some ( function ( gid ) { return Array . isArray ( user . groupIds ) && user . groupIds . includes ( gid ) ; } ) ) return true ;
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
return isOperator ( app , user ) ;
}
function accessLevel ( app , user ) {
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
if ( isAdmin ( user ) ) return ACCESS _LEVEL _ADMIN ;
if ( isOperator ( app , user ) ) return ACCESS _LEVEL _OPERATOR ;
return canAccess ( app , user ) ? ACCESS _LEVEL _USER : ACCESS _LEVEL _NONE ;
2015-10-15 15:06:34 +02:00
}
2026-02-14 16:34:34 +01:00
function pickFields ( app , accessLevel ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof accessLevel , 'string' ) ;
2024-02-28 14:56:04 +01:00
2026-02-14 16:34:34 +01:00
if ( accessLevel === ACCESS _LEVEL _NONE ) return null ; // cannot happen!
2024-02-28 14:56:04 +01:00
2026-02-14 16:34:34 +01:00
let result ;
if ( accessLevel === ACCESS _LEVEL _USER ) {
result = _ . pick ( app , [
'id' , 'appStoreId' , 'versionsUrl' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' , 'accessRestriction' ,
'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'sso' , 'subdomain' , 'domain' , 'fqdn' , 'certificate' ,
'manifest' , 'portBindings' , 'iconUrl' , 'creationTime' , 'ts' , 'tags' , 'label' , 'upstreamUri' ] ) ;
} else { // admin or operator
result = _ . pick ( app , [
'id' , 'appStoreId' , 'versionsUrl' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' ,
'subdomain' , 'domain' , 'fqdn' , 'certificate' , 'crontab' , 'upstreamUri' ,
'accessRestriction' , 'manifest' , 'portBindings' , 'iconUrl' , 'memoryLimit' , 'cpuQuota' , 'operators' ,
'sso' , 'debugMode' , 'reverseProxyConfig' , 'enableBackup' , 'creationTime' , 'updateTime' , 'ts' , 'tags' ,
'label' , 'notes' , 'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'devices' , 'env' , 'enableAutomaticUpdate' ,
'storageVolumeId' , 'storageVolumePrefix' , 'mounts' , 'enableTurn' , 'enableRedis' , 'checklist' ,
'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'updateInfo' ] ) ;
}
// remove private certificate key
if ( result . certificate ) delete result . certificate . key ;
result . secondaryDomains . forEach ( sd => { if ( sd . certificate ) delete sd . certificate . key ; } ) ;
result . aliasDomains . forEach ( ad => { if ( ad . certificate ) delete ad . certificate . key ; } ) ;
result . redirectDomains . forEach ( rd => { if ( rd . certificate ) delete rd . certificate . key ; } ) ;
return result ;
}
async function checkForPortBindingConflict ( portBindings , options ) {
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const existingPortBindings = options . appId
? await database . query ( 'SELECT * FROM appPortBindings WHERE appId != ?' , [ options . appId ] )
: await database . query ( 'SELECT * FROM appPortBindings' , [ ] ) ;
if ( existingPortBindings . length === 0 ) return ;
const tcpPortBindings = existingPortBindings . filter ( ( p ) => p . type === 'tcp' ) ;
const udpPortBindings = existingPortBindings . filter ( ( p ) => p . type === 'udp' ) ;
for ( const portName in portBindings ) {
const portBinding = portBindings [ portName ] ;
const existingPortBinding = portBinding . type === 'tcp' ? tcpPortBindings : udpPortBindings ;
const found = existingPortBinding . find ( ( epb ) => {
2024-02-28 14:56:04 +01:00
// if one is true we dont have a conflict
// a1 <----> a2 b1 <-------> b2
// b1 <------> b2 a1 <-----> a2
2024-07-16 22:21:36 +02:00
const a2 = ( epb . hostPort + epb . count - 1 ) ;
const b1 = portBinding . hostPort ;
const b2 = ( portBinding . hostPort + portBinding . count - 1 ) ;
const a1 = epb . hostPort ;
2024-02-28 14:56:04 +01:00
return ! ( ( a2 < b1 ) || ( b2 < a1 ) ) ;
} ) ;
2024-07-16 22:21:36 +02:00
if ( found ) throw new BoxError ( BoxError . CONFLICT , ` Conflicting ${ portBinding . type } port ${ portBinding . hostPort } ` ) ;
2024-02-28 14:56:04 +01:00
}
}
2026-02-05 17:29:00 +01:00
async function add ( id , appStoreId , versionsUrl , manifest , subdomain , domain , portBindings , data ) {
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof appStoreId , 'string' ) ;
2026-02-05 17:29:00 +01:00
assert . strictEqual ( typeof versionsUrl , 'string' ) ;
2021-08-20 09:19:44 -07:00
assert ( manifest && typeof manifest === 'object' ) ;
assert . strictEqual ( typeof manifest . version , 'string' ) ;
2022-01-16 12:32:12 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2024-07-16 22:21:36 +02:00
assert ( portBindings && typeof portBindings === 'object' ) ;
2021-08-20 09:19:44 -07:00
assert ( data && typeof data === 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-10-01 12:09:13 -07:00
const manifestJson = JSON . stringify ( manifest ) ,
2024-12-10 17:30:23 +01:00
accessRestrictionJson = data . accessRestriction ? JSON . stringify ( data . accessRestriction ) : null ,
operatorsJson = data . operators ? JSON . stringify ( data . operators ) : null ,
2021-10-01 12:09:13 -07:00
memoryLimit = data . memoryLimit || 0 ,
2024-04-10 17:38:49 +02:00
cpuQuota = data . cpuQuota || 100 ,
2021-10-01 12:09:13 -07:00
installationState = data . installationState ,
runState = data . runState ,
sso = 'sso' in data ? data . sso : null ,
debugModeJson = data . debugMode ? JSON . stringify ( data . debugMode ) : null ,
2024-12-05 13:47:59 +01:00
devicesJson = data . devices ? JSON . stringify ( data . devices ) : null ,
2021-10-01 12:09:13 -07:00
env = data . env || { } ,
label = data . label || null ,
tagsJson = data . tags ? JSON . stringify ( data . tags ) : null ,
2024-04-17 16:54:54 +02:00
checklistJson = data . checklist ? JSON . stringify ( data . checklist ) : null ,
2021-10-01 12:09:13 -07:00
mailboxName = data . mailboxName || null ,
mailboxDomain = data . mailboxDomain || null ,
2022-05-31 17:53:09 -07:00
mailboxDisplayName = data . mailboxDisplayName || '' ,
2021-10-01 12:09:13 -07:00
reverseProxyConfigJson = data . reverseProxyConfig ? JSON . stringify ( data . reverseProxyConfig ) : null ,
servicesConfigJson = data . servicesConfig ? JSON . stringify ( data . servicesConfig ) : null ,
enableMailbox = data . enableMailbox || false ,
2022-06-09 14:21:09 +02:00
upstreamUri = data . upstreamUri || '' ,
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data . enableTurn : true ,
2024-12-10 17:30:23 +01:00
icon = data . icon || null ,
notes = data . notes || null ,
crontab = data . crontab || null ,
enableBackup = 'enableBackup' in data ? data . enableBackup : true ,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data . enableAutomaticUpdate : true ;
2021-08-20 09:19:44 -07:00
2025-06-14 10:28:20 +02:00
// when redis is optional, do not enable it by default. it's mostly used for caching in those setups
const enableRedis = 'enableRedis' in data ? data . enableRedis : ! manifest . addons ? . redis ? . optional ;
2024-07-16 22:21:36 +02:00
await checkForPortBindingConflict ( portBindings , { appId : null } ) ;
2024-02-28 14:56:04 +01:00
2021-10-01 12:09:13 -07:00
const queries = [ ] ;
2021-08-20 09:19:44 -07:00
queries . push ( {
2026-02-05 17:29:00 +01:00
query : 'INSERT INTO apps (id, appStoreId, versionsUrl, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
2024-04-17 16:54:54 +02:00
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon, '
2024-12-10 17:30:23 +01:00
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate) '
2026-02-05 17:29:00 +01:00
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
args : [ id , appStoreId , versionsUrl , manifestJson , installationState , runState , accessRestrictionJson , operatorsJson , memoryLimit , cpuQuota ,
2024-04-17 16:54:54 +02:00
sso , debugModeJson , mailboxName , mailboxDomain , label , tagsJson , reverseProxyConfigJson , checklistJson , servicesConfigJson , icon ,
2024-12-10 17:30:23 +01:00
enableMailbox , mailboxDisplayName , upstreamUri , enableTurn , enableRedis , devicesJson , notes , crontab ,
enableBackup , enableAutomaticUpdate
]
2021-08-20 09:19:44 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , domain , subdomain , Location . TYPE _PRIMARY ]
2021-08-20 09:19:44 -07:00
} ) ;
2019-03-06 11:12:39 -08:00
2021-08-20 09:19:44 -07:00
Object . keys ( portBindings ) . forEach ( function ( env ) {
queries . push ( {
2024-02-22 16:42:28 +01:00
query : 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId, count) VALUES (?, ?, ?, ?, ?)' ,
2024-07-16 22:21:36 +02:00
args : [ env , portBindings [ env ] . hostPort , portBindings [ env ] . type , id , portBindings [ env ] . count ]
2021-08-20 09:19:44 -07:00
} ) ;
2019-03-06 11:12:39 -08:00
} ) ;
2021-08-20 09:19:44 -07:00
Object . keys ( env ) . forEach ( function ( name ) {
queries . push ( {
query : 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)' ,
args : [ id , name , env [ name ] ]
} ) ;
} ) ;
2022-01-14 22:40:51 -08:00
if ( data . secondaryDomains ) {
data . secondaryDomains . forEach ( function ( d ) {
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _SECONDARY , d . environmentVariable ]
2022-01-14 22:40:51 -08:00
} ) ;
} ) ;
}
2022-01-14 22:29:47 -08:00
if ( data . redirectDomains ) {
data . redirectDomains . forEach ( function ( d ) {
2021-08-20 09:19:44 -07:00
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _REDIRECT ]
2021-08-20 09:19:44 -07:00
} ) ;
} ) ;
}
if ( data . aliasDomains ) {
data . aliasDomains . forEach ( function ( d ) {
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _ALIAS ]
2021-08-20 09:19:44 -07:00
} ) ;
} ) ;
}
const [ error ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , 'no such domain' ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2019-03-06 11:12:39 -08:00
}
2026-02-14 16:34:34 +01:00
// note: this value cannot be cached as it depends on enableAutomaticUpdate and runState
2021-08-20 09:19:44 -07:00
async function getIcons ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-03-06 11:12:39 -08:00
2026-02-06 18:45:40 +01:00
const results = await database . query ( 'SELECT icon, packageIcon FROM apps WHERE id = ?' , [ id ] ) ;
2021-08-20 09:19:44 -07:00
if ( results . length === 0 ) return null ;
2026-02-06 18:45:40 +01:00
return { icon : results [ 0 ] . icon , packageIcon : results [ 0 ] . packageIcon } ;
2021-08-20 09:19:44 -07:00
}
2019-03-06 11:12:39 -08:00
2026-02-14 16:34:34 +01:00
// attaches computed properties
async function getIcon ( app , options ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const icons = await getIcons ( app . id ) ;
if ( ! icons ) throw new BoxError ( BoxError . NOT _FOUND , 'No such app' ) ;
if ( ! options . original && icons . icon ) return icons . icon ;
if ( icons . packageIcon ) return icons . packageIcon ;
return null ;
}
2021-08-20 09:19:44 -07:00
async function updateWithConstraints ( id , app , constraints ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof constraints , 'string' ) ;
assert ( ! ( 'portBindings' in app ) || typeof app . portBindings === 'object' ) ;
assert ( ! ( 'accessRestriction' in app ) || typeof app . accessRestriction === 'object' || app . accessRestriction === '' ) ;
2022-01-14 22:40:51 -08:00
assert ( ! ( 'secondaryDomains' in app ) || Array . isArray ( app . secondaryDomains ) ) ;
2022-01-14 22:29:47 -08:00
assert ( ! ( 'redirectDomains' in app ) || Array . isArray ( app . redirectDomains ) ) ;
2021-08-20 09:19:44 -07:00
assert ( ! ( 'aliasDomains' in app ) || Array . isArray ( app . aliasDomains ) ) ;
assert ( ! ( 'tags' in app ) || Array . isArray ( app . tags ) ) ;
2024-04-17 16:54:54 +02:00
assert ( ! ( 'checklist' in app ) || typeof app . checklist === 'object' ) ;
2021-08-20 09:19:44 -07:00
assert ( ! ( 'env' in app ) || typeof app . env === 'object' ) ;
const queries = [ ] ;
if ( 'portBindings' in app ) {
2024-07-16 22:21:36 +02:00
const portBindings = app . portBindings ;
2024-02-28 14:56:04 +01:00
2024-07-16 19:31:54 +02:00
await checkForPortBindingConflict ( portBindings , { appId : id } ) ;
2024-02-28 14:56:04 +01:00
2021-08-20 09:19:44 -07:00
// replace entries by app id
queries . push ( { query : 'DELETE FROM appPortBindings WHERE appId = ?' , args : [ id ] } ) ;
Object . keys ( portBindings ) . forEach ( function ( env ) {
2024-07-16 22:21:36 +02:00
const values = [ portBindings [ env ] . hostPort , portBindings [ env ] . type , env , id , portBindings [ env ] . count ] ;
2024-02-22 16:42:28 +01:00
queries . push ( { query : 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId, count) VALUES(?, ?, ?, ?, ?)' , args : values } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
2018-01-09 21:03:59 -08:00
2021-08-20 09:19:44 -07:00
if ( 'env' in app ) {
queries . push ( { query : 'DELETE FROM appEnvVars WHERE appId = ?' , args : [ id ] } ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
Object . keys ( app . env ) . forEach ( function ( name ) {
queries . push ( {
query : 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)' ,
args : [ id , name , app . env [ name ] ]
} ) ;
2018-01-09 21:03:59 -08:00
} ) ;
2021-08-20 09:19:44 -07:00
}
2022-01-16 12:32:12 -08:00
if ( 'subdomain' in app && 'domain' in app ) { // must be updated together as they are unique together
2022-02-07 13:53:24 -08:00
queries . push ( { query : 'DELETE FROM locations WHERE appId = ?' , args : [ id ] } ) ; // all locations of an app must be updated together
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , app . domain , app . subdomain , Location . TYPE _PRIMARY ] } ) ;
2021-08-20 09:19:44 -07:00
2022-01-14 22:40:51 -08:00
if ( 'secondaryDomains' in app ) {
app . secondaryDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _SECONDARY , d . environmentVariable ] } ) ;
2022-01-14 22:40:51 -08:00
} ) ;
}
2022-01-14 22:29:47 -08:00
if ( 'redirectDomains' in app ) {
app . redirectDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _REDIRECT ] } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
if ( 'aliasDomains' in app ) {
app . aliasDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _ALIAS ] } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
}
if ( 'mounts' in app ) {
queries . push ( { query : 'DELETE FROM appMounts WHERE appId = ?' , args : [ id ] } ) ;
app . mounts . forEach ( function ( m ) {
queries . push ( { query : 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)' , args : [ id , m . volumeId , m . readOnly ] } ) ;
} ) ;
}
const fields = [ ] , values = [ ] ;
2021-09-21 17:28:58 -07:00
for ( const p in app ) {
2025-06-26 15:19:28 +02:00
if ( p === 'manifest' || p === 'tags' || p === 'checklist' || p === 'accessRestriction' || p === 'devices' || p === 'debugMode' || p === 'error'
|| p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators' || p === 'updateInfo' ) {
2021-08-20 09:19:44 -07:00
fields . push ( ` ${ p } Json = ? ` ) ;
values . push ( JSON . stringify ( app [ p ] ) ) ;
2022-01-16 12:32:12 -08:00
} else if ( p !== 'portBindings' && p !== 'subdomain' && p !== 'domain' && p !== 'secondaryDomains' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts' ) {
2021-08-20 09:19:44 -07:00
fields . push ( p + ' = ?' ) ;
values . push ( app [ p ] ) ;
}
}
if ( values . length !== 0 ) {
values . push ( id ) ;
queries . push ( { query : 'UPDATE apps SET ' + fields . join ( ', ' ) + ' WHERE id = ? ' + constraints , args : values } ) ;
}
const [ error , results ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
if ( results [ results . length - 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'App not found' ) ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function update ( id , app ) {
// ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db.
// this way health and healthTime can be updated without changing ts
app . ts = new Date ( ) ;
await updateWithConstraints ( id , app , '' ) ;
}
2019-03-06 11:15:12 -08:00
2026-02-14 16:34:34 +01:00
function validateMemoryLimit ( manifest , memoryLimit ) {
assert . strictEqual ( typeof manifest , 'object' ) ;
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
// max is not checked because docker allows any value for --memory
const min = manifest . memoryLimit || constants . DEFAULT _MEMORY _LIMIT ;
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if ( memoryLimit === 0 ) return null ;
// a special value that indicates unlimited memory
if ( memoryLimit === - 1 ) return null ;
if ( memoryLimit < min ) return new BoxError ( BoxError . BAD _FIELD , 'memoryLimit too small' ) ;
return null ;
}
function canAutoupdateAppSync ( app , updateInfo ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof updateInfo , 'object' ) ;
const manifest = updateInfo . manifest ;
if ( ! app . enableAutomaticUpdate ) return { code : false , reason : 'Automatic updates for the app is disabled' } ;
// for invalid subscriptions the appstore does not return a dockerImage
if ( ! manifest . dockerImage ) return { code : false , reason : 'Invalid or Expired subscription' } ;
if ( updateInfo . unstable ) return { code : false , reason : 'Update is marked as unstable' } ; // only manual update allowed for unstable updates
// for community apps, it's a warning sign when the repo changes (for example: versions domain gets hijacked)
if ( docker . parseImageRef ( manifest . dockerImage ) . fullRepositoryName !== docker . parseImageRef ( app . manifest . dockerImage ) . fullRepositoryName ) return { code : false , reason : 'Package docker image repository has changed' } ;
if ( ( semver . major ( app . manifest . version ) !== 0 ) && ( semver . major ( app . manifest . version ) !== semver . major ( manifest . version ) ) ) {
return { code : false , reason : 'Major package version requires review of breaking changes' } ; // major changes are blocking
}
if ( app . runState === RSTATE _STOPPED ) return { code : false , reason : 'Stopped apps cannot run migration scripts' } ;
const newTcpPorts = manifest . tcpPorts || { } ;
const newUdpPorts = manifest . udpPorts || { } ;
const portBindings = app . portBindings ; // this is never null
for ( const portName in portBindings ) {
if ( ! ( portName in newTcpPorts ) && ! ( portName in newUdpPorts ) ) return { code : false , reason : ` ${ portName } port was in use but new update removes it ` } ;
}
// it's fine if one or more (unused) port keys got removed
return { code : true , reason : '' } ;
}
function attachProperties ( app , domainObjectMap ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof domainObjectMap , 'object' ) ;
app . iconUrl = app . hasIcon || app . hasPackageIcon ? ` /api/v1/apps/ ${ app . id } /icon ` : null ;
app . fqdn = dns . fqdn ( app . subdomain , app . domain ) ;
app . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
app . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
app . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
if ( app . updateInfo ) {
const { code , reason } = canAutoupdateAppSync ( app , app . updateInfo ) ; // isAutoUpdatable is not cached since it depends on enableAutomaticUpdate and runState
app . updateInfo . isAutoUpdatable = code ;
app . updateInfo . manualUpdateReason = reason ;
}
}
2021-08-20 09:19:44 -07:00
async function setHealth ( appId , health , healthTime ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof health , 'string' ) ;
assert ( util . types . isDate ( healthTime ) ) ;
await updateWithConstraints ( appId , { health , healthTime } , '' ) ;
}
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
async function setTask ( appId , values , options ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof values , 'object' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
values . ts = new Date ( ) ;
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
if ( ! options . requireNullTaskId ) return await updateWithConstraints ( appId , values , '' ) ;
if ( options . requiredState === null ) {
await updateWithConstraints ( appId , values , 'AND taskId IS NULL' ) ;
} else {
await updateWithConstraints ( appId , values , ` AND taskId IS NULL AND installationState = " ${ options . requiredState } " ` ) ;
}
2019-03-06 11:15:12 -08:00
}
2021-08-20 09:19:44 -07:00
async function del ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-03-19 16:23:03 -07:00
2021-08-20 09:19:44 -07:00
const queries = [
2022-02-07 13:53:24 -08:00
{ query : 'DELETE FROM locations WHERE appId = ?' , args : [ id ] } ,
2021-08-20 09:19:44 -07:00
{ query : 'DELETE FROM appPortBindings WHERE appId = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appEnvVars WHERE appId = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appPasswords WHERE identifier = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appMounts WHERE appId = ?' , args : [ id ] } ,
2025-09-30 19:23:44 +02:00
{ query : ` UPDATE backupSites SET contentsJson = JSON_REMOVE(contentsJson, JSON_UNQUOTE(JSON_SEARCH(contentsJson, 'one', ?, NULL, ' $ .*[*]'))) WHERE contentsJson LIKE ${ mysql . escape ( '%"' + id + '"%' ) } ` , args : [ id ] } ,
2021-08-20 09:19:44 -07:00
{ query : 'DELETE FROM apps WHERE id = ?' , args : [ id ] }
] ;
2019-03-19 16:23:03 -07:00
2021-08-20 09:19:44 -07:00
const results = await database . transaction ( queries ) ;
2025-09-30 16:17:19 +02:00
if ( results [ 6 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'App not found' ) ;
2021-09-17 09:52:18 -07:00
}
2021-08-20 09:19:44 -07:00
async function clear ( ) {
2022-02-07 13:53:24 -08:00
await database . query ( 'DELETE FROM locations' ) ;
2021-08-20 09:19:44 -07:00
await database . query ( 'DELETE FROM appPortBindings' ) ;
await database . query ( 'DELETE FROM appAddonConfigs' ) ;
await database . query ( 'DELETE FROM appEnvVars' ) ;
await database . query ( 'DELETE FROM apps' ) ;
2019-03-19 16:23:03 -07:00
}
2021-08-20 09:19:44 -07:00
// each query simply join apps table with another table by id. we then join the full result together
2024-07-15 22:59:16 +02:00
const PB _QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, GROUP_CONCAT(CAST(appPortBindings.count AS CHAR(6))) AS portCounts FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id' ;
2021-08-20 09:19:44 -07:00
const ENV _QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id' ;
2022-07-14 11:57:04 +05:30
const SUBDOMAIN _QUERY = 'SELECT id, JSON_ARRAYAGG(locations.subdomain) AS subdomains, JSON_ARRAYAGG(locations.domain) AS domains, JSON_ARRAYAGG(locations.type) AS subdomainTypes, JSON_ARRAYAGG(locations.environmentVariable) AS subdomainEnvironmentVariables, JSON_ARRAYAGG(locations.certificateJson) AS subdomainCertificateJsons FROM apps LEFT JOIN locations ON apps.id = locations.appId GROUP BY apps.id' ;
2021-08-20 09:19:44 -07:00
const MOUNTS _QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id' ;
2024-07-15 22:59:16 +02:00
const APPS _QUERY = ` SELECT ${ APPS _FIELDS _PREFIXED } , hostPorts, environmentVariables, portTypes, portCounts, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, subdomainCertificateJsons, volumeIds, volumeReadOnlys FROM apps `
2021-08-20 09:19:44 -07:00
+ ` LEFT JOIN ( ${ PB _QUERY } ) AS q1 on q1.id = apps.id `
+ ` LEFT JOIN ( ${ ENV _QUERY } ) AS q2 on q2.id = apps.id `
+ ` LEFT JOIN ( ${ SUBDOMAIN _QUERY } ) AS q3 on q3.id = apps.id `
+ ` LEFT JOIN ( ${ MOUNTS _QUERY } ) AS q4 on q4.id = apps.id ` ;
2018-08-12 19:33:11 -07:00
2021-08-20 09:19:44 -07:00
async function get ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2018-01-09 21:03:59 -08:00
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
const result = await database . query ( ` ${ APPS _QUERY } WHERE apps.id = ? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
postProcess ( result [ 0 ] ) ;
attachProperties ( result [ 0 ] , domainObjectMap ) ;
return result [ 0 ] ;
2015-07-20 00:09:47 -07:00
}
2026-02-14 16:34:34 +01:00
async function getStorageDir ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
if ( ! app . manifest . addons ? . localstorage ) return null ;
if ( ! app . storageVolumeId ) return path . join ( paths . APPS _DATA _DIR , app . id , 'data' ) ;
const volume = await volumes . get ( app . storageVolumeId ) ;
if ( ! volume ) throw new BoxError ( BoxError . NOT _FOUND , 'Volume not found' ) ; // not possible
return path . join ( volume . hostPath , app . storageVolumePrefix ) ;
}
async function checkStorage ( app , volumeId , prefix ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof volumeId , 'string' ) ;
assert . strictEqual ( typeof prefix , 'string' ) ;
const volume = await volumes . get ( volumeId ) ;
if ( volume === null ) throw new BoxError ( BoxError . BAD _FIELD , 'Storage volume not found' ) ;
// lack of file perms makes these unsupported
if ( volume . mountType === 'cifs' || volume . mountType === 'sshfs' ) throw new BoxError ( BoxError . BAD _FIELD , ` ${ volume . mountType } volumes cannot be used as data directory ` ) ;
const status = await volumes . getStatus ( volume ) ;
if ( status . state !== 'active' ) throw new BoxError ( BoxError . BAD _FIELD , 'Volume is not active' ) ;
if ( path . isAbsolute ( prefix ) ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " must be a relative path ` ) ;
if ( prefix . endsWith ( '/' ) ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " contains trailing slash ` ) ;
if ( prefix !== '' && path . normalize ( prefix ) !== prefix ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " is not a normalized path ` ) ;
const sourceDir = await getStorageDir ( app ) ;
if ( sourceDir === null ) throw new BoxError ( BoxError . BAD _STATE , 'App does not use localstorage addon' ) ;
const targetDir = path . join ( volume . hostPath , prefix ) ;
const rel = path . relative ( sourceDir , targetDir ) ;
if ( ! rel . startsWith ( '../' ) && rel . split ( '/' ) . length > 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'Only one level subdirectory moves are supported' ) ;
const [ error ] = await safe ( shell . sudo ( [ CHECKVOLUME _CMD , targetDir , sourceDir ] , { } ) ) ;
if ( error && error . code === 2 ) throw new BoxError ( BoxError . BAD _FIELD , ` Target directory ${ targetDir } is not empty ` ) ;
if ( error && error . code === 3 ) throw new BoxError ( BoxError . BAD _FIELD , ` Target directory ${ targetDir } does not support chown ` ) ;
return null ;
}
2021-08-20 09:19:44 -07:00
async function getByIpAddress ( ip ) {
assert . strictEqual ( typeof ip , 'string' ) ;
2016-02-25 11:28:29 +01:00
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2016-02-25 11:28:29 +01:00
2021-08-20 09:19:44 -07:00
const result = await database . query ( ` ${ APPS _QUERY } WHERE apps.containerIp = ? ` , [ ip ] ) ;
if ( result . length === 0 ) return null ;
postProcess ( result [ 0 ] ) ;
attachProperties ( result [ 0 ] , domainObjectMap ) ;
return result [ 0 ] ;
}
async function list ( ) {
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2021-08-20 09:19:44 -07:00
const results = await database . query ( ` ${ APPS _QUERY } ORDER BY apps.id ` , [ ] ) ;
results . forEach ( postProcess ) ;
results . forEach ( ( app ) => attachProperties ( app , domainObjectMap ) ) ;
return results ;
}
2026-02-14 16:34:34 +01:00
// returns the app associated with this IP (app or scheduler)
2021-08-20 09:19:44 -07:00
async function getByFqdn ( fqdn ) {
assert . strictEqual ( typeof fqdn , 'string' ) ;
const result = await list ( ) ;
const app = result . find ( function ( a ) { return a . fqdn === fqdn ; } ) ;
return app ;
}
async function listByUser ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
2021-09-21 17:28:58 -07:00
const result = await list ( ) ;
return result . filter ( ( app ) => canAccess ( app , user ) ) ;
2016-02-25 11:28:29 +01:00
}
2021-09-21 22:19:20 -07:00
async function getTask ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
if ( ! app . taskId ) return null ;
return await tasks . get ( app . taskId ) ;
}
2022-01-16 12:32:12 -08:00
function mailboxNameForSubdomain ( subdomain , manifest ) {
if ( subdomain ) return ` ${ subdomain } .app ` ;
2019-11-14 21:43:14 -08:00
if ( manifest . title ) return manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) + '.app' ;
return 'noreply.app' ;
2018-06-09 11:05:54 -07:00
}
2021-11-17 10:42:04 -08:00
async function onTaskFinished ( error , appId , installationState , taskId , auditSource ) {
assert ( ! error || typeof error === 'object' ) ;
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
assert . strictEqual ( typeof taskId , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-09-30 10:31:50 -07:00
const success = ! error ;
const errorMessage = error ? . message || null ;
2021-09-30 10:45:25 -07:00
const app = await get ( appId ) ;
const task = await tasks . get ( taskId ) ;
if ( ! app || ! task ) return ;
2021-09-30 10:31:50 -07:00
switch ( installationState ) {
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
case ISTATE _PENDING _DATA _DIR _MIGRATION :
2021-11-17 10:42:04 -08:00
if ( success ) await safe ( services . rebuildService ( 'sftp' , auditSource ) , { debug } ) ;
2021-09-30 10:31:50 -07:00
break ;
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
case ISTATE _PENDING _UPDATE : {
2021-09-30 10:31:50 -07:00
const fromManifest = success ? task . args [ 1 ] . updateConfig . manifest : app . manifest ;
const toManifest = success ? app . manifest : task . args [ 1 ] . updateConfig . manifest ;
2021-11-17 10:42:04 -08:00
await eventlog . add ( eventlog . ACTION _APP _UPDATE _FINISH , auditSource , { app , toManifest , fromManifest , success , errorMessage } ) ;
2024-12-11 15:47:41 +01:00
await notifications . unpin ( notifications . TYPE _MANUAL _APP _UPDATE _NEEDED , { context : app . id } ) ;
2021-09-30 10:31:50 -07:00
break ;
}
2022-04-05 09:28:30 -07:00
}
2021-09-30 10:31:50 -07:00
}
2026-02-14 16:34:34 +01:00
async function getCount ( ) {
const result = await database . query ( 'SELECT COUNT(*) AS total FROM apps' ) ;
return result [ 0 ] . total ;
}
2021-01-06 14:46:46 -08:00
2026-02-14 16:34:34 +01:00
async function setAccessRestriction ( app , accessRestriction , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-09-07 09:57:49 -07:00
2026-02-14 16:34:34 +01:00
const appId = app . id ;
const error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) throw error ;
2021-01-06 14:46:46 -08:00
2026-02-14 16:34:34 +01:00
await update ( appId , { accessRestriction } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , accessRestriction } ) ;
2019-09-24 00:07:20 -07:00
}
2026-02-14 16:34:34 +01:00
async function setOperators ( app , operators , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof operators , 'object' ) ;
2021-11-17 10:38:02 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-08-27 16:12:24 -07:00
2026-02-14 16:34:34 +01:00
const appId = app . id ;
const error = validateAccessRestriction ( operators ) ; // not a typo. same structure for operators and accessRestriction
if ( error ) throw error ;
2019-08-29 12:14:42 -07:00
2026-02-14 16:34:34 +01:00
await update ( appId , { operators } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , operators } ) ;
}
2019-08-27 22:39:59 -07:00
2026-02-14 16:34:34 +01:00
async function setCrontab ( app , crontab , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( crontab === null || typeof crontab === 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-08-26 15:55:57 -07:00
2026-02-14 16:34:34 +01:00
const appId = app . id ;
parseCrontab ( crontab ) ;
2019-08-26 15:55:57 -07:00
2026-02-14 16:34:34 +01:00
await update ( appId , { crontab } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , crontab } ) ;
2019-08-26 15:55:57 -07:00
}
2026-02-14 16:34:34 +01:00
async function setUpstreamUri ( app , upstreamUri , auditSource ) {
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2026-02-14 16:34:34 +01:00
assert . strictEqual ( typeof upstreamUri , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
if ( app . manifest . id !== constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'upstreamUri can only be set for proxy app' ) ;
const appId = app . id ;
const error = validateUpstreamUri ( upstreamUri ) ;
if ( error ) throw error ;
await reverseProxy . writeAppConfigs ( Object . assign ( { } , app , { upstreamUri } ) ) ;
await update ( appId , { upstreamUri } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , upstreamUri } ) ;
}
async function setLabel ( app , label , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof label , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
const error = validateLabel ( label ) ;
if ( error ) throw error ;
await update ( appId , { label } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , label } ) ;
}
async function setTags ( app , tags , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( Array . isArray ( tags ) ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
const error = validateTags ( tags ) ;
if ( error ) throw error ;
await update ( appId , { tags } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , tags } ) ;
}
async function setNotes ( app , notes , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof notes , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
await update ( app . id , { notes } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , notes } ) ;
}
async function setChecklistItem ( app , checklistItemKey , acknowledged , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof checklistItemKey , 'string' ) ;
assert . strictEqual ( typeof acknowledged , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
if ( ! app . checklist [ checklistItemKey ] ) throw new BoxError ( BoxError . NOT _FOUND , 'no such checklist item' ) ;
// nothing changed
if ( app . checklist [ checklistItemKey ] . acknowledged === acknowledged ) return ;
const checklist = app . checklist ;
checklist [ checklistItemKey ] . acknowledged = acknowledged ;
checklist [ checklistItemKey ] . changedAt = Date . now ( ) ;
checklist [ checklistItemKey ] . changedBy = auditSource . username ;
await update ( app . id , { checklist } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , checklist } ) ;
}
async function setIcon ( app , icon , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( icon === null || typeof icon === 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
if ( icon ) {
icon = Buffer . from ( icon , 'base64' ) ;
if ( icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' ) ;
}
await update ( appId , { icon } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , iconChanged : true } ) ;
}
async function setAutomaticBackup ( app , enable , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
await update ( appId , { enableBackup : enable } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableBackup : enable } ) ;
}
async function setAutomaticUpdate ( app , enable , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
await update ( appId , { enableAutomaticUpdate : enable } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableAutomaticUpdate : enable } ) ;
}
async function setReverseProxyConfig ( app , reverseProxyConfig , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof reverseProxyConfig , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
reverseProxyConfig = Object . assign ( { robotsTxt : null , csp : null , hstsPreload : false } , reverseProxyConfig ) ;
const appId = app . id ;
let error = validateCsp ( reverseProxyConfig . csp ) ;
if ( error ) throw error ;
error = validateRobotsTxt ( reverseProxyConfig . robotsTxt ) ;
if ( error ) throw error ;
await reverseProxy . writeAppConfigs ( Object . assign ( { } , app , { reverseProxyConfig } ) ) ;
await update ( appId , { reverseProxyConfig } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , reverseProxyConfig } ) ;
}
async function getLocation ( subdomain , domain ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await database . query ( ` SELECT ${ LOCATION _FIELDS } FROM locations WHERE subdomain=? AND domain=? ` , [ subdomain , domain ] ) ;
if ( result . length === 0 ) return null ;
return new Location ( subdomain , domain , result [ 0 ] . type , safe . JSON . parse ( result [ 0 ] . certificateJson ) ) ;
}
async function validateLocations ( locations ) {
assert ( Array . isArray ( locations ) ) ;
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
const RESERVED _SUBDOMAINS = [
constants . SMTP _SUBDOMAIN ,
constants . IMAP _SUBDOMAIN
] ;
const dashboardLocation = await dashboard . getLocation ( ) ;
for ( const location of locations ) {
if ( ! ( location . domain in domainObjectMap ) ) return new BoxError ( BoxError . BAD _FIELD , ` No such domain in ${ location . type } location ` ) ;
let subdomain = location . subdomain ;
if ( location . type === Location . TYPE _ALIAS && subdomain . startsWith ( '*' ) ) {
if ( subdomain === '*' ) continue ;
subdomain = subdomain . replace ( /^\*\./ , '' ) ; // remove *.
}
if ( RESERVED _SUBDOMAINS . indexOf ( subdomain ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` subdomain ' ${ subdomain } ' is reserved ` ) ;
if ( location . fqdn === dashboardLocation . fqdn ) return new BoxError ( BoxError . BAD _FIELD , ` subdomain ' ${ subdomain } ' is reserved for dashboard ` ) ;
const error = dns . validateHostname ( subdomain , location . domain ) ;
if ( error ) return new BoxError ( BoxError . BAD _FIELD , ` Bad ${ location . type } location: ${ error . message } ` ) ;
}
return null ;
}
async function setCertificate ( app , data , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const { subdomain , domain , cert , key } = data ;
const domainObject = await domains . get ( domain ) ;
if ( domainObject === null ) throw new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ;
if ( cert && key ) await reverseProxy . validateCertificate ( subdomain , domain , { cert , key } ) ;
const certificate = cert && key ? { cert , key } : null ;
const result = await database . query ( 'UPDATE locations SET certificateJson=? WHERE location=? AND domain=?' , [ certificate ? JSON . stringify ( certificate ) : null , subdomain , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Location not found' ) ;
const location = await getLocation ( subdomain , domain ) ; // fresh location object with type
await reverseProxy . setUserCertificate ( app , location ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , subdomain , domain , cert } ) ;
}
async function getLogPaths ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
const appId = app . id ;
const filePaths = [ ] ;
filePaths . push ( path . join ( paths . LOG _DIR , appId , 'apptask.log' ) ) ;
filePaths . push ( path . join ( paths . LOG _DIR , appId , 'app.log' ) ) ;
if ( app . manifest . addons && app . manifest . addons . redis ) filePaths . push ( path . join ( paths . LOG _DIR , ` redis- ${ appId } /app.log ` ) ) ;
if ( app . manifest . logPaths ) {
const [ error , result ] = await safe ( docker . inspect ( app . containerId ) ) ;
if ( ! error ) {
const runVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/run' ; } ) ;
const tmpVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/tmp' ; } ) ;
const dataVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/app/data' ; } ) ;
// note: wild cards are not supported yet in logPaths since that will require shell expansion
for ( const logPath of app . manifest . logPaths ) {
if ( logPath . startsWith ( '/tmp/' ) ) filePaths . push ( ` ${ tmpVolume . Source } / ${ logPath . slice ( '/tmp/' . length ) } ` ) ;
else if ( logPath . startsWith ( '/run/' ) ) filePaths . push ( ` ${ runVolume . Source } / ${ logPath . slice ( '/run/' . length ) } ` ) ;
else if ( logPath . startsWith ( '/app/data/' ) ) filePaths . push ( ` ${ dataVolume . Source } / ${ logPath . slice ( '/app/data/' . length ) } ` ) ;
}
}
}
return filePaths ;
}
async function getLogs ( app , options ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof options . lines , 'number' ) ;
assert . strictEqual ( typeof options . format , 'string' ) ;
assert . strictEqual ( typeof options . follow , 'boolean' ) ;
const appId = app . id ;
const logPaths = await getLogPaths ( app ) ;
const cp = logs . tail ( logPaths , { lines : options . lines , follow : options . follow , sudo : true } ) ; // need sudo access for paths inside app container (manifest.logPaths)
const logStream = new logs . LogStream ( { format : options . format || 'json' , source : appId } ) ;
logStream . on ( 'close' , ( ) => cp . terminate ( ) ) ; // the caller has to call destroy() on logStream. destroy() of Transform emits 'close'
cp . stdout . pipe ( logStream ) ;
return logStream ;
}
async function appendLogLine ( app , line ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof line , 'string' ) ;
const logFilePath = path . join ( paths . LOG _DIR , app . id , 'app.log' ) ;
const isoDate = new Date ( new Date ( ) . toUTCString ( ) ) . toISOString ( ) ;
if ( ! safe . fs . appendFileSync ( logFilePath , ` ${ isoDate } ${ line } \n ` ) ) console . error ( ` Could not append log line for app ${ app . id } at ${ logFilePath } : ${ safe . error . message } ` ) ;
}
async function checkManifest ( manifest ) {
assert ( manifest && typeof manifest === 'object' ) ;
if ( manifest . manifestVersion !== 2 ) return new BoxError ( BoxError . BAD _FIELD , 'Manifest version must be 2' ) ;
if ( ! manifest . dockerImage ) return new BoxError ( BoxError . BAD _FIELD , 'Missing dockerImage' ) ; // dockerImage is optional in manifest
if ( semver . valid ( manifest . maxBoxVersion ) && semver . gt ( constants . VERSION , manifest . maxBoxVersion ) ) {
return new BoxError ( BoxError . BAD _FIELD , 'Box version exceeds Apps maxBoxVersion' ) ;
}
if ( semver . valid ( manifest . minBoxVersion ) && semver . gt ( manifest . minBoxVersion , constants . VERSION ) ) {
return new BoxError ( BoxError . BAD _FIELD , 'App version requires a new platform version' ) ;
}
const error = await services . checkAddonsSupport ( manifest . addons || { } ) ;
return error ;
}
async function createExec ( app , options ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( options && typeof options === 'object' ) ;
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'cannot exec on proxy app' ) ;
const cmd = options . cmd || [ '/bin/bash' ] ;
assert ( Array . isArray ( cmd ) && cmd . length > 0 ) ;
if ( app . installationState !== ISTATE _INSTALLED || app . runState !== RSTATE _RUNNING ) {
throw new BoxError ( BoxError . BAD _STATE , 'App not installed or running' ) ;
}
const createOptions = {
AttachStdin : true ,
AttachStdout : true ,
AttachStderr : true ,
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
Tty : options . tty ,
Cmd : cmd
} ;
// currently the webterminal and cli sets C.UTF-8
if ( options . lang ) createOptions . Env = [ 'LANG=' + options . lang ] ;
if ( options . cwd ) createOptions . WorkingDir = options . cwd ;
return await docker . createExec ( app . containerId , createOptions ) ;
}
async function startExec ( app , execId , options ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof execId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
2019-09-21 19:45:55 -07:00
2026-02-14 16:34:34 +01:00
if ( app . installationState !== ISTATE _INSTALLED || app . runState !== RSTATE _RUNNING ) {
throw new BoxError ( BoxError . BAD _STATE , 'App not installed or running' ) ;
}
const startOptions = {
Detach : false ,
Tty : options . tty ,
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack : true ,
stream : true ,
stdin : true ,
stdout : true ,
stderr : true
} ;
const stream = await docker . startExec ( execId , startOptions ) ;
if ( options . rows && options . columns ) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout ( async function ( ) {
await safe ( docker . resizeExec ( execId , { h : options . rows , w : options . columns } , { debug } ) ) ;
} , 2000 ) ;
}
return stream ;
}
async function getExec ( app , execId ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof execId , 'string' ) ;
return await docker . getExec ( execId ) ;
}
async function listBackups ( app , page , perPage ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
return await backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , page , perPage ) ;
}
async function listEventlog ( app , page , perPage ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
const actions = [ ] ;
const search = app . id ;
return await eventlog . listPaged ( actions , search , page , perPage ) ;
}
async function drainStream ( stream ) {
return new Promise ( ( resolve , reject ) => {
let data = '' ;
stream . setEncoding ( 'utf8' ) ;
stream . on ( 'error' , ( error ) => reject ( new BoxError . FS _ERROR , error . message ) ) ;
stream . on ( 'data' , function ( d ) { data += d ; } ) ;
stream . on ( 'end' , function ( ) {
resolve ( data ) ;
} ) ;
} ) ;
}
async function downloadFile ( app , filePath ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof filePath , 'string' ) ;
const statExecId = await createExec ( app , { cmd : [ 'stat' , '--printf=%F-%s' , filePath ] , tty : true } ) ;
const statStream = await startExec ( app , statExecId , { tty : true } ) ;
const data = await drainStream ( statStream ) ;
const parts = data . split ( '-' ) ;
if ( parts . length !== 2 ) throw new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ;
const type = parts [ 0 ] ;
let filename , cmd , size ;
if ( type === 'regular file' ) {
cmd = [ 'cat' , filePath ] ;
size = parseInt ( parts [ 1 ] , 10 ) ;
filename = path . basename ( filePath ) ;
if ( isNaN ( size ) ) throw new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ;
} else if ( type === 'directory' ) {
cmd = [ 'tar' , 'zcf' , '-' , '-C' , filePath , '.' ] ;
filename = path . basename ( filePath ) + '.tar.gz' ;
size = 0 ; // unknown
} else {
throw new BoxError ( BoxError . NOT _FOUND , 'only files or dirs can be downloaded' ) ;
}
const execId = await createExec ( app , { cmd , tty : false } ) ;
const inputStream = await startExec ( app , execId , { tty : false } ) ;
// transforms the docker stream into a normal stream
const stdoutStream = new TransformStream ( {
transform : function ( chunk , ignoredEncoding , callback ) {
this . _buffer = this . _buffer ? Buffer . concat ( [ this . _buffer , chunk ] ) : chunk ;
for ( ; ; ) {
if ( this . _buffer . length < 8 ) break ; // header is 8 bytes
const type = this . _buffer . readUInt8 ( 0 ) ;
const len = this . _buffer . readUInt32BE ( 4 ) ;
if ( this . _buffer . length < ( 8 + len ) ) break ; // not enough
const payload = this . _buffer . slice ( 8 , 8 + len ) ;
this . _buffer = this . _buffer . slice ( 8 + len ) ; // consumed
if ( type === 1 ) this . push ( payload ) ;
}
callback ( ) ;
}
} ) ;
inputStream . pipe ( stdoutStream ) ;
return { stream : stdoutStream , filename , size } ;
}
async function uploadFile ( app , sourceFilePath , destFilePath ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof sourceFilePath , 'string' ) ;
assert . strictEqual ( typeof destFilePath , 'string' ) ;
// the built-in bash printf understands "%q" but not /usr/bin/printf.
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
const escapedDestFilePath = await shell . bash ( ` printf %q ' ${ destFilePath . replace ( /'/g , '\'\\\'\'' ) } ' ` , { encoding : 'utf8' } ) ;
debug ( ` uploadFile: ${ sourceFilePath } -> ${ escapedDestFilePath } ` ) ;
const execId = await createExec ( app , { cmd : [ 'bash' , '-c' , ` cat - > ${ escapedDestFilePath } ` ] , tty : false } ) ;
const destStream = await startExec ( app , execId , { tty : false } ) ;
return new Promise ( ( resolve , reject ) => {
const done = once ( error => reject ( new BoxError ( BoxError . FS _ERROR , error . message ) ) ) ;
const sourceStream = fs . createReadStream ( sourceFilePath ) ;
sourceStream . on ( 'error' , done ) ;
destStream . on ( 'error' , done ) ;
destStream . on ( 'finish' , resolve ) ;
sourceStream . pipe ( destStream ) ;
} ) ;
}
async function writeConfig ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( app , null , 4 ) ) ) {
throw new BoxError ( BoxError . FS _ERROR , 'Error creating config.json: ' + safe . error . message ) ;
}
const [ error , icons ] = await safe ( getIcons ( app . id ) ) ;
if ( ! error && icons . icon ) safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) , icons . icon ) ;
}
async function loadConfig ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
const appConfig = safe . JSON . parse ( safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) ) ) ;
let data = { } ;
if ( appConfig ) {
data = _ . pick ( appConfig , [ 'memoryLimit' , 'cpuQuota' , 'enableBackup' , 'reverseProxyConfig' , 'env' , 'servicesConfig' , 'label' , 'tags' , 'enableAutomaticUpdate' ] ) ;
}
const icon = safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) ) ;
if ( icon ) data . icon = icon ;
await update ( app . id , data ) ;
}
function validatePorts ( ports , manifest ) {
assert . strictEqual ( typeof ports , 'object' ) ;
assert . strictEqual ( typeof manifest , 'object' ) ;
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
const RESERVED _PORTS = [
22 , /* ssh */
25 , /* smtp */
80 , /* http */
143 , /* imap */
202 , /* alternate ssh */
222 , /* proftd */
443 , /* https */
465 , /* smtps */
587 , /* submission */
993 , /* imaps */
995 , /* pop3s */
2003 , /* graphite (lo) */
constants . PORT , /* app server (lo) */
constants . AUTHWALL _PORT , /* protected sites */
constants . INTERNAL _SMTP _PORT , /* internal smtp port (lo) */
constants . LDAP _PORT ,
3306 , /* mysql (lo) */
3478 , /* turn,stun */
4190 , /* managesieve */
5349 , /* turn,stun TLS */
8000 , /* ESXi monitoring */
] ;
const RESERVED _PORT _RANGES = [
[ constants . TURN _UDP _PORT _START , constants . TURN _UDP _PORT _END ] /* turn udp ports */
] ;
const ALLOWED _PORTS = [
53 , // dns 53 is special and adblocker apps can use them
853 // dns over tls
] ;
if ( ! ports ) return null ;
const tcpPorts = manifest . tcpPorts || { } ;
const udpPorts = manifest . udpPorts || { } ;
for ( const portName in ports ) {
if ( ! /^[A-Z0-9_]+$/ . test ( portName ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ portName } is not a valid environment variable in ports ` ) ;
const hostPort = ports [ portName ] ;
if ( ! Number . isInteger ( hostPort ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } is not an integer in ${ portName } ports ` ) ;
if ( RESERVED _PORTS . indexOf ( hostPort ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } for ${ portName } is reserved in ports ` ) ;
if ( RESERVED _PORT _RANGES . find ( range => ( hostPort >= range [ 0 ] && hostPort <= range [ 1 ] ) ) ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } for ${ portName } is reserved in ports ` ) ;
if ( ALLOWED _PORTS . indexOf ( hostPort ) === - 1 && ( hostPort <= 1023 || hostPort > 65535 ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } for ${ portName } is not in permitted range in ports ` ) ;
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and ports. missing values implies the service is disabled
const portSpec = tcpPorts [ portName ] || udpPorts [ portName ] ;
if ( ! portSpec ) return new BoxError ( BoxError . BAD _FIELD , ` Invalid portBinding ${ portName } ` ) ;
if ( portSpec . readOnly && portSpec . defaultValue !== hostPort ) return new BoxError ( BoxError . BAD _FIELD , ` portBinding ${ portName } is readOnly and cannot have a different value that the default ` ) ;
if ( ( hostPort + ( portSpec . portCount || 1 ) ) > 65535 ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } + ${ portSpec . portCount } for ${ portName } exceeds valid port range ` ) ;
}
return null ;
}
function validateDevices ( devices ) {
for ( const key in devices ) {
if ( key . indexOf ( '/dev/' ) !== 0 ) return new BoxError ( BoxError . BAD _FIELD , ` " ${ key } " must start with /dev/ ` ) ;
}
return null ;
}
async function scheduleTask ( appId , installationState , taskId , auditSource ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
assert . strictEqual ( typeof taskId , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-11-23 18:06:31 -08:00
2026-02-14 16:34:34 +01:00
let memoryLimit = 400 ;
if ( installationState === ISTATE _PENDING _CLONE || installationState === ISTATE _PENDING _RESTORE
|| installationState === ISTATE _PENDING _IMPORT || installationState === ISTATE _PENDING _UPDATE ) {
const sites = await backupSites . listByContentForUpdates ( appId ) ;
memoryLimit = sites . reduce ( ( acc , cur ) => cur . limits ? . memoryLimit ? Math . max ( cur . limits . memoryLimit / 1024 / 1024 , acc ) : acc , 400 ) ;
} else if ( installationState === ISTATE _PENDING _DATA _DIR _MIGRATION ) {
memoryLimit = 1024 ; // cp takes more memory than we think
2019-09-23 10:35:42 -07:00
}
2019-09-21 19:45:55 -07:00
2026-02-14 16:34:34 +01:00
const options = { timeout : 20 * 60 * 60 * 1000 /* 20 hours */ , nice : 15 , memoryLimit } ;
2020-05-28 12:16:28 -07:00
2026-02-14 16:34:34 +01:00
appTaskManager . scheduleTask ( appId , taskId , options , async function ( error ) {
debug ( ` scheduleTask: task ${ taskId } of ${ appId } completed. error: %o ` , error ) ;
if ( error ? . code === tasks . ECRASHED || error ? . code === tasks . ESTOPPED ) { // if task crashed, update the error
debug ( ` Apptask crashed/stopped: ${ error . message } ` ) ;
const appError = {
message : error . message ,
reason : BoxError . TASK _ERROR ,
crashed : error . code === tasks . ECRASHED ,
stopped : error . code === tasks . ESTOPPED ,
taskId ,
installationState
} ;
await safe ( update ( appId , { installationState : ISTATE _ERROR , error : appError , taskId : null } ) , { debug } ) ;
} else if ( ! ( installationState === ISTATE _PENDING _UNINSTALL && ! error ) ) { // clear out taskId except for successful uninstall
await safe ( update ( appId , { taskId : null } ) , { debug } ) ;
}
await safe ( onTaskFinished ( error , appId , installationState , taskId , auditSource ) , { debug } ) ; // ignore error
} ) ;
2019-09-21 19:45:55 -07:00
}
2026-02-14 16:34:34 +01:00
async function addTask ( appId , installationState , task , auditSource ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ; // { args, values }
assert . strictEqual ( typeof auditSource , 'object' ) ;
2022-11-28 22:16:22 +01:00
2026-02-14 16:34:34 +01:00
const { args , values } = task ;
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
const requiredState = null ; // 'requiredState' in task ? task.requiredState : ISTATE_INSTALLED;
const scheduleNow = 'scheduleNow' in task ? task . scheduleNow : true ;
const requireNullTaskId = 'requireNullTaskId' in task ? task . requireNullTaskId : true ;
2019-09-27 10:25:26 -07:00
2026-02-14 16:34:34 +01:00
const taskId = await tasks . add ( tasks . TASK _APP , [ appId , args ] ) ;
2023-08-01 19:33:59 +05:30
2026-02-14 16:34:34 +01:00
const [ updateError ] = await safe ( setTask ( appId , Object . assign ( { installationState , taskId , error : null } , values ) , { requiredState , requireNullTaskId } ) ) ;
if ( updateError && updateError . reason === BoxError . NOT _FOUND ) throw new BoxError ( BoxError . BAD _STATE , 'Another task is scheduled for this app' ) ; // could be because app went away OR a taskId exists
if ( updateError ) throw updateError ;
2021-01-18 22:47:53 -08:00
2026-02-14 16:34:34 +01:00
if ( scheduleNow ) await safe ( scheduleTask ( appId , installationState , taskId , auditSource ) , { debug } ) ; // ignore error
2019-09-27 10:25:26 -07:00
2026-02-14 16:34:34 +01:00
return taskId ;
}
2023-08-01 19:33:59 +05:30
2026-02-14 16:34:34 +01:00
function checkAppState ( app , state ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof state , 'string' ) ;
2023-08-01 19:33:59 +05:30
2026-02-14 16:34:34 +01:00
if ( app . taskId ) return new BoxError ( BoxError . BAD _STATE , ` Locked by task ${ app . taskId } : ${ app . installationState } / ${ app . runState } ` ) ;
if ( app . installationState === ISTATE _ERROR ) {
// allow task to be called again if that was the errored task
if ( app . error . installationState === state ) return null ;
// allow uninstall from any state
if ( state !== ISTATE _PENDING _UNINSTALL && state !== ISTATE _PENDING _RESTORE && state !== ISTATE _PENDING _IMPORT ) return new BoxError ( BoxError . BAD _STATE , 'Not allowed in error state' ) ;
2021-08-20 09:19:44 -07:00
}
2022-11-28 22:16:22 +01:00
2026-02-14 16:34:34 +01:00
if ( app . runState === RSTATE _STOPPED ) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
if ( state === ISTATE _PENDING _UPDATE || state === ISTATE _PENDING _RESTORE || state === ISTATE _PENDING _IMPORT ) return new BoxError ( BoxError . BAD _STATE , 'Not allowed in stopped state' ) ;
}
2019-09-27 10:25:26 -07:00
2026-02-14 16:34:34 +01:00
return null ;
2021-11-15 13:55:29 -08:00
}
2021-08-20 09:19:44 -07:00
async function install ( data , auditSource ) {
2016-06-03 23:22:38 -07:00
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 19:12:07 -07:00
assert . strictEqual ( typeof data . manifest , 'object' ) ; // manifest is already downloaded
2020-03-29 16:24:04 -07:00
2025-08-20 20:01:43 +02:00
const features = appstore . getFeatures ( ) ;
const installedAppCount = await getCount ( ) ;
2025-08-21 11:20:18 +02:00
if ( features . appMaxCount <= installedAppCount ) throw new BoxError ( BoxError . PLAN _LIMIT , 'app limit reached' ) ;
2025-08-20 20:01:43 +02:00
2022-01-16 12:32:12 -08:00
const subdomain = data . subdomain . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
2016-06-03 23:22:38 -07:00
accessRestriction = data . accessRestriction || null ,
2024-12-10 17:30:23 +01:00
operators = data . operators || null ,
2016-06-03 23:22:38 -07:00
memoryLimit = data . memoryLimit || 0 ,
2024-12-10 17:30:23 +01:00
cpuQuota = data . cpuQuota || 100 ,
2017-04-11 12:49:21 -07:00
debugMode = data . debugMode || null ,
2017-08-16 14:12:07 -07:00
enableBackup = 'enableBackup' in data ? data . enableBackup : true ,
2018-12-07 09:03:28 -08:00
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data . enableAutomaticUpdate : true ,
2022-01-14 22:29:47 -08:00
redirectDomains = data . redirectDomains || [ ] ,
2021-01-18 17:26:26 -08:00
aliasDomains = data . aliasDomains || [ ] ,
2024-12-05 13:47:59 +01:00
devices = data . devices || { } ,
2018-12-12 12:55:35 -08:00
env = data . env || { } ,
2019-03-22 07:48:31 -07:00
label = data . label || null ,
2019-09-16 09:31:34 -07:00
tags = data . tags || [ ] ,
2020-03-29 16:24:04 -07:00
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ,
2021-02-24 14:49:30 -08:00
skipDnsSetup = 'skipDnsSetup' in data ? data . skipDnsSetup : false ,
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data . enableTurn : true ,
2026-02-05 17:29:00 +01:00
appStoreId = data . appStoreId || '' ,
versionsUrl = data . versionsUrl || '' ,
2022-06-10 11:23:58 -07:00
upstreamUri = data . upstreamUri || '' ,
2024-12-10 17:30:23 +01:00
manifest = data . manifest ,
notes = data . notes || null ,
crontab = data . crontab || null ;
2016-06-03 23:22:38 -07:00
2025-06-14 10:28:20 +02:00
// when redis is optional, do not enable it by default. it's mostly used for caching in those setups
const enableRedis = 'enableRedis' in data ? data . enableRedis : ! manifest . addons ? . redis ? . optional ;
2020-03-29 16:24:04 -07:00
let error = manifestFormat . parse ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` Manifest error: ${ error . message } ` ) ;
2015-07-20 00:09:47 -07:00
2026-02-14 19:01:00 +01:00
if ( data . sourceArchiveFilePath ) manifest . dockerImage = ` local/ ${ manifest . id } : ${ manifest . version } - ${ Date . now ( ) } ` ;
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2024-07-16 22:21:36 +02:00
error = validatePorts ( data . ports || null , manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateAccessRestriction ( accessRestriction ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2024-12-10 17:30:23 +01:00
error = validateAccessRestriction ( operators ) ; // not a typo. same structure for operators and accessRestriction
if ( error ) throw error ;
2020-03-29 16:24:04 -07:00
error = validateMemoryLimit ( manifest , memoryLimit ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateDebugMode ( debugMode ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-02-05 15:07:27 +01:00
2020-03-29 16:24:04 -07:00
error = validateLabel ( label ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-02-11 18:14:16 +01:00
2024-12-10 17:30:23 +01:00
error = validateCpuQuota ( cpuQuota ) ;
if ( error ) throw error ;
parseCrontab ( crontab ) ;
2022-11-23 12:53:21 +01:00
if ( 'upstreamUri' in data ) error = validateUpstreamUri ( upstreamUri ) ;
2022-06-09 14:21:09 +02:00
if ( error ) throw error ;
2020-03-29 16:24:04 -07:00
error = validateTags ( tags ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-10-11 20:21:32 -07:00
2022-01-20 16:57:30 -08:00
error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2022-01-14 22:40:51 -08:00
2021-10-01 09:37:33 -07:00
let sso = 'sso' in data ? data . sso : null ;
2021-08-20 09:19:44 -07:00
if ( 'sso' in data && ! ( 'optionalSso' in manifest ) ) throw new BoxError ( BoxError . BAD _FIELD , 'sso can only be specified for apps with optionalSso' ) ;
2020-03-29 16:24:04 -07:00
// if sso was unspecified, enable it by default if possible
2023-04-14 21:18:44 +02:00
if ( sso === null ) sso = ! ! manifest . addons ? . ldap || ! ! manifest . addons ? . proxyAuth || ! ! manifest . addons ? . oidc ;
2019-03-22 07:48:31 -07:00
2024-12-05 13:47:59 +01:00
error = validateDevices ( devices ) ;
if ( error ) throw error ;
2026-02-14 16:34:34 +01:00
error = validateEnv ( env ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2026-02-14 16:34:34 +01:00
if ( constants . DEMO && constants . DEMO _BLOCKED _APPS . includes ( appStoreId ) ) throw new BoxError ( BoxError . BAD _FIELD , 'This app is blocked in the demo' ) ;
2019-09-08 16:57:08 -07:00
2026-02-14 16:34:34 +01:00
// sendmail is enabled by default
const enableMailbox = 'enableMailbox' in data ? data . enableMailbox : true ;
const mailboxName = manifest . addons ? . sendmail ? mailboxNameForSubdomain ( subdomain , manifest ) : null ;
const mailboxDomain = manifest . addons ? . sendmail ? domain : null ;
2024-04-10 17:02:32 +02:00
2026-02-14 16:34:34 +01:00
let icon = data . icon || null ;
if ( icon ) {
icon = Buffer . from ( icon , 'base64' ) ;
if ( icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' ) ;
}
2024-04-10 17:02:32 +02:00
2026-02-14 16:34:34 +01:00
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) )
. concat ( redirectDomains . map ( rd => new Location ( rd . subdomain , rd . domain , Location . TYPE _REDIRECT ) ) )
. concat ( aliasDomains . map ( ad => new Location ( ad . subdomain , ad . domain , Location . TYPE _ALIAS ) ) ) ;
2024-04-19 12:40:35 +02:00
2026-02-14 16:34:34 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2024-06-24 18:39:37 +02:00
2026-02-14 16:34:34 +01:00
if ( constants . DEMO && ( await getCount ( ) >= constants . DEMO _APP _LIMIT ) ) throw new BoxError ( BoxError . BAD _STATE , 'Too many installed apps, please uninstall a few and try again' ) ;
2024-06-24 18:39:37 +02:00
2026-02-14 16:34:34 +01:00
const appId = crypto . randomUUID ( ) ;
debug ( ` Installing app ${ appId } ` ) ;
2024-06-24 18:39:37 +02:00
2026-02-14 16:34:34 +01:00
const app = {
accessRestriction ,
operators ,
memoryLimit ,
cpuQuota ,
sso ,
debugMode ,
mailboxName ,
mailboxDomain ,
enableBackup ,
enableAutomaticUpdate ,
secondaryDomains ,
redirectDomains ,
aliasDomains ,
env ,
devices ,
label ,
tags ,
icon ,
enableMailbox ,
upstreamUri ,
enableTurn ,
enableRedis ,
notes ,
crontab ,
runState : RSTATE _RUNNING ,
installationState : ISTATE _PENDING _INSTALL
} ;
2019-09-08 16:57:08 -07:00
2026-02-14 16:34:34 +01:00
const [ addError ] = await safe ( add ( appId , appStoreId , versionsUrl , manifest , subdomain , domain , portBindings , app ) ) ;
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
if ( addError ) throw addError ;
2019-09-08 16:57:08 -07:00
2026-02-14 19:01:00 +01:00
if ( data . sourceArchiveFilePath ) await fileUtils . renameFile ( data . sourceArchiveFilePath , ` ${ paths . SOURCE _ARCHIVES _DIR } / ${ appId } .tar.gz ` ) ;
2026-02-14 16:34:34 +01:00
const task = {
args : { restoreConfig : null , skipDnsSetup , overwriteDns } ,
values : { } ,
requiredState : app . installationState
} ;
2019-09-08 16:57:08 -07:00
2026-02-14 16:34:34 +01:00
const taskId = await addTask ( appId , app . installationState , task , auditSource ) ;
const newApp = Object . assign ( { } , _ . omit ( app , [ 'icon' ] ) , { appStoreId , versionsUrl , manifest , subdomain , domain , portBindings } ) ;
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
await eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId , app : newApp , taskId } ) ;
return { id : appId , taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMemoryLimit ( app , memoryLimit , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _RESIZE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateMemoryLimit ( app . manifest , memoryLimit ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { memoryLimit }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _RESIZE , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , memoryLimit , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2026-02-14 16:34:34 +01:00
// never fails just prints error
2024-04-10 17:38:49 +02:00
async function setCpuQuota ( app , cpuQuota , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-04-10 17:38:49 +02:00
assert . strictEqual ( typeof cpuQuota , 'number' ) ;
2020-01-28 21:30:35 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _RESIZE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
error = validateCpuQuota ( cpuQuota ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
2024-04-10 17:38:49 +02:00
values : { cpuQuota }
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await safe ( addTask ( appId , ISTATE _PENDING _RESIZE , task , auditSource ) ) ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , cpuQuota , taskId } ) ;
2020-01-28 21:30:35 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-01-28 21:30:35 -08:00
}
2026-02-14 16:34:34 +01:00
// does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest
// re-configure can take a dockerImage but not a manifest because re-configure does not clean up addons
2021-08-20 09:19:44 -07:00
async function setMounts ( app , mounts , auditSource ) {
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-10-28 19:42:48 -07:00
assert ( Array . isArray ( mounts ) ) ;
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _RECREATE _CONTAINER ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-04-29 21:55:21 -07:00
const task = {
args : { } ,
2020-10-28 19:42:48 -07:00
values : { mounts }
2020-04-29 21:55:21 -07:00
} ;
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
const [ taskError , taskId ] = await safe ( addTask ( appId , ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ) ;
2021-08-20 09:19:44 -07:00
if ( taskError && taskError . reason === BoxError . ALREADY _EXISTS ) throw new BoxError ( BoxError . CONFLICT , 'Duplicate mount points' ) ;
if ( taskError ) throw taskError ;
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mounts , taskId } ) ;
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-04-29 21:55:21 -07:00
}
2024-12-05 13:47:59 +01:00
async function setDevices ( app , devices , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof devices , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _RECREATE _CONTAINER ) ;
2024-12-05 15:16:06 +01:00
if ( error ) throw error ;
error = validateDevices ( devices ) ;
2024-12-05 13:47:59 +01:00
if ( error ) throw error ;
const task = {
args : { } ,
values : { devices }
} ;
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
const [ taskError , taskId ] = await safe ( addTask ( appId , ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ) ;
2024-12-05 13:47:59 +01:00
if ( taskError ) throw taskError ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , devices , taskId } ) ;
return { taskId } ;
}
2021-08-20 09:19:44 -07:00
async function setEnvironment ( app , env , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof env , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _RECREATE _CONTAINER ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateEnv ( env ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { env }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , env , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setDebugMode ( app , debugMode , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof debugMode , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _DEBUG ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateDebugMode ( debugMode ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { debugMode }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _DEBUG , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , debugMode , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMailbox ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-03-16 22:38:59 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-10-01 12:09:13 -07:00
assert . strictEqual ( typeof data . enable , 'boolean' ) ;
const enableMailbox = data . enable ;
2021-03-16 22:38:59 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _SERVICES _CHANGE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-03-30 22:18:39 -07:00
2021-10-01 11:19:26 -07:00
if ( ! app . manifest . addons ? . sendmail ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use sendmail' ) ;
const optional = 'optional' in app . manifest . addons . sendmail ? app . manifest . addons . sendmail . optional : false ;
2021-10-01 12:09:13 -07:00
if ( ! optional && ! enableMailbox ) throw new BoxError ( BoxError . BAD _FIELD , 'App requires sendmail to be enabled' ) ;
2021-06-29 14:26:34 -07:00
2022-05-31 17:53:09 -07:00
const mailboxDisplayName = data . mailboxDisplayName || '' ;
2021-10-01 12:09:13 -07:00
let mailboxName = data . mailboxName || null ;
const mailboxDomain = data . mailboxDomain || null ;
2019-09-21 19:45:55 -07:00
2021-10-01 12:09:13 -07:00
if ( enableMailbox ) {
await mail . getDomain ( mailboxDomain ) ; // check if domain exists
if ( mailboxName ) {
error = mail . validateName ( mailboxName ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
2021-10-01 12:09:13 -07:00
} else {
2022-01-16 12:32:12 -08:00
mailboxName = mailboxNameForSubdomain ( app . subdomain , app . domain , app . manifest ) ;
2021-10-01 12:09:13 -07:00
}
2022-05-31 17:53:09 -07:00
if ( mailboxDisplayName ) {
error = mail . validateDisplayName ( mailboxDisplayName ) ;
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
}
2021-10-01 12:09:13 -07:00
}
const task = {
args : { } ,
2022-05-31 17:53:09 -07:00
values : { enableMailbox , mailboxName , mailboxDomain , mailboxDisplayName }
2021-10-01 12:09:13 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2021-10-01 12:09:13 -07:00
2022-05-31 17:53:09 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mailboxName , mailboxDomain , mailboxDisplayName , taskId } ) ;
2021-10-01 12:09:13 -07:00
return { taskId } ;
}
async function setInbox ( app , data , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof data . enable , 'boolean' ) ;
const enableInbox = data . enable ;
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _SERVICES _CHANGE ) ;
2021-10-01 12:09:13 -07:00
if ( error ) throw error ;
if ( ! app . manifest . addons ? . recvmail ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use recvmail addon' ) ;
const inboxName = data . inboxName || null ;
const inboxDomain = data . inboxDomain || null ;
if ( enableInbox ) {
const domain = await mail . getDomain ( data . inboxDomain ) ; // check if domain exists
if ( ! domain . enabled ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain does not have incoming email enabled' ) ;
error = mail . validateName ( data . inboxName ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
2021-08-20 09:19:44 -07:00
}
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
2021-10-01 12:09:13 -07:00
values : { enableInbox , inboxName , inboxDomain }
2021-08-20 09:19:44 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2019-11-14 21:43:14 -08:00
2021-10-01 12:09:13 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableInbox , inboxName , inboxDomain , taskId } ) ;
2019-11-14 21:43:14 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2023-07-13 15:06:07 +05:30
async function setTurn ( app , enableTurn , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enableTurn , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _SERVICES _CHANGE ) ;
2023-07-13 15:06:07 +05:30
if ( error ) throw error ;
if ( ! app . manifest . addons ? . turn ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use turn addon' ) ;
if ( ! app . manifest . addons . turn . optional ) throw new BoxError ( BoxError . BAD _FIELD , 'turn service is not optional' ) ;
const task = {
args : { } ,
values : { enableTurn }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2023-07-13 15:06:07 +05:30
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableTurn , taskId } ) ;
return { taskId } ;
}
2023-07-13 16:37:33 +05:30
async function setRedis ( app , enableRedis , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enableRedis , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _SERVICES _CHANGE ) ;
2023-07-13 16:37:33 +05:30
if ( error ) throw error ;
if ( ! app . manifest . addons ? . redis ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use redis addon' ) ;
if ( ! app . manifest . addons . redis . optional ) throw new BoxError ( BoxError . BAD _FIELD , 'redis service is not optional' ) ;
const task = {
args : { } ,
values : { enableRedis }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2023-07-13 16:37:33 +05:30
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableRedis , taskId } ) ;
return { taskId } ;
}
2021-08-20 09:19:44 -07:00
async function setLocation ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let error = checkAppState ( app , ISTATE _PENDING _LOCATION _CHANGE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2022-01-16 18:43:19 -08:00
const values = {
subdomain : data . subdomain . toLowerCase ( ) ,
2020-03-29 17:11:10 -07:00
domain : data . domain . toLowerCase ( ) ,
// these are intentionally reset, if not set
2024-07-16 22:21:36 +02:00
portBindings : { } ,
2022-01-14 22:40:51 -08:00
secondaryDomains : [ ] ,
2022-01-14 22:29:47 -08:00
redirectDomains : [ ] ,
2021-01-18 17:26:26 -08:00
aliasDomains : [ ]
2020-03-29 17:11:10 -07:00
} ;
2019-09-21 19:45:55 -07:00
2024-07-16 22:21:36 +02:00
if ( 'ports' in data ) {
error = validatePorts ( data . ports || null , app . manifest ) ;
2024-02-25 16:28:57 +01:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
values . portBindings = translateToPortBindings ( data . ports || null , app . manifest ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-10-01 09:37:33 -07:00
// rename the auto-created mailbox to match the new location
2021-12-06 13:59:00 -08:00
if ( app . manifest . addons ? . sendmail && app . mailboxName ? . endsWith ( '.app' ) ) {
2022-01-16 12:32:12 -08:00
values . mailboxName = mailboxNameForSubdomain ( values . subdomain , app . manifest ) ;
2020-03-30 22:18:39 -07:00
values . mailboxDomain = values . domain ;
}
2019-09-08 16:57:08 -07:00
2022-01-20 16:57:30 -08:00
error = validateSecondaryDomains ( data . secondaryDomains || { } , app . manifest ) ;
if ( error ) throw error ;
values . secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2022-01-14 22:40:51 -08:00
2022-01-14 22:29:47 -08:00
if ( 'redirectDomains' in data ) {
values . redirectDomains = data . redirectDomains ;
2020-03-29 17:11:10 -07:00
}
2019-09-27 10:25:26 -07:00
2021-01-18 17:26:26 -08:00
if ( 'aliasDomains' in data ) {
values . aliasDomains = data . aliasDomains ;
}
2023-08-17 16:05:19 +05:30
const locations = [ new Location ( values . subdomain , values . domain , Location . TYPE _PRIMARY ) ]
. concat ( values . secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) )
. concat ( values . redirectDomains . map ( rd => new Location ( rd . subdomain , rd . domain , Location . TYPE _REDIRECT ) ) )
. concat ( values . aliasDomains . map ( ad => new Location ( ad . subdomain , ad . domain , Location . TYPE _ALIAS ) ) ) ;
2019-09-08 16:57:08 -07:00
2022-11-28 22:16:22 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : {
2025-02-13 14:03:25 +01:00
oldConfig : _ . pick ( app , [ 'subdomain' , 'domain' , 'fqdn' , 'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'portBindings' ] ) ,
2021-08-20 09:19:44 -07:00
skipDnsSetup : ! ! data . skipDnsSetup ,
overwriteDns : ! ! data . overwriteDns
} ,
values
} ;
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
const [ taskError , taskId ] = await safe ( addTask ( appId , ISTATE _PENDING _LOCATION _CHANGE , task , auditSource ) ) ;
2024-05-13 08:43:28 +02:00
if ( taskError && taskError . reason !== BoxError . ALREADY _EXISTS ) throw taskError ;
2024-07-16 22:21:36 +02:00
if ( taskError && taskError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( taskError . message , locations , values . portBindings ) ;
2019-09-08 16:57:08 -07:00
2022-11-28 21:23:06 +01:00
values . fqdn = dns . fqdn ( values . subdomain , values . domain ) ;
values . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
values . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
values . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2019-09-27 11:24:21 -07:00
2023-05-25 11:27:23 +02:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , Object . assign ( { appId , app , taskId } , values ) ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2022-06-01 22:44:52 -07:00
async function setStorage ( app , volumeId , volumePrefix , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2022-06-01 22:44:52 -07:00
assert ( volumeId === null || typeof volumeId === 'string' ) ;
assert ( volumePrefix === null || typeof volumePrefix === 'string' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _DATA _DIR _MIGRATION ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2022-06-01 22:44:52 -07:00
if ( volumeId ) {
2022-06-08 12:24:11 -07:00
await checkStorage ( app , volumeId , volumePrefix ) ;
2022-06-03 09:10:37 -07:00
} else {
volumeId = volumePrefix = null ;
2022-06-01 22:44:52 -07:00
}
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
2022-06-01 22:44:52 -07:00
args : { newStorageVolumeId : volumeId , newStorageVolumePrefix : volumePrefix } ,
2021-09-30 10:31:50 -07:00
values : { }
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _DATA _DIR _MIGRATION , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2022-06-01 22:44:52 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , volumeId , volumePrefix , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function updateApp ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2016-06-04 19:06:16 -07:00
assert ( data && typeof data === 'object' ) ;
2020-03-30 15:05:37 -07:00
assert ( data . manifest && typeof data . manifest === 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2024-07-17 08:58:43 +02:00
const skipBackup = ! ! data . skipBackup , appId = app . id , manifest = data . manifest ;
2025-06-26 17:08:14 +02:00
const values = { updateInfo : null } ; // clear update indicator immediately
2020-03-31 15:44:46 -07:00
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
if ( app . runState === RSTATE _STOPPED ) throw new BoxError ( BoxError . BAD _STATE , 'Stopped apps cannot be updated' ) ;
2019-09-26 20:10:11 -07:00
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
let error = checkAppState ( app , ISTATE _PENDING _UPDATE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-06-04 19:06:16 -07:00
2020-03-29 17:11:10 -07:00
error = manifestFormat . parse ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , 'Manifest error:' + error . message ) ;
2015-07-20 00:09:47 -07:00
2026-02-14 19:01:00 +01:00
if ( data . sourceArchiveFilePath ) manifest . dockerImage = ` local/ ${ manifest . id } : ${ manifest . version } - ${ Date . now ( ) } ` ;
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2026-02-05 17:29:00 +01:00
const updateConfig = { skipBackup , manifest } ; // this will clear appStoreId/versionsUrl when updating from a repo and set it if passed in for update route
2024-07-17 08:58:43 +02:00
if ( 'appStoreId' in data ) updateConfig . appStoreId = data . appStoreId ;
2026-02-05 17:29:00 +01:00
if ( 'versionsUrl' in data ) updateConfig . versionsUrl = data . versionsUrl ;
2016-03-25 11:35:47 -07:00
2020-03-29 17:11:10 -07:00
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if ( app . manifest . id !== updateConfig . manifest . id ) {
2021-08-20 09:19:44 -07:00
if ( ! data . force ) throw new BoxError ( BoxError . BAD _FIELD , 'manifest id does not match. force to override' ) ;
2020-03-29 17:11:10 -07:00
}
2019-01-11 14:19:32 -08:00
2020-03-29 17:11:10 -07:00
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver . prerelease ( app . manifest . version ) ? app . manifest . version : ` ${ app . manifest . version } -0 ` ;
const updateVersion = semver . prerelease ( updateConfig . manifest . version ) ? updateConfig . manifest . version : ` ${ updateConfig . manifest . version } -0 ` ;
2026-02-05 17:29:00 +01:00
if ( ( app . appStoreId !== '' || app . versionsUrl !== '' ) && semver . lte ( updateVersion , currentVersion ) ) {
if ( ! data . force ) throw new BoxError ( BoxError . BAD _FIELD , 'Downgrades are not permitted for apps installed from AppStore or Community. force to override' ) ;
2020-03-29 17:11:10 -07:00
}
2019-01-11 14:19:32 -08:00
2020-03-29 17:11:10 -07:00
if ( 'icon' in data ) {
if ( data . icon ) {
2021-04-30 13:18:15 -07:00
data . icon = Buffer . from ( data . icon , 'base64' ) ;
2025-02-10 14:58:36 +01:00
if ( data . icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' ) ;
2020-03-29 16:24:04 -07:00
}
2021-04-30 13:18:15 -07:00
values . icon = data . icon ;
2020-03-29 17:11:10 -07:00
}
2016-06-04 19:19:00 -07:00
2020-03-29 17:11:10 -07:00
// do not update apps in debug mode
2021-08-20 09:19:44 -07:00
if ( app . debugMode && ! data . force ) throw new BoxError ( BoxError . BAD _STATE , 'debug mode enabled. force to override' ) ;
2017-01-19 11:20:24 -08:00
2020-03-29 17:11:10 -07:00
// Ensure we update the memory limit in case the new app requires more memory as a minimum
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
if ( app . memoryLimit > 0 && updateConfig . manifest . memoryLimit && app . memoryLimit < updateConfig . manifest . memoryLimit ) {
updateConfig . memoryLimit = updateConfig . manifest . memoryLimit ;
}
2016-02-14 12:10:22 +01:00
2021-10-03 23:38:12 -07:00
if ( ! manifest . addons ? . sendmail ) { // clear if the update removed addon
2020-03-31 15:44:46 -07:00
values . mailboxName = values . mailboxDomain = null ;
2021-10-03 23:38:12 -07:00
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since update added the addon
2022-01-16 12:32:12 -08:00
values . mailboxName = mailboxNameForSubdomain ( app . subdomain , manifest ) ;
2020-03-31 15:44:46 -07:00
values . mailboxDomain = app . domain ;
}
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) { // clear if the update removed addon. required for fk constraint
values . enableInbox = false ;
values . inboxName = values . inboxDomain = null ;
}
2023-04-21 15:36:05 +02:00
const hasSso = ! ! updateConfig . manifest . addons ? . proxyAuth || ! ! updateConfig . manifest . addons ? . ldap || ! ! manifest . addons ? . oidc ;
2022-04-25 23:11:18 -07:00
if ( ! hasSso && app . sso ) values . sso = false ; // turn off sso flag, if the update removes sso options
2026-02-14 19:01:00 +01:00
if ( data . sourceArchiveFilePath ) await fileUtils . renameFile ( data . sourceArchiveFilePath , ` ${ paths . SOURCE _ARCHIVES _DIR } / ${ appId } .tar.gz ` ) ;
2026-01-27 20:41:26 +01:00
2020-03-29 17:11:10 -07:00
const task = {
args : { updateConfig } ,
2021-09-30 10:31:50 -07:00
values
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _UPDATE , task , auditSource ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _UPDATE , auditSource , { appId , app , skipBackup , toManifest : manifest , fromManifest : app . manifest , force : data . force , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function repair ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-11-23 18:35:51 -08:00
assert . strictEqual ( typeof data , 'object' ) ; // { manifest }
2019-09-19 17:04:11 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
let errorState = ( app . error && app . error . installationState ) || ISTATE _PENDING _CONFIGURE ;
2019-09-19 17:04:11 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { } ,
requiredState : null
} ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
// maybe split this into a separate route like reinstall?
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
if ( errorState === ISTATE _PENDING _INSTALL || errorState === ISTATE _PENDING _CLONE ) {
2021-02-24 14:49:30 -08:00
task . args = { skipDnsSetup : false , overwriteDns : true } ;
2020-03-29 17:11:10 -07:00
if ( data . manifest ) {
let error = manifestFormat . parse ( data . manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` manifest error: ${ error . message } ` ) ;
2019-09-19 17:04:11 -07:00
2024-03-30 18:25:33 +01:00
error = await checkManifest ( data . manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2021-10-01 12:09:13 -07:00
if ( ! data . manifest . addons ? . sendmail ) { // clear if repair removed addon
2020-03-31 15:44:46 -07:00
task . values . mailboxName = task . values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since repair added the addon
2022-01-16 12:32:12 -08:00
task . values . mailboxName = mailboxNameForSubdomain ( app . subdomain , data . manifest ) ;
2020-03-31 15:44:46 -07:00
task . values . mailboxDomain = app . domain ;
}
2020-03-29 17:11:10 -07:00
task . values . manifest = data . manifest ;
task . args . oldManifest = app . manifest ;
}
} else {
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
errorState = ISTATE _PENDING _CONFIGURE ;
2020-03-29 17:11:10 -07:00
if ( data . dockerImage ) {
2024-05-13 08:43:28 +02:00
const newManifest = Object . assign ( { } , app . manifest , { dockerImage : data . dockerImage } ) ;
2020-03-29 17:11:10 -07:00
task . values . manifest = newManifest ;
2019-11-23 18:35:51 -08:00
}
2020-03-29 17:11:10 -07:00
}
2019-09-21 19:45:55 -07:00
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , errorState , task , auditSource ) ;
2019-10-03 11:35:27 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _REPAIR , auditSource , { app , taskId } ) ;
2019-10-03 11:35:27 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-19 17:04:11 -07:00
}
2021-07-14 11:07:19 -07:00
async function restore ( app , backupId , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-12-05 21:15:09 -08:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2015-07-20 00:09:47 -07:00
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
let error = checkAppState ( app , ISTATE _PENDING _RESTORE ) ;
2021-07-14 11:07:19 -07:00
if ( error ) throw error ;
2020-03-29 17:11:10 -07:00
// for empty or null backupId, use existing manifest to mimic a reinstall
2025-07-25 12:55:14 +02:00
const backup = backupId ? await backups . get ( backupId ) : { manifest : app . manifest } ;
if ( ! backup ) throw new BoxError ( BoxError . BAD _FIELD , 'No such backup' ) ;
const manifest = backup . manifest ;
2019-09-21 19:45:55 -07:00
2024-02-27 11:35:14 +01:00
if ( ! manifest ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not get restore manifest' ) ;
2025-07-25 12:55:14 +02:00
if ( backup . encryptionVersion === 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool' ) ;
2015-07-20 00:09:47 -07:00
2021-07-14 11:07:19 -07:00
// re-validate because this new box version may not accept old configs
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-07-14 11:07:19 -07:00
if ( error ) throw error ;
2016-06-13 13:44:49 -07:00
2024-02-27 11:35:14 +01:00
const values = { manifest } ;
if ( ! manifest . addons ? . sendmail ) { // clear if restore removed addon
2021-07-14 11:07:19 -07:00
values . mailboxName = values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since restore added the addon
2024-02-27 11:35:14 +01:00
values . mailboxName = mailboxNameForSubdomain ( app . subdomain , manifest ) ;
2021-07-14 11:07:19 -07:00
values . mailboxDomain = app . domain ;
}
2016-06-13 18:11:11 -07:00
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) { // recvmail is always optional. clear if restore removed addon
values . enableInbox = false ;
values . inboxName = values . inboxDomain = null ;
}
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
2020-03-31 15:44:46 -07:00
2021-07-14 11:07:19 -07:00
const task = {
args : {
restoreConfig ,
oldManifest : app . manifest ,
skipDnsSetup : ! ! backupId , // if this is a restore, just skip dns setup. only re-installs should setup dns
overwriteDns : true
} ,
values
} ;
2015-08-19 10:54:39 -07:00
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
const taskId = await addTask ( appId , ISTATE _PENDING _RESTORE , task , auditSource ) ;
2015-08-19 10:54:39 -07:00
2025-07-25 12:55:14 +02:00
await eventlog . add ( eventlog . ACTION _APP _RESTORE , auditSource , { app , backupId : backup . id , remotePath : backup . remotePath , fromManifest : app . manifest , toManifest : manifest , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function importApp ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-10-15 09:16:29 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2019-10-15 09:16:29 -07:00
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
const error = checkAppState ( app , ISTATE _PENDING _IMPORT ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-12-04 18:54:25 -08:00
2025-08-01 23:20:51 +02:00
let restoreConfig ;
2022-10-02 10:08:50 +02:00
if ( data . remotePath ) { // if not provided, we import in-place
2025-09-12 09:48:37 +02:00
const backupSite = await backupSites . createPseudo ( {
2025-08-01 23:20:51 +02:00
id : ` appimport- ${ app . id } ` ,
provider : data . provider ,
config : data . config ,
format : data . format ,
2025-08-02 19:09:21 +02:00
encryptionPassword : data . encryptionPassword ? ? null ,
encryptedFilenames : data . encryptedFilenames ? ? false
2025-08-01 23:20:51 +02:00
} ) ;
2024-04-09 13:24:33 +02:00
2025-09-12 09:48:37 +02:00
restoreConfig = { remotePath : data . remotePath , backupSite } ;
2025-07-28 11:45:10 +02:00
} else { // inPlace
2025-08-01 13:16:57 +02:00
restoreConfig = { inPlace : true } ;
2022-10-02 10:08:50 +02:00
}
2019-10-15 09:16:29 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : {
restoreConfig ,
oldManifest : app . manifest ,
skipDnsSetup : false ,
overwriteDns : true
} ,
values : { }
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _IMPORT , task , auditSource ) ;
2019-10-15 09:16:29 -07:00
2025-08-01 23:20:51 +02:00
await eventlog . add ( eventlog . ACTION _APP _IMPORT , auditSource , { app , remotePath : data . remotePath , inPlace : data . inPlace , taskId } ) ;
2019-12-04 18:54:25 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-10-15 09:16:29 -07:00
}
2026-02-14 16:34:34 +01:00
function canBackupApp ( app ) {
// only backup apps that are installed or specific pending states
// stopped apps cannot be backed up because addons might be down (redis)
if ( app . runState === RSTATE _STOPPED ) return false ;
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app . installationState === ISTATE _INSTALLED ||
app . installationState === ISTATE _PENDING _CONFIGURE ||
app . installationState === ISTATE _PENDING _UPDATE ; // called from apptask
}
2025-09-22 17:59:26 +02:00
async function exportApp ( app , backupSiteId , auditSource ) {
2020-12-06 19:38:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2025-09-22 17:59:26 +02:00
assert . strictEqual ( typeof backupSiteId , 'string' ) ; // FIXME: this is not used at all in snapshotOnly mode
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-12-06 19:38:50 -08:00
const appId = app . id ;
2025-07-18 10:56:52 +02:00
if ( ! canBackupApp ( app ) ) throw new BoxError ( BoxError . BAD _STATE , 'App cannot be backed up in this state' ) ;
2020-12-06 19:38:50 -08:00
2025-09-22 17:59:26 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _APP _BACKUP _PREFIX } ${ app . id } ` , [ appId , backupSiteId , { snapshotOnly : true } ] ) ;
2025-07-18 10:56:52 +02:00
safe ( tasks . startTask ( taskId , { } ) , { debug } ) ; // background
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-12-06 19:38:50 -08:00
}
2021-08-20 09:19:44 -07:00
async function clone ( app , data , user , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-09-04 16:37:08 -07:00
assert ( user && typeof user === 'object' ) ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2022-01-16 12:32:12 -08:00
const subdomain = data . subdomain . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
2018-05-13 21:02:57 -07:00
backupId = data . backupId ,
2020-03-29 17:11:10 -07:00
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ,
2024-02-27 11:44:42 +01:00
skipDnsSetup = 'skipDnsSetup' in data ? data . skipDnsSetup : false ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2022-01-16 12:32:12 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2017-11-02 22:17:44 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2016-06-17 17:12:55 -05:00
2025-07-25 12:55:14 +02:00
const backup = await backups . get ( backupId ) ;
2016-06-17 17:12:55 -05:00
2025-07-25 12:55:14 +02:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( ! backup . manifest ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not detect restore manifest' ) ;
if ( backup . encryptionVersion === 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'This encrypted backup was created with an older Cloudron version and cannot be cloned' ) ;
2021-07-14 11:07:19 -07:00
2026-02-05 17:29:00 +01:00
const manifest = backup . manifest , appStoreId = app . appStoreId , versionsUrl = app . versionsUrl ;
2016-06-17 17:12:55 -05:00
2022-02-01 23:36:41 -08:00
let error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2023-08-17 16:05:19 +05:30
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) ) ;
2022-02-01 23:36:41 -08:00
2022-11-28 22:16:22 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2022-02-01 23:36:41 -08:00
2021-08-20 09:19:44 -07:00
// re-validate because this new box version may not accept old configs
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-06-17 17:12:55 -05:00
2024-07-16 22:21:36 +02:00
error = validatePorts ( data . ports || null , manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2018-12-12 12:55:35 -08:00
2021-08-20 09:19:44 -07:00
// should we copy the original app's mailbox settings instead?
2022-01-16 12:32:12 -08:00
const mailboxName = manifest . addons ? . sendmail ? mailboxNameForSubdomain ( subdomain , manifest ) : null ;
2021-10-01 09:37:33 -07:00
const mailboxDomain = manifest . addons ? . sendmail ? domain : null ;
2021-08-20 09:19:44 -07:00
2025-07-28 12:53:27 +02:00
const newAppId = crypto . randomUUID ( ) ;
2021-08-20 09:19:44 -07:00
2024-12-10 22:43:06 +01:00
// label, checklist intentionally omitted . icon is loaded in apptask from the backup
2025-07-25 12:55:14 +02:00
const dolly = _ . pick ( backup . appConfig || app , [ 'memoryLimit' , 'cpuQuota' , 'crontab' , 'reverseProxyConfig' , 'env' , 'servicesConfig' , 'tags' , 'devices' ,
2024-12-10 16:48:17 +01:00
'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'debugMode' ,
2024-12-10 17:16:06 +01:00
'enableTurn' , 'enableRedis' , 'mounts' , 'enableBackup' , 'enableAutomaticUpdate' , 'accessRestriction' , 'operators' , 'sso' ,
2025-02-13 14:03:25 +01:00
'notes' , 'checklist' ] ) ;
2024-02-27 11:49:12 +01:00
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) dolly . inboxDomain = null ; // needed because we are cloning _current_ app settings with old manifest
2024-02-27 11:49:12 +01:00
const obj = Object . assign ( dolly , {
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
installationState : ISTATE _PENDING _CLONE ,
runState : RSTATE _RUNNING ,
2022-02-01 23:36:41 -08:00
mailboxName ,
mailboxDomain ,
secondaryDomains ,
2022-01-14 22:29:47 -08:00
redirectDomains : [ ] ,
2021-08-20 09:19:44 -07:00
aliasDomains : [ ] ,
2024-12-10 21:07:59 +01:00
label : dolly . label ? ` ${ dolly . label } -clone ` : '' ,
2024-02-27 11:49:12 +01:00
} ) ;
2016-06-17 17:12:55 -05:00
2026-02-05 17:29:00 +01:00
const [ addError ] = await safe ( add ( newAppId , appStoreId , versionsUrl , manifest , subdomain , domain , portBindings , obj ) ) ;
2024-02-27 13:45:08 +01:00
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
2021-08-20 09:19:44 -07:00
if ( addError ) throw addError ;
2018-05-03 13:20:34 +02:00
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
2021-08-20 09:19:44 -07:00
const task = {
args : { restoreConfig , overwriteDns , skipDnsSetup , oldManifest : null } ,
values : { } ,
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
requiredState : ISTATE _PENDING _CLONE
2021-08-20 09:19:44 -07:00
} ;
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
const taskId = await addTask ( newAppId , ISTATE _PENDING _CLONE , task , auditSource ) ;
2018-01-11 10:59:30 -08:00
2026-02-05 21:51:55 +01:00
const newApp = Object . assign ( { } , _ . omit ( obj , [ 'icon' ] ) , { appStoreId , versionsUrl , manifest , subdomain , domain , portBindings } ) ;
2022-11-28 21:23:06 +01:00
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2021-01-18 17:26:26 -08:00
2025-07-25 12:55:14 +02:00
await eventlog . add ( eventlog . ACTION _APP _CLONE , auditSource , { appId : newAppId , oldAppId : app . id , backupId , remotePath : backup . remotePath , oldApp : app , newApp , taskId } ) ;
2020-03-29 17:11:10 -07:00
2021-08-20 09:19:44 -07:00
return { id : newAppId , taskId } ;
2016-06-17 17:12:55 -05:00
}
2024-12-10 17:19:12 +01:00
async function unarchive ( archive , data , auditSource ) {
assert . strictEqual ( typeof archive , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( archive . backupId ) ;
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
2024-12-10 19:05:31 +01:00
const subdomain = data . subdomain . toLowerCase ( ) ,
domain = data . domain . toLowerCase ( ) ,
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ;
2026-02-05 17:29:00 +01:00
const manifest = backup . manifest , appStoreId = backup . manifest . id , versionsUrl = backup . appConfig ? . versionsUrl || '' ;
2024-12-10 17:19:12 +01:00
2024-12-10 19:05:31 +01:00
let error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) ) ;
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
// re-validate because this new box version may not accept old configs
error = await checkManifest ( manifest ) ;
if ( error ) throw error ;
error = validatePorts ( data . ports || null , manifest ) ;
if ( error ) throw error ;
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2025-07-28 12:53:27 +02:00
const appId = crypto . randomUUID ( ) ;
2024-12-10 19:05:31 +01:00
2024-12-19 14:21:39 +01:00
// appConfig is null for pre-8.2 backups
2025-02-13 14:03:25 +01:00
const dolly = _ . pick ( backup . appConfig || { } , [ 'memoryLimit' , 'cpuQuota' , 'crontab' , 'reverseProxyConfig' , 'env' , 'servicesConfig' ,
2024-12-10 17:19:12 +01:00
'tags' , 'label' , 'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'devices' ,
'enableTurn' , 'enableRedis' , 'mounts' , 'enableBackup' , 'enableAutomaticUpdate' , 'accessRestriction' , 'operators' , 'sso' ,
2025-02-13 14:03:25 +01:00
'notes' , 'checklist' ] ) ;
2024-12-10 17:19:12 +01:00
// intentionally not filled up: redirectDomain, aliasDomains, mailboxDomain
2024-12-10 19:05:31 +01:00
const obj = Object . assign ( dolly , {
secondaryDomains ,
redirectDomains : [ ] ,
aliasDomains : [ ] ,
2024-12-10 17:19:12 +01:00
mailboxDomain : data . domain , // archive's mailboxDomain may not exist
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
runState : RSTATE _RUNNING ,
installationState : ISTATE _PENDING _INSTALL ,
2024-12-19 14:21:39 +01:00
sso : backup . appConfig ? backup . appConfig . sso : true // when no appConfig take a blind guess
2024-12-10 17:19:12 +01:00
} ) ;
2024-12-10 19:05:31 +01:00
obj . icon = ( await archives . getIcons ( archive . id ) ) ? . icon ;
2026-02-05 17:29:00 +01:00
const [ addError ] = await safe ( add ( appId , appStoreId , versionsUrl , manifest , subdomain , domain , portBindings , obj ) ) ;
2024-12-10 19:05:31 +01:00
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
if ( addError ) throw addError ;
const task = {
args : { restoreConfig , overwriteDns } ,
values : { } ,
requiredState : obj . installationState
} ;
const taskId = await addTask ( appId , obj . installationState , task , auditSource ) ;
2024-12-10 17:19:12 +01:00
2026-02-05 21:51:55 +01:00
const newApp = Object . assign ( { } , _ . omit ( obj , [ 'icon' ] ) , { appStoreId , versionsUrl , manifest , subdomain , domain , portBindings } ) ;
2024-12-10 19:05:31 +01:00
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
await eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId , app : newApp , taskId } ) ;
return { id : appId , taskId } ;
2024-12-10 17:19:12 +01:00
}
2021-08-20 09:19:44 -07:00
async function uninstall ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _UNINSTALL ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
values : { } ,
requiredState : null // can run in any state, as long as no task is active
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _UNINSTALL , task , auditSource ) ;
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _UNINSTALL , auditSource , { appId , app , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2024-12-09 18:28:35 +01:00
async function archive ( app , backupId , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2024-12-10 16:29:34 +01:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'cannot archive proxy app' ) ;
2025-07-25 01:34:29 +02:00
const result = await backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ;
2024-12-09 18:28:35 +01:00
if ( result . length === 0 ) throw new BoxError ( BoxError . BAD _STATE , 'No recent backup to archive' ) ;
if ( result [ 0 ] . id !== backupId ) throw new BoxError ( BoxError . BAD _STATE , 'Latest backup id has changed' ) ;
2024-12-09 23:20:44 +01:00
const icons = await getIcons ( app . id ) ;
2026-02-06 18:45:40 +01:00
const archiveId = await archives . add ( backupId , { icon : icons . icon , packageIcon : icons . packageIcon , appConfig : app } , auditSource ) ;
2024-12-09 18:28:35 +01:00
const { taskId } = await uninstall ( app , auditSource ) ;
2024-12-17 14:33:36 +01:00
return { taskId , id : archiveId } ;
2024-12-09 18:28:35 +01:00
}
2021-08-20 09:19:44 -07:00
async function start ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _START ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
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
values : { runState : RSTATE _RUNNING }
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _START , task , auditSource ) ;
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _START , auditSource , { appId , app , taskId } ) ;
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function stop ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _STOP ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
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
values : { runState : RSTATE _STOPPED }
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _STOP , task , auditSource ) ;
2019-08-29 09:10:39 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _STOP , auditSource , { appId , app , taskId } ) ;
2020-03-19 17:02:42 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function restart ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-12-20 10:29:29 -08:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
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
const error = checkAppState ( app , ISTATE _PENDING _RESTART ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-12-20 10:29:29 -08:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
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
values : { runState : RSTATE _RUNNING }
2020-03-29 17:11:10 -07:00
} ;
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
const taskId = await addTask ( appId , ISTATE _PENDING _RESTART , task , auditSource ) ;
2019-12-20 10:29:29 -08:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _RESTART , auditSource , { appId , app , taskId } ) ;
2020-03-19 17:02:42 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-12-20 10:29:29 -08:00
}
2026-02-14 16:34:34 +01:00
// auto-restart app tasks after a crash
2025-09-22 17:59:26 +02:00
async function backup ( app , backupSiteId , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2025-09-22 17:59:26 +02:00
assert . strictEqual ( typeof backupSiteId , 'string' ) ;
2021-09-30 10:45:25 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2025-07-18 10:56:52 +02:00
if ( ! canBackupApp ( app ) ) throw new BoxError ( BoxError . BAD _STATE , 'App cannot be backed up in this state' ) ;
2019-09-21 19:45:55 -07:00
2025-09-22 17:59:26 +02:00
const backupSite = await backupSites . get ( backupSiteId ) ;
if ( ! backupSite ) throw new BoxError ( BoxError . BAD _FIELD , 'No such backup site' ) ;
2015-07-20 00:09:47 -07:00
2025-09-12 09:48:37 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _APP _BACKUP _PREFIX } ${ app . id } ` , [ app . id , backupSite . id , { snapshotOnly : false } ] ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
const memoryLimit = backupSite . limits ? . memoryLimit ? Math . max ( backupSite . limits . memoryLimit / 1024 / 1024 , 1024 ) : 1024 ;
2025-07-18 10:56:52 +02:00
// background
tasks . startTask ( taskId , { timeout : 24 * 60 * 60 * 1000 /* 24 hours */ , nice : 15 , memoryLimit , oomScoreAdjust : - 999 } )
. then ( async ( backupId ) => {
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ; // if task crashed, no result
2025-07-18 10:56:52 +02:00
await eventlog . add ( eventlog . ACTION _APP _BACKUP _FINISH , auditSource , { app , success : ! ! backup , errorMessage : '' , remotePath : backup ? . remotePath , backupId : backupId } ) ;
} )
. catch ( async ( error ) => {
await eventlog . add ( eventlog . ACTION _APP _BACKUP _FINISH , auditSource , { app , success : false , errorMessage : error . message } ) ;
2025-07-18 18:11:56 +02:00
} )
. finally ( async ( ) => {
await locks . releaseByTaskId ( taskId ) ;
2025-07-18 10:56:52 +02:00
} ) ;
await eventlog . add ( eventlog . ACTION _APP _BACKUP , auditSource , { app , appId : app . id , taskId } ) ;
2019-08-27 20:55:49 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2022-04-02 17:09:08 -07:00
async function updateBackup ( app , backupId , data ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ;
2022-04-02 17:09:08 -07:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( backup . identifier !== app . id ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ; // some other app's backup
2025-07-25 11:49:13 +02:00
await backups . update ( backup , data ) ;
2022-04-02 17:09:08 -07:00
}
2022-11-03 22:13:57 +01:00
async function getBackupDownloadStream ( app , backupId ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ;
2022-11-03 22:13:57 +01:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( backup . identifier !== app . id ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ; // some other app's backup
2025-09-12 09:48:37 +02:00
const backupSite = await backupSites . get ( backup . siteId ) ;
if ( ! backupSite ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup site not found' ) ; // not possible
2025-11-05 18:14:46 +01:00
if ( backupSite . format !== 'tgz' ) throw new BoxError ( BoxError . BAD _STATE , 'only tgz backups can be downloaded' ) ;
2022-11-03 22:13:57 +01:00
2023-07-24 22:25:06 +05:30
const ps = new PassThrough ( ) ;
2025-09-12 09:48:37 +02:00
const stream = await backupSites . storageApi ( backupSite ) . download ( backupSite . config , backup . remotePath ) ;
2023-07-24 22:25:06 +05:30
stream . on ( 'error' , function ( error ) {
debug ( ` getBackupDownloadStream: read stream error: ${ error . message } ` ) ;
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , error ) ) ;
2022-11-03 22:13:57 +01:00
} ) ;
2023-07-24 22:25:06 +05:30
stream . pipe ( ps ) ;
const now = ( new Date ( ) ) . toISOString ( ) . replace ( /:|T/g , '-' ) . replace ( /\..*/ , '' ) ;
const encryptionSuffix = backup . encryptionVersion ? '.enc' : '' ;
const filename = ` app-backup- ${ now } ( ${ app . fqdn } ).tar.gz ${ encryptionSuffix } ` ;
return { stream : ps , filename } ;
2022-11-03 22:13:57 +01:00
}
2023-08-21 18:18:03 +05:30
async function restoreApps ( apps , options , auditSource ) {
assert ( Array . isArray ( apps ) ) ;
2021-02-24 16:29:43 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2016-05-24 10:33:10 -07:00
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
apps = apps . filter ( app => app . installationState !== ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
apps = apps . filter ( app => app . installationState !== ISTATE _PENDING _RESTORE ) ; // safeguard against tasks being created non-stop if we crash on startup
2021-08-20 09:19:44 -07:00
for ( const app of apps ) {
2025-11-26 14:39:16 +01:00
const [ error , results ] = await safe ( backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ) ;
2025-08-04 23:24:06 +02:00
let installationState , restoreConfig ;
2025-11-26 14:39:16 +01:00
if ( ! error && results . length ) {
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
installationState = ISTATE _PENDING _RESTORE ;
2025-12-05 16:12:59 +01:00
// intentionally ignore any backupSite provided during restore by the user because the site may not have all the apps
2025-11-26 14:39:16 +01:00
restoreConfig = { backupId : results [ 0 ] . id } ;
2021-08-20 09:19:44 -07:00
} else {
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
installationState = ISTATE _PENDING _INSTALL ;
2021-08-20 09:19:44 -07:00
restoreConfig = null ;
2021-07-14 11:07:19 -07:00
}
2017-11-17 22:29:13 -08:00
2021-08-20 09:19:44 -07:00
const task = {
2025-08-04 23:24:06 +02:00
args : { restoreConfig , skipDnsSetup : options . skipDnsSetup , overwriteDns : true , oldManifest : null } ,
2021-08-20 09:19:44 -07:00
values : { } ,
scheduleNow : false , // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId : false // ignore existing stale taskId
} ;
2016-05-24 10:33:10 -07:00
2025-11-26 14:39:16 +01:00
debug ( ` restoreApps: marking ${ app . fqdn } as ${ installationState } using restore config ${ JSON . stringify ( restoreConfig ) } ` ) ;
2019-10-24 10:39:47 -07:00
2021-11-17 10:38:02 -08:00
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , installationState , task , auditSource ) ) ;
2023-08-21 18:18:03 +05:30
if ( addTaskError ) debug ( ` restoreApps: error marking ${ app . fqdn } for restore: ${ JSON . stringify ( addTaskError ) } ` ) ;
2025-11-26 14:39:16 +01:00
else debug ( ` restoreApps: marked ${ app . id } as ${ installationState } with taskId ${ taskId } ` ) ;
2021-08-20 09:19:44 -07:00
}
}
2016-05-24 10:33:10 -07:00
2023-08-21 18:18:03 +05:30
async function configureApps ( apps , options , auditSource ) {
2023-07-21 17:10:25 +02:00
assert ( Array . isArray ( apps ) ) ;
2023-08-21 18:18:03 +05:30
assert . strictEqual ( typeof options , 'object' ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
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
apps = apps . filter ( app => app . installationState !== ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
apps = apps . filter ( app => app . installationState !== ISTATE _PENDING _CONFIGURE ) ; // safeguard against tasks being created non-stop if we crash on startup
2016-05-24 10:33:10 -07:00
2023-08-21 18:18:03 +05:30
const scheduleNow = ! ! options . scheduleNow ;
2021-08-20 09:19:44 -07:00
for ( const app of apps ) {
2023-08-21 18:18:03 +05:30
debug ( ` configureApps: marking ${ app . fqdn } for reconfigure (scheduleNow: ${ scheduleNow } ) ` ) ;
2016-06-16 06:38:47 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
values : { } ,
2023-08-21 18:18:03 +05:30
scheduleNow ,
2021-08-20 09:19:44 -07:00
requireNullTaskId : false // ignore existing stale taskId
} ;
2019-09-24 20:29:01 -07:00
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
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , ISTATE _PENDING _CONFIGURE , task , auditSource ) ) ;
2023-08-21 18:18:03 +05:30
if ( addTaskError ) debug ( ` configureApps: error marking ${ app . fqdn } for configure: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` configureApps: marked ${ app . id } for re-configure with taskId ${ taskId } ` ) ;
2021-08-20 09:19:44 -07:00
}
2016-05-24 10:33:10 -07:00
}
2017-08-18 20:45:52 -07:00
2021-11-17 10:33:28 -08:00
async function restartAppsUsingAddons ( changedAddons , auditSource ) {
2020-05-22 16:43:16 -07:00
assert ( Array . isArray ( changedAddons ) ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
let apps = await list ( ) ;
2023-07-14 08:34:02 +05:30
// TODO: This ends up restarting apps that have optional redis
2021-08-20 09:19:44 -07:00
apps = apps . filter ( app => app . manifest . addons && _ . intersection ( Object . keys ( app . manifest . addons ) , changedAddons ) . length !== 0 ) ;
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
apps = apps . filter ( app => app . installationState !== ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
apps = apps . filter ( app => app . installationState !== ISTATE _PENDING _RESTART ) ; // safeguard against tasks being created non-stop restart if we crash on startup
apps = apps . filter ( app => app . runState !== RSTATE _STOPPED ) ; // don't start stopped apps
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
for ( const app of apps ) {
debug ( ` restartAppsUsingAddons: marking ${ app . fqdn } for restart ` ) ;
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
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
values : { runState : RSTATE _RUNNING }
2021-08-20 09:19:44 -07:00
} ;
2020-08-31 17:53:29 -07:00
2021-08-20 09:19:44 -07:00
// stop apps before updating the databases because postgres will "lock" them preventing import
2021-08-25 19:41:46 -07:00
const [ stopError ] = await safe ( docker . stopContainers ( app . id ) ) ;
2021-08-20 09:19:44 -07:00
if ( stopError ) debug ( ` restartAppsUsingAddons: error stopping ${ app . fqdn } ` , stopError ) ;
2020-08-31 17:53:29 -07:00
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
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , ISTATE _PENDING _RESTART , task , auditSource ) ) ;
2021-08-20 09:19:44 -07:00
if ( addTaskError ) debug ( ` restartAppsUsingAddons: error marking ${ app . fqdn } for restart: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` restartAppsUsingAddons: marked ${ app . id } for restart with taskId ${ taskId } ` ) ;
}
2020-05-22 16:43:16 -07:00
}
2021-11-17 10:33:28 -08:00
async function schedulePendingTasks ( auditSource ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-09-24 20:29:01 -07:00
debug ( 'schedulePendingTasks: scheduling app tasks' ) ;
2019-09-24 10:28:50 -07:00
2021-08-25 19:41:46 -07:00
const result = await list ( ) ;
2019-09-24 10:28:50 -07:00
2021-09-07 09:57:49 -07:00
for ( const app of result ) {
if ( ! app . taskId ) continue ; // if not in any pending state, do nothing
2019-09-24 10:28:50 -07:00
2021-08-20 09:19:44 -07:00
debug ( ` schedulePendingTasks: schedule task for ${ app . fqdn } ${ app . id } : state= ${ app . installationState } ,taskId= ${ app . taskId } ` ) ;
2019-09-24 10:28:50 -07:00
2021-11-17 10:42:04 -08:00
await safe ( scheduleTask ( app . id , app . installationState , app . taskId , auditSource ) , { debug } ) ; // ignore error
2021-09-07 09:57:49 -07:00
}
2019-09-24 10:28:50 -07:00
}
2026-02-14 15:43:24 +01:00
export default {
canAccess ,
isOperator ,
accessLevel ,
pickFields ,
// database crud
add ,
update ,
setHealth ,
del ,
get ,
getByIpAddress ,
getByFqdn ,
list ,
listByUser ,
// user actions
install ,
unarchive ,
uninstall ,
archive ,
setAccessRestriction ,
setOperators ,
setCrontab ,
setUpstreamUri ,
setLabel ,
setIcon ,
setTags ,
setNotes ,
setChecklistItem ,
setMemoryLimit ,
setCpuQuota ,
setMounts ,
setDevices ,
setAutomaticBackup ,
setAutomaticUpdate ,
setReverseProxyConfig ,
setCertificate ,
setDebugMode ,
setEnvironment ,
setMailbox ,
setInbox ,
setTurn ,
setRedis ,
setLocation ,
setStorage ,
repair ,
restore ,
importApp ,
exportApp ,
clone ,
updateApp ,
backup ,
listBackups ,
updateBackup ,
getBackupDownloadStream ,
getTask ,
getLogPaths ,
getLogs ,
appendLogLine ,
start ,
stop ,
restart ,
createExec ,
startExec ,
getExec ,
checkManifest ,
restoreApps ,
configureApps ,
schedulePendingTasks ,
restartAppsUsingAddons ,
getStorageDir ,
getIcon ,
getMemoryLimit ,
getSchedulerConfig ,
listEventlog ,
downloadFile ,
uploadFile ,
writeConfig ,
loadConfig ,
canBackupApp ,
PORT _TYPE _TCP ,
PORT _TYPE _UDP ,
// task codes - the installation state is now a misnomer (keep in sync in UI)
ISTATE _PENDING _INSTALL ,
ISTATE _PENDING _CLONE ,
ISTATE _PENDING _CONFIGURE ,
ISTATE _PENDING _RECREATE _CONTAINER ,
ISTATE _PENDING _LOCATION _CHANGE ,
ISTATE _PENDING _SERVICES _CHANGE ,
ISTATE _PENDING _DATA _DIR _MIGRATION ,
ISTATE _PENDING _RESIZE ,
ISTATE _PENDING _DEBUG ,
ISTATE _PENDING _UNINSTALL ,
ISTATE _PENDING _RESTORE ,
ISTATE _PENDING _IMPORT ,
ISTATE _PENDING _UPDATE ,
ISTATE _PENDING _START ,
ISTATE _PENDING _STOP ,
ISTATE _PENDING _RESTART ,
ISTATE _ERROR ,
ISTATE _INSTALLED ,
// run states
RSTATE _RUNNING ,
RSTATE _STOPPED ,
// health states (keep in sync in UI)
HEALTH _HEALTHY : 'healthy' ,
HEALTH _UNHEALTHY : 'unhealthy' ,
HEALTH _ERROR : 'error' ,
HEALTH _DEAD : 'dead' ,
// app access levels
ACCESS _LEVEL _ADMIN ,
ACCESS _LEVEL _OPERATOR ,
ACCESS _LEVEL _USER ,
ACCESS _LEVEL _NONE ,
// exported for testing
_checkForPortBindingConflict : checkForPortBindingConflict ,
_validatePorts : validatePorts ,
_validateAccessRestriction : validateAccessRestriction ,
_validateUpstreamUri : validateUpstreamUri ,
_validateLocations : validateLocations ,
_parseCrontab : parseCrontab ,
_clear : clear
} ;