diff --git a/app/EmojiService.main.ts b/app/EmojiService.main.ts index d14e38b344..98a2377337 100644 --- a/app/EmojiService.main.ts +++ b/app/EmojiService.main.ts @@ -11,8 +11,9 @@ import type { OptionalResourceService } from './OptionalResourceService.main.js' import { SignalService as Proto } from '../ts/protobuf/index.std.js'; import { parseUnknown } from '../ts/util/schemas.std.js'; import { utf16ToEmoji } from '../ts/util/utf16ToEmoji.node.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; -const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json'); +const MANIFEST_PATH = join(getAppRootDir(), 'build', 'jumbomoji.json'); const manifestSchema = z.record(z.string(), z.string().array()); diff --git a/app/OptionalResourceService.main.ts b/app/OptionalResourceService.main.ts index 653811ae3b..d8cce850ee 100644 --- a/app/OptionalResourceService.main.ts +++ b/app/OptionalResourceService.main.ts @@ -15,15 +15,15 @@ import type { } from '../ts/types/OptionalResource.std.js'; import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource.std.js'; import { createLogger } from '../ts/logging/log.std.js'; -import { getGotOptions } from '../ts/updater/got.node.js'; +import { getGotOptions } from '../ts/updater/got.main.js'; import { drop } from '../ts/util/drop.std.js'; import { parseUnknown } from '../ts/util/schemas.std.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; const log = createLogger('OptionalResourceService'); const RESOURCES_DICT_PATH = join( - __dirname, - '..', + getAppRootDir(), 'build', 'optional-resources.json' ); diff --git a/app/SystemTrayService.main.ts b/app/SystemTrayService.main.ts index 83a7240e41..b290b8dea7 100644 --- a/app/SystemTrayService.main.ts +++ b/app/SystemTrayService.main.ts @@ -8,6 +8,7 @@ import { join } from 'node:path'; import { readFileSync } from 'node:fs'; import { createLogger } from '../ts/logging/log.std.js'; import type { LocalizerType } from '../ts/types/I18N.std.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; const log = createLogger('SystemTrayService'); @@ -285,8 +286,7 @@ function getTrayIconImagePath(size: number, unreadCount: number): string { } const iconPath = join( - __dirname, - '..', + getAppRootDir(), 'images', 'tray-icons', dirName, diff --git a/app/WindowsNotifications.main.ts b/app/WindowsNotifications.main.ts index 57c1583c22..cc4bc825c4 100644 --- a/app/WindowsNotifications.main.ts +++ b/app/WindowsNotifications.main.ts @@ -1,7 +1,7 @@ // Copyright 2017 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { ipcMain as ipc, app } from 'electron'; +import { ipcMain as ipc } from 'electron'; import type { IpcMainInvokeEvent } from 'electron'; import { join } from 'node:path'; import { Worker } from 'node:worker_threads'; @@ -15,6 +15,7 @@ import type { import { WindowsNotificationDataSchema } from '../ts/types/notifications.std.js'; import OS from '../ts/util/os/osMain.node.js'; import { createLogger } from '../ts/logging/log.std.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; const log = createLogger('WindowsNotifications'); @@ -22,7 +23,7 @@ let worker: Worker | undefined; if (OS.isWindows()) { const scriptPath = join( - app.getAppPath(), + getAppRootDir(), 'app', 'WindowsNotificationsWorker.node.js' ); diff --git a/app/config.main.ts b/app/config.main.ts index ac10f0b374..7f55e4fc82 100644 --- a/app/config.main.ts +++ b/app/config.main.ts @@ -13,6 +13,7 @@ import { parseEnvironment, } from '../ts/environment.std.js'; import { createLogger } from '../ts/logging/log.std.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; const log = createLogger('config'); @@ -28,7 +29,6 @@ if (app.isPackaged) { // Set environment vars to configure node-config before requiring it process.env.NODE_ENV = getEnvironment(); -process.env.NODE_CONFIG_DIR = join(__dirname, '..', 'config'); if (getEnvironment() === Environment.PackagedApp) { // harden production config against the local env @@ -43,8 +43,12 @@ if (getEnvironment() === Environment.PackagedApp) { process.env.SIGNAL_CI_CONFIG = ''; process.env.GENERATE_PRELOAD_CACHE = ''; process.env.REACT_DEVTOOLS = ''; + process.env.IS_BUNDLED = '1'; } +// Call `getAppRootDir()` after hardening since it relies on env variables +process.env.NODE_CONFIG_DIR = join(getAppRootDir(), 'config'); + // We load config after we've made our modifications to NODE_ENV // Note: we use `require()` because esbuild moves the imports to the top of // the module regardless of their actual placement in the file. diff --git a/app/dns-fallback.node.ts b/app/dns-fallback.main.ts similarity index 88% rename from app/dns-fallback.node.ts rename to app/dns-fallback.main.ts index e101578fe1..7c512b627b 100644 --- a/app/dns-fallback.node.ts +++ b/app/dns-fallback.main.ts @@ -6,6 +6,7 @@ import { readFile } from 'node:fs/promises'; import { DNSFallbackSchema } from '../ts/types/DNSFallback.std.js'; import type { DNSFallbackType } from '../ts/types/DNSFallback.std.js'; import { parseUnknown } from '../ts/util/schemas.std.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; import { createLogger } from '../ts/logging/log.std.js'; const log = createLogger('dns-fallback'); @@ -17,7 +18,7 @@ export async function getDNSFallback(): Promise { return cached; } - const configPath = join(__dirname, '..', 'build', 'dns-fallback.json'); + const configPath = join(getAppRootDir(), 'build', 'dns-fallback.json'); let str: string; try { str = await readFile(configPath, 'utf8'); diff --git a/app/locale.node.ts b/app/locale.node.ts index 4286903f6f..061eb7c87f 100644 --- a/app/locale.node.ts +++ b/app/locale.node.ts @@ -20,19 +20,25 @@ const { merge } = lodash; type CompactLocaleMessagesType = ReadonlyArray; type CompactLocaleKeysType = ReadonlyArray; -function getLocaleMessages(locale: string): LocaleMessagesType { - const targetFile = join(__dirname, '..', '_locales', locale, 'messages.json'); +function getLocaleMessages( + rootDir: string, + locale: string +): LocaleMessagesType { + const targetFile = join(rootDir, '_locales', locale, 'messages.json'); return JSON.parse(readFileSync(targetFile, 'utf-8')); } -function getCompactLocaleKeys(): CompactLocaleKeysType { - const targetFile = join(__dirname, '..', '_locales', 'keys.json'); +function getCompactLocaleKeys(rootDir: string): CompactLocaleKeysType { + const targetFile = join(rootDir, '_locales', 'keys.json'); return JSON.parse(readFileSync(targetFile, 'utf-8')); } -function getCompactLocaleValues(locale: string): CompactLocaleMessagesType { - const targetFile = join(__dirname, '..', '_locales', locale, 'values.json'); +function getCompactLocaleValues( + rootDir: string, + locale: string +): CompactLocaleMessagesType { + const targetFile = join(rootDir, '_locales', locale, 'values.json'); return JSON.parse(readFileSync(targetFile, 'utf-8')); } @@ -40,23 +46,13 @@ function getCompactLocaleValues(locale: string): CompactLocaleMessagesType { export type LocaleDisplayNames = Record>; export type CountryDisplayNames = Record>; -function getLocaleDisplayNames(): LocaleDisplayNames { - const targetFile = join( - __dirname, - '..', - 'build', - 'locale-display-names.json' - ); +function getLocaleDisplayNames(rootDir: string): LocaleDisplayNames { + const targetFile = join(rootDir, 'build', 'locale-display-names.json'); return JSON.parse(readFileSync(targetFile, 'utf-8')); } -function getCountryDisplayNames(): CountryDisplayNames { - const targetFile = join( - __dirname, - '..', - 'build', - 'country-display-names.json' - ); +function getCountryDisplayNames(rootDir: string): CountryDisplayNames { + const targetFile = join(rootDir, 'build', 'country-display-names.json'); return JSON.parse(readFileSync(targetFile, 'utf-8')); } @@ -77,16 +73,14 @@ function getLocaleDirection(localeName: string): LocaleDirection { return new Intl.Locale(localeName).getTextInfo().direction ?? 'ltr'; } -export function _getAvailableLocales(): Array { +export function _getAvailableLocales(rootDir: string): Array { return JSON.parse( - readFileSync( - join(__dirname, '..', 'build', 'available-locales.json'), - 'utf-8' - ) + readFileSync(join(rootDir, 'build', 'available-locales.json'), 'utf-8') ) as Array; } export function load({ + rootDir, hourCyclePreference, isPackaged, localeDirectionTestingOverride, @@ -94,6 +88,7 @@ export function load({ logger, preferredSystemLocales, }: { + rootDir: string; hourCyclePreference: HourCyclePreference; isPackaged: boolean; localeDirectionTestingOverride: LocaleDirection | null; @@ -109,7 +104,7 @@ export function load({ logger.warn('locale: `preferredSystemLocales` was empty'); } - const availableLocales = _getAvailableLocales(); + const availableLocales = _getAvailableLocales(rootDir); logger.info('locale: Supported locales:', availableLocales.join(', ')); logger.info('locale: Preferred locales:', preferredSystemLocales.join(', ')); @@ -124,14 +119,17 @@ export function load({ logger.info(`locale: Matched locale: ${matchedLocale}`); - const localeDisplayNames = getLocaleDisplayNames(); - const countryDisplayNames = getCountryDisplayNames(); + const localeDisplayNames = getLocaleDisplayNames(rootDir); + const countryDisplayNames = getCountryDisplayNames(rootDir); let finalMessages: LocaleMessagesType; if (isPackaged) { - const matchedLocaleMessages = getCompactLocaleValues(matchedLocale); - const englishMessages = getCompactLocaleValues('en'); - const keys = getCompactLocaleKeys(); + const matchedLocaleMessages = getCompactLocaleValues( + rootDir, + matchedLocale + ); + const englishMessages = getCompactLocaleValues(rootDir, 'en'); + const keys = getCompactLocaleKeys(rootDir); if (matchedLocaleMessages.length !== keys.length) { throw new Error( `Invalid "${matchedLocale}" entry count, ` + @@ -153,8 +151,8 @@ export function load({ }; } } else { - const matchedLocaleMessages = getLocaleMessages(matchedLocale); - const englishMessages = getLocaleMessages('en'); + const matchedLocaleMessages = getLocaleMessages(rootDir, matchedLocale); + const englishMessages = getLocaleMessages(rootDir, 'en'); // We start with english, then overwrite that with anything present in locale finalMessages = merge(englishMessages, matchedLocaleMessages); diff --git a/app/main.main.ts b/app/main.main.ts index 503ffa5766..3e48d0ffab 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -32,15 +32,12 @@ import { import type { MenuItemConstructorOptions, Settings } from 'electron'; import { z } from 'zod'; -import { - version as packageVersion, - productName, -} from '../ts/util/packageJson.node.js'; +import { packageJson } from '../ts/util/packageJson.main.js'; import * as GlobalErrors from './global_errors.main.js'; import { setup as setupCrashReports } from './crashReports.main.js'; import { setup as setupSpellChecker } from './spell_check.main.js'; -import { getDNSFallback } from './dns-fallback.node.js'; -import { redactAll, addSensitivePath } from '../ts/util/privacy.node.js'; +import { getDNSFallback } from './dns-fallback.main.js'; +import { redactAll, addSensitivePath } from './privacy.main.js'; import { createSupportUrl } from '../ts/util/createSupportUrl.std.js'; import { missingCaseError } from '../ts/util/missingCaseError.std.js'; import { strictAssert } from '../ts/util/assert.std.js'; @@ -137,6 +134,7 @@ import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas.std.js'; import { getAppErrorIcon } from '../ts/util/getAppErrorIcon.node.js'; import { promptOSAuth } from '../ts/util/os/promptOSAuthMain.main.js'; import { appRelaunch } from '../ts/util/relaunch.main.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; import { sendDummyKeystroke, show as showWindowsNotification, @@ -626,13 +624,14 @@ function isVisible(window: BoundsType, bounds: BoundsType) { } let windowIcon: string; +const rootDir = getAppRootDir(); if (OS.isWindows()) { - windowIcon = join(__dirname, '../build/icons/win/icon.ico'); + windowIcon = join(rootDir, 'build', 'icons', 'win', 'icon.ico'); } else if (OS.isLinux()) { - windowIcon = join(__dirname, '../images/signal-logo-desktop-linux.png'); + windowIcon = join(rootDir, 'images', 'signal-logo-desktop-linux.png'); } else { - windowIcon = join(__dirname, '../build/icons/png/512x512.png'); + windowIcon = join(rootDir, 'build', 'icons', 'png', '512x512.png'); } // The titlebar is hidden on: @@ -722,10 +721,10 @@ async function createWindow() { sandbox: false, contextIsolation: !isTestEnvironment(getEnvironment()), preload: join( - __dirname, - usePreloadBundle - ? '../preload.wrapper.js' - : '../ts/windows/main/preload.preload.js' + rootDir, + ...(usePreloadBundle + ? ['preload.wrapper.js'] + : ['ts', 'windows', 'main', 'preload.preload.js']) ), spellcheck, }, @@ -1056,8 +1055,8 @@ async function createWindow() { await safeLoadURL( mainWindow, getEnvironment() === Environment.Test - ? await prepareFileUrl([__dirname, '../test/index.html']) - : await prepareFileUrl([__dirname, '../background.html']) + ? await prepareFileUrl([rootDir, 'test', 'index.html']) + : await prepareFileUrl([rootDir, 'background.html']) ); } @@ -1304,7 +1303,7 @@ async function showScreenShareWindow(sourceName: string | undefined) { nodeIntegrationInWorker: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/screenShare/preload.preload.js'), + preload: join(rootDir, 'bundles', 'screenShare', 'preload.preload.js'), }, x: Math.floor(display.size.width / 2) - width / 2, y: 24, @@ -1326,7 +1325,7 @@ async function showScreenShareWindow(sourceName: string | undefined) { await safeLoadURL( screenShareWindow, - await prepareFileUrl([__dirname, '../screenShare.html'], { sourceName }) + await prepareFileUrl([rootDir, 'screenShare.html'], { sourceName }) ); } @@ -1352,7 +1351,7 @@ async function showAbout() { nodeIntegrationInWorker: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/about/preload.preload.js'), + preload: join(rootDir, 'bundles', 'about', 'preload.preload.js'), nativeWindowOpen: true, }, }; @@ -1371,10 +1370,7 @@ async function showAbout() { } }); - await safeLoadURL( - aboutWindow, - await prepareFileUrl([__dirname, '../about.html']) - ); + await safeLoadURL(aboutWindow, await prepareFileUrl([rootDir, 'about.html'])); } async function getIsLinked() { @@ -1416,7 +1412,7 @@ async function showDebugLogWindow(options: DebugLogWindowOptions = {}) { if (debugLogWindow) { if (debugLogCurrentMode !== newMode) { debugLogCurrentMode = newMode; - const url = pathToFileURL(join(__dirname, '../debug_log.html')); + const url = pathToFileURL(join(rootDir, 'debug_log.html')); url.searchParams.set('mode', newMode); await safeLoadURL(debugLogWindow, url.href); } @@ -1457,7 +1453,7 @@ async function showDebugLogWindow(options: DebugLogWindowOptions = {}) { nodeIntegrationInWorker: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/debuglog/preload.preload.js'), + preload: join(rootDir, 'bundles', 'debuglog', 'preload.preload.js'), }, parent: mainWindow, }; @@ -1480,7 +1476,7 @@ async function showDebugLogWindow(options: DebugLogWindowOptions = {}) { } }); - const url = pathToFileURL(join(__dirname, '../debug_log.html')); + const url = pathToFileURL(join(rootDir, 'debug_log.html')); if (options.mode) { url.searchParams.set('mode', options.mode); } @@ -1527,7 +1523,7 @@ async function showCallDiagnosticWindow() { nodeIntegrationInWorker: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/calldiagnostic/preload.preload.js'), + preload: join(rootDir, 'bundles', 'calldiagnostic', 'preload.preload.js'), }, parent: mainWindow, }; @@ -1549,7 +1545,7 @@ async function showCallDiagnosticWindow() { } }); - const url = pathToFileURL(join(__dirname, '../call_diagnostic.html')); + const url = pathToFileURL(join(rootDir, 'call_diagnostic.html')); await safeLoadURL(callDiagnosticWindow, url.href); } @@ -1586,7 +1582,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { nodeIntegrationInWorker: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/permissions/preload.preload.js'), + preload: join(rootDir, 'bundles', 'permissions', 'preload.preload.js'), nativeWindowOpen: true, }, parent: mainWindow, @@ -1612,7 +1608,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { await safeLoadURL( permissionsPopupWindow, - await prepareFileUrl([__dirname, '../permissions_popup.html'], { + await prepareFileUrl([rootDir, 'permissions_popup.html'], { forCalling, forCamera, }) @@ -1790,7 +1786,7 @@ function handleSafeStorageDecryptionError(): 'continue' | 'quit' { cancelId: copyErrorAndQuitIndex, message, detail, - icon: getAppErrorIcon(), + icon: getAppErrorIcon(rootDir), noLink: true, }); if (resultIndex === copyErrorAndQuitIndex) { @@ -1951,7 +1947,7 @@ const onDatabaseInitializationError = async (error: Error) => { cancelId: copyErrorAndQuitButtonIndex, message: messageTitle, detail: messageDetail, - icon: getAppErrorIcon(), + icon: getAppErrorIcon(rootDir), noLink: true, }); @@ -1977,7 +1973,7 @@ const onDatabaseInitializationError = async (error: Error) => { cancelId: cancelButtonIndex, message: i18n('icu:databaseError__deleteDataConfirmation'), detail: i18n('icu:databaseError__deleteDataConfirmation__detail'), - icon: getAppErrorIcon(), + icon: getAppErrorIcon(rootDir), noLink: true, }); @@ -2080,7 +2076,7 @@ app.on('ready', async () => { const [userDataPath, crashDumpsPath, installPath] = await Promise.all([ realpath(app.getPath('userData')), realpath(app.getPath('crashDumps')), - realpath(app.getAppPath()), + realpath(rootDir), ]); updateDefaultSession(session.defaultSession, log); @@ -2120,6 +2116,7 @@ app.on('ready', async () => { log.info('app.ready: preferred system locales:', preferredSystemLocales); resolvedTranslationsLocale = loadLocale({ + rootDir, hourCyclePreference, isPackaged: app.isPackaged, localeDirectionTestingOverride, @@ -2233,7 +2230,7 @@ app.on('ready', async () => { } log.info('app ready'); - log.info(`starting version ${packageVersion}`); + log.info(`starting version ${packageJson.version}`); // This logging helps us debug user reports about broken devices. { @@ -2292,7 +2289,7 @@ app.on('ready', async () => { nodeIntegration: false, sandbox: true, contextIsolation: true, - preload: join(__dirname, '../bundles/loading/preload.preload.js'), + preload: join(rootDir, 'bundles', 'loading', 'preload.preload.js'), }, icon: windowIcon, }); @@ -2310,7 +2307,7 @@ app.on('ready', async () => { await safeLoadURL( loadingWindow, - await prepareFileUrl([__dirname, '../loading.html']) + await prepareFileUrl([rootDir, 'loading.html']) ); }) ); @@ -2863,7 +2860,7 @@ ipc.on('get-config', async event => { } const parsed = safeParseLoose(rendererConfigSchema, { - name: productName, + name: packageJson.productName, availableLocales: getResolvedMessagesLocale().availableLocales, resolvedTranslationsLocale: getResolvedMessagesLocale().name, resolvedTranslationsLocaleDirection: getResolvedMessagesLocale().direction, @@ -2915,7 +2912,7 @@ ipc.on('get-config', async event => { // paths crashDumpsPath: app.getPath('crashDumps'), homePath: app.getPath('home'), - installPath: app.getAppPath(), + installPath: rootDir, userDataPath: app.getPath('userData'), directoryConfig: directoryConfig.data, @@ -3411,8 +3408,11 @@ async function showStickerCreatorWindow() { sandbox: true, contextIsolation: true, preload: join( - __dirname, - '../ts/windows/sticker-creator/preload.preload.js' + rootDir, + 'ts', + 'windows', + 'sticker-creator', + 'preload.preload.js' ), nativeWindowOpen: true, }, @@ -3432,7 +3432,7 @@ async function showStickerCreatorWindow() { await safeLoadURL( stickerCreatorWindow, - await prepareFileUrl([__dirname, '../sticker-creator/dist/index.html']) + await prepareFileUrl([rootDir, 'sticker-creator', 'dist', 'index.html']) ); } diff --git a/app/privacy.main.ts b/app/privacy.main.ts new file mode 100644 index 0000000000..479f13b024 --- /dev/null +++ b/app/privacy.main.ts @@ -0,0 +1,9 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { redactAll, addSensitivePath } from '../ts/util/privacy.node.js'; +import { getAppRootDir } from '../ts/util/appRootDir.main.js'; + +addSensitivePath(getAppRootDir()); + +export { redactAll, addSensitivePath }; diff --git a/app/startup_config.main.ts b/app/startup_config.main.ts index f32bbd7565..c01bd6d6b3 100644 --- a/app/startup_config.main.ts +++ b/app/startup_config.main.ts @@ -3,7 +3,7 @@ import { app } from 'electron'; -import { name } from '../ts/util/packageJson.node.js'; +import { packageJson } from '../ts/util/packageJson.main.js'; import { createLogger } from '../ts/logging/log.std.js'; import * as GlobalErrors from './global_errors.main.js'; @@ -15,7 +15,7 @@ GlobalErrors.addHandler(); // set such that only we have read access to our files process.umask(0o077); -export const AUMID = `org.whispersystems.${name}`; +export const AUMID = `org.whispersystems.${packageJson.name}`; log.info('Set Windows Application User Model ID (AUMID)', { AUMID, }); diff --git a/package.json b/package.json index c56be501ae..0077a7f787 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "generate:phase-1": "run-p --aggregate-output --print-label generate:phase-1:bundle build:icu-types build:compact-locales build:styles get-expire-time copy-components build:policy-files", "generate:phase-1:bundle": "run-s build:protobuf build:esbuild:bundle", "build-release": "pnpm run build", - "sign-release": "node ts/updater/generateSignature.js", "notarize": "echo 'No longer necessary'", "get-strings": "ts-node ts/scripts/get-strings.node.ts && ts-node ts/scripts/gen-nsis-script.node.ts && ts-node ts/scripts/gen-locales-config.node.ts && run-p get-strings:locales get-strings:countries get-strings:emoji mark-unusued-strings-deleted && run-p build:compact-locales", "get-strings:locales": "ts-node ./ts/scripts/build-localized-display-names.node.ts locales ts/scripts/locale-data/locale-display-names.csv build/locale-display-names.json", diff --git a/test/setup-test-node.js b/test/setup-test-node.js index 577e0be147..dc717e9ef1 100644 --- a/test/setup-test-node.js +++ b/test/setup-test-node.js @@ -7,7 +7,7 @@ const chaiAsPromised = require('chai-as-promised'); const { Crypto } = require('../ts/context/Crypto.node.js'); const { setEnvironment, Environment } = require('../ts/environment.std.js'); const { HourCyclePreference } = require('../ts/types/I18N.std.js'); -const { default: package } = require('../ts/util/packageJson.node.js'); +const { packageJson } = require('../ts/util/packageJson.node.js'); chai.use(chaiAsPromised); @@ -20,14 +20,14 @@ global.window = { SignalContext: { i18n: key => `i18n(${key})`, getPath: () => '/tmp', - getVersion: () => package.version, + getVersion: () => packageJson.version, config: { serverUrl: 'https://127.0.0.1:9', storageUrl: 'https://127.0.0.1:9', updatesUrl: 'https://127.0.0.1:9', resourcesUrl: 'https://127.0.0.1:9', - certificateAuthority: package.certificateAuthority, - version: package.version, + certificateAuthority: packageJson.certificateAuthority, + version: packageJson.version, }, crypto: new Crypto(), getResolvedMessagesLocale: () => 'en', diff --git a/ts/scripts/build-localized-display-names.node.ts b/ts/scripts/build-localized-display-names.node.ts index c4c34a51a9..13972ba37f 100644 --- a/ts/scripts/build-localized-display-names.node.ts +++ b/ts/scripts/build-localized-display-names.node.ts @@ -3,6 +3,7 @@ import { parse } from 'csv-parse'; import fs from 'node:fs/promises'; +import { join } from 'node:path'; import { z } from 'zod'; import { _getAvailableLocales } from '../../app/locale.node.js'; import { parseUnknown } from '../util/schemas.std.js'; @@ -21,8 +22,9 @@ if (!process.argv[4]) { throw new Error('Missing third argument: output json file'); } const localeDisplayNamesBuildPath = process.argv[4]; +const rootDir = join(__dirname, '..', '..'); -const availableLocales = _getAvailableLocales(); +const availableLocales = _getAvailableLocales(rootDir); const LocaleString = z.string().refine(arg => { try { diff --git a/ts/scripts/check-min-os-version.node.ts b/ts/scripts/check-min-os-version.node.ts index 6507908025..c48f1e95dc 100644 --- a/ts/scripts/check-min-os-version.node.ts +++ b/ts/scripts/check-min-os-version.node.ts @@ -14,7 +14,7 @@ import { Format, NtExecutable } from 'pe-library'; import ELECTRON_BINARY from 'electron'; import { drop } from '../util/drop.std.js'; -import packageJson from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; const { ImageDosHeader, ImageNtHeaders, ImageDirectoryEntry } = Format; diff --git a/ts/scripts/get-expire-time.node.ts b/ts/scripts/get-expire-time.node.ts index 7938f74d37..8db26d3179 100644 --- a/ts/scripts/get-expire-time.node.ts +++ b/ts/scripts/get-expire-time.node.ts @@ -6,7 +6,7 @@ import { execSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; import { DAY } from '../util/durations/index.std.js'; -import { version } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; import { isNotUpdatable } from '../util/version.std.js'; const unixTimestamp = parseInt( @@ -18,7 +18,7 @@ const buildCreation = unixTimestamp * 1000; // NB: Build expirations are also determined via users' auto-update settings; see // getExpirationTimestamp -const validDuration = isNotUpdatable(version) ? DAY * 30 : DAY * 90; +const validDuration = isNotUpdatable(packageJson.version) ? DAY * 30 : DAY * 90; const buildExpiration = buildCreation + validDuration; const localProductionPath = join( @@ -29,7 +29,7 @@ const localProductionPath = join( const localProductionConfig = { buildCreation, buildExpiration, - ...(isNotUpdatable(version) ? { updatesEnabled: false } : {}), + ...(isNotUpdatable(packageJson.version) ? { updatesEnabled: false } : {}), }; writeFileSync( diff --git a/ts/scripts/notarize-universal-dmg.node.ts b/ts/scripts/notarize-universal-dmg.node.ts index 0035b26bae..d34d1e449e 100644 --- a/ts/scripts/notarize-universal-dmg.node.ts +++ b/ts/scripts/notarize-universal-dmg.node.ts @@ -5,7 +5,7 @@ import type { BuildResult } from 'electron-builder'; import { notarize } from '@electron/notarize'; -import { build } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; export async function afterAllArtifactBuild({ platformToTargets, @@ -24,7 +24,7 @@ export async function afterAllArtifactBuild({ return; } - const appBundleId = build.appId; + const appBundleId = packageJson.build.appId; if (!appBundleId) { throw new Error( 'appBundleId must be provided in package.json: build.appId' diff --git a/ts/scripts/notarize.node.ts b/ts/scripts/notarize.node.ts index a9e686040e..4ebf89e6df 100644 --- a/ts/scripts/notarize.node.ts +++ b/ts/scripts/notarize.node.ts @@ -6,7 +6,7 @@ import type { AfterPackContext } from 'electron-builder'; import { notarize } from '@electron/notarize'; -import { build } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; export async function afterSign({ appOutDir, @@ -22,7 +22,7 @@ export async function afterSign({ const appPath = path.join(appOutDir, `${productFilename}.app`); - const appBundleId = build.appId; + const appBundleId = packageJson.build.appId; if (!appBundleId) { throw new Error( 'appBundleId must be provided in package.json: build.appId' diff --git a/ts/scripts/publish-installer-size.node.ts b/ts/scripts/publish-installer-size.node.ts index 1f0c9c3e21..0cfa407ec4 100644 --- a/ts/scripts/publish-installer-size.node.ts +++ b/ts/scripts/publish-installer-size.node.ts @@ -4,7 +4,10 @@ import { stat } from 'node:fs/promises'; import { join } from 'node:path'; -import { name as NAME, version as VERSION } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; + +const NAME = packageJson.name; +const VERSION = packageJson.version; const SUPPORT_CONFIG = new Set([ 'linux', diff --git a/ts/scripts/test-release.node.ts b/ts/scripts/test-release.node.ts index 1dbb427abd..5baa17cfd0 100644 --- a/ts/scripts/test-release.node.ts +++ b/ts/scripts/test-release.node.ts @@ -9,7 +9,7 @@ import { mkdtemp, cp } from 'node:fs/promises'; import { constants as fsConstants } from 'node:fs'; import { _electron as electron } from 'playwright'; -import { productName, name } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.node.js'; import { gracefulRmRecursive } from '../util/gracefulFs.node.js'; import { consoleLogger } from '../util/consoleLogger.std.js'; @@ -21,24 +21,24 @@ let exe: string; if (process.platform === 'darwin') { archive = join( 'mac-arm64', - `${productName}.app`, + `${packageJson.productName}.app`, 'Contents', 'Resources', 'app.asar' ); exe = join( 'mac-arm64', - `${productName}.app`, + `${packageJson.productName}.app`, 'Contents', 'MacOS', - productName + packageJson.productName ); } else if (process.platform === 'win32') { archive = join('win-unpacked', 'resources', 'app.asar'); - exe = join('win-unpacked', `${productName}.exe`); + exe = join('win-unpacked', `${packageJson.productName}.exe`); } else if (process.platform === 'linux') { archive = join('linux-unpacked', 'resources', 'app.asar'); - exe = join('linux-unpacked', name); + exe = join('linux-unpacked', packageJson.name); } else { throw new Error(`Unsupported platform: ${process.platform}`); } @@ -87,7 +87,7 @@ const main = async () => { ); console.log('Checking window title'); - assert.strictEqual(await window.title(), productName); + assert.strictEqual(await window.title(), packageJson.productName); await app.close(); } finally { diff --git a/ts/sql/main.main.ts b/ts/sql/main.main.ts index 4aba051603..45909433b9 100644 --- a/ts/sql/main.main.ts +++ b/ts/sql/main.main.ts @@ -8,6 +8,7 @@ import { app } from 'electron'; import { strictAssert } from '../util/assert.std.js'; import { explodePromise } from '../util/explodePromise.std.js'; +import { getAppRootDir } from '../util/appRootDir.main.js'; import type { LoggerType } from '../types/Logging.std.js'; import * as Errors from '../types/errors.std.js'; import { SqliteErrorKind } from './errors.std.js'; @@ -551,12 +552,7 @@ export class MainSQL { } #createWorker(): CreateWorkerResultType { - const scriptPath = join( - app.getAppPath(), - 'ts', - 'sql', - 'mainWorker.node.js' - ); + const scriptPath = join(getAppRootDir(), 'ts', 'sql', 'mainWorker.node.js'); const worker = new Worker(scriptPath); diff --git a/ts/test-node/app/locale_test.main.ts b/ts/test-node/app/locale_test.main.ts index 560cb47345..73e745b6c0 100644 --- a/ts/test-node/app/locale_test.main.ts +++ b/ts/test-node/app/locale_test.main.ts @@ -1,6 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { join } from 'node:path'; import { assert } from 'chai'; import { stub } from 'sinon'; import * as LocaleMatcher from '@formatjs/intl-localematcher'; @@ -8,6 +9,8 @@ import { load, _getAvailableLocales } from '../../../app/locale.node.js'; import { FAKE_DEFAULT_LOCALE } from '../../../app/spell_check.main.js'; import { HourCyclePreference } from '../../types/I18N.std.js'; +const rootDir = join(__dirname, '..', '..', '..'); + describe('locale', async () => { describe('load', () => { it('resolves expected locales correctly', async () => { @@ -26,6 +29,7 @@ describe('locale', async () => { expectedLocale: string ) { const actualLocale = await load({ + rootDir, hourCyclePreference: HourCyclePreference.UnknownPreference, isPackaged: false, localeDirectionTestingOverride: null, @@ -150,7 +154,7 @@ describe('locale', async () => { 'VE', ]; - const availableLocales = _getAvailableLocales(); + const availableLocales = _getAvailableLocales(rootDir); for (const locale of SINGLE_REGION_LOCALES) { const { language } = new Intl.Locale(locale); diff --git a/ts/test-node/app/menu_test.node.ts b/ts/test-node/app/menu_test.node.ts index c50bd428b3..f266fdb307 100644 --- a/ts/test-node/app/menu_test.node.ts +++ b/ts/test-node/app/menu_test.node.ts @@ -1,6 +1,7 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { join } from 'node:path'; import { assert } from 'chai'; import { stub } from 'sinon'; import type { MenuItemConstructorOptions } from 'electron'; @@ -214,6 +215,7 @@ describe('createTemplate', () => { }; const { i18n } = loadLocale({ + rootDir: join(__dirname, '..', '..', '..'), hourCyclePreference: HourCyclePreference.UnknownPreference, isPackaged: false, localeDirectionTestingOverride: null, diff --git a/ts/test-node/updater/differential_test.node.ts b/ts/test-node/updater/differential_test.main.ts similarity index 98% rename from ts/test-node/updater/differential_test.node.ts rename to ts/test-node/updater/differential_test.main.ts index 99e24a50ac..25f050de28 100644 --- a/ts/test-node/updater/differential_test.node.ts +++ b/ts/test-node/updater/differential_test.main.ts @@ -9,14 +9,14 @@ import { tmpdir } from 'node:os'; import { strictAssert } from '../../util/assert.std.js'; import * as durations from '../../util/durations/index.std.js'; -import { getGotOptions } from '../../updater/got.node.js'; +import { getGotOptions } from '../../updater/got.main.js'; import { computeDiff, getBlockMapFileName, prepareDownload, isValidPreparedData, download, -} from '../../updater/differential.node.js'; +} from '../../updater/differential.main.js'; const FIXTURES = path.join(__dirname, '..', '..', '..', 'fixtures'); const CRLF = '\r\n'; diff --git a/ts/test-node/util/privacy_test.node.ts b/ts/test-node/util/privacy_test.node.ts index e76c019d59..1262b8582c 100644 --- a/ts/test-node/util/privacy_test.node.ts +++ b/ts/test-node/util/privacy_test.node.ts @@ -4,10 +4,12 @@ import { assert } from 'chai'; import * as Privacy from '../../util/privacy.node.js'; -import { APP_ROOT_PATH } from '../../util/privacy.node.js'; Privacy.addSensitivePath('sensitive-path'); +const APP_ROOT_PATH = __dirname; +Privacy.addSensitivePath(APP_ROOT_PATH); + describe('Privacy', () => { describe('redactCardNumbers', () => { it('should redact anything that looks like a credit card', () => { diff --git a/ts/types/packageJson.d.ts b/ts/types/packageJson.d.ts new file mode 100644 index 0000000000..18571536a3 --- /dev/null +++ b/ts/types/packageJson.d.ts @@ -0,0 +1,24 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReadonlyDeep } from 'type-fest'; + +export type PackageJsonType = ReadonlyDeep<{ + name: string; + version: string; + productName: string; + build: { + appId: string; + mac: { + releaseInfo: { + vendor: { + minOSVersion: string; + }; + }; + }; + deb: { + depends: Array; + }; + files: Array>; + }; +}>; diff --git a/ts/updater/common.main.ts b/ts/updater/common.main.ts index 2c82e874b4..60e90de578 100644 --- a/ts/updater/common.main.ts +++ b/ts/updater/common.main.ts @@ -1,7 +1,6 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable no-console */ import { createWriteStream } from 'node:fs'; import fsExtra from 'fs-extra'; import { mkdir, readdir, stat, writeFile } from 'node:fs/promises'; @@ -10,8 +9,6 @@ import { release as osRelease, tmpdir } from 'node:os'; import { extname, join, normalize } from 'node:path'; import config from 'config'; -import type { ParserConfiguration } from 'dashdash'; -import { createParser } from 'dashdash'; import { FAILSAFE_SCHEMA, load as loadYaml } from 'js-yaml'; import { gt, gte, lt } from 'semver'; import got from 'got'; @@ -39,7 +36,7 @@ import { } from '../util/version.std.js'; import { isPathInside } from '../util/isPathInside.node.js'; -import { version as packageVersion } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.main.js'; import { getSignatureFileName, @@ -51,8 +48,8 @@ import { getBlockMapFileName, isValidPreparedData as isValidDifferentialData, prepareDownload as prepareDifferentialDownload, -} from './differential.node.js'; -import { getGotOptions } from './got.node.js'; +} from './differential.main.js'; +import { getGotOptions } from './got.main.js'; import { checkIntegrity, isTimeToUpdate } from './util.node.js'; import { gracefulRename, @@ -60,7 +57,7 @@ import { } from '../util/gracefulFs.node.js'; import type { LoggerType } from '../types/Logging.std.js'; -import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential.node.js'; +import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential.main.js'; import type { MainSQL } from '../sql/main.main.js'; const { pathExists } = fsExtra; @@ -578,7 +575,7 @@ export abstract class Updater { async #checkForUpdates( checkType: CheckType ): Promise { - if (isNotUpdatable(packageVersion)) { + if (isNotUpdatable(packageJson.version)) { this.logger.info( 'checkForUpdates: not checking for updates, this is not an updatable build' ); @@ -605,7 +602,7 @@ export abstract class Updater { if (checkType === CheckType.Normal && !isVersionNewer(version)) { this.logger.info( - `checkForUpdates: ${version} is not newer than ${packageVersion}; ` + + `checkForUpdates: ${version} is not newer than ${packageJson.version}; ` + 'no new update available' ); @@ -1007,27 +1004,27 @@ export function getUpdatesFileName(): string { } function getChannel(): string { - if (isNotUpdatable(packageVersion)) { + if (isNotUpdatable(packageJson.version)) { // we don't want ad hoc versions to update - return packageVersion; + return packageJson.version; } - if (isStaging(packageVersion)) { + if (isStaging(packageJson.version)) { return 'staging'; } - if (isAlpha(packageVersion)) { + if (isAlpha(packageJson.version)) { return 'alpha'; } - if (isAxolotl(packageVersion)) { + if (isAxolotl(packageJson.version)) { return 'axolotl'; } - if (isBeta(packageVersion)) { + if (isBeta(packageJson.version)) { return 'beta'; } return 'latest'; } function isVersionNewer(newVersion: string): boolean { - return gt(newVersion, packageVersion); + return gt(newVersion, packageJson.version); } export function getVersion(info: JSONUpdateSchema): string | null { @@ -1179,16 +1176,3 @@ export async function deleteTempDir( await gracefulRmRecursive(logger, targetDir); } - -export function getCliOptions(options: ParserConfiguration['options']): T { - const parser = createParser({ options }); - const cliOptions = parser.parse(process.argv); - - if (cliOptions.help) { - const help = parser.help().trimRight(); - console.log(help); - process.exit(0); - } - - return cliOptions as unknown as T; -} diff --git a/ts/updater/differential.node.ts b/ts/updater/differential.main.ts similarity index 99% rename from ts/updater/differential.node.ts rename to ts/updater/differential.main.ts index 4724c7878b..ccec72186a 100644 --- a/ts/updater/differential.node.ts +++ b/ts/updater/differential.main.ts @@ -15,8 +15,8 @@ import Dicer from '@indutny/dicer'; import { strictAssert } from '../util/assert.std.js'; import { wrapEventEmitterOnce } from '../util/wrapEventEmitterOnce.node.js'; import type { LoggerType } from '../types/Logging.std.js'; -import { getGotOptions } from './got.node.js'; -import type { GotOptions } from './got.node.js'; +import { getGotOptions } from './got.main.js'; +import type { GotOptions } from './got.main.js'; import { checkIntegrity } from './util.node.js'; const { chunk: lodashChunk, noop } = lodash; diff --git a/ts/updater/generateKeyPair.main.ts b/ts/updater/generateKeyPair.main.ts deleted file mode 100644 index d253ef5bbc..0000000000 --- a/ts/updater/generateKeyPair.main.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* eslint-disable no-console */ -import * as Errors from '../types/errors.std.js'; -import { getCliOptions } from './common.main.js'; -import { keyPair } from './curve.node.js'; -import { writeHexToPath } from './signature.node.js'; - -const OPTIONS = [ - { - names: ['help', 'h'], - type: 'bool', - help: 'Print this help and exit.', - }, - { - names: ['key', 'k'], - type: 'string', - help: 'Path where public key will go', - default: 'public.key', - }, - { - names: ['private', 'p'], - type: 'string', - help: 'Path where private key will go', - default: 'private.key', - }, -]; - -type OptionsType = { - key: string; - private: string; -}; - -const cliOptions = getCliOptions(OPTIONS); -go(cliOptions).catch(error => { - console.error('Something went wrong!', Errors.toLogFormat(error)); -}); - -async function go(options: OptionsType) { - const { key: publicKeyPath, private: privateKeyPath } = options; - const { publicKey, privateKey } = keyPair(); - - await Promise.all([ - writeHexToPath(publicKeyPath, publicKey), - writeHexToPath(privateKeyPath, privateKey), - ]); -} diff --git a/ts/updater/generateSignature.main.ts b/ts/updater/generateSignature.main.ts deleted file mode 100644 index b59bd78e42..0000000000 --- a/ts/updater/generateSignature.main.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* eslint-disable no-console */ -import { join, resolve } from 'node:path'; -import { readdir as readdirCallback } from 'node:fs'; - -import pify from 'pify'; - -import * as Errors from '../types/errors.std.js'; -import { getCliOptions } from './common.main.js'; -import { writeSignature } from './signature.node.js'; -import { version as packageVersion } from '../util/packageJson.node.js'; - -const readdir = pify(readdirCallback); - -const OPTIONS = [ - { - names: ['help', 'h'], - type: 'bool', - help: 'Print this help and exit.', - }, - { - names: ['private', 'p'], - type: 'string', - help: 'Path to private key file (default: ./private.key)', - default: 'private.key', - }, - { - names: ['update', 'u'], - type: 'string', - help: 'Path to the update package (default: the .exe or .zip in ./release)', - }, - { - names: ['version', 'v'], - type: 'string', - help: `Version number of this package (default: ${packageVersion})`, - default: packageVersion, - }, -]; - -type OptionsType = { - private: string; - update: string; - version: string; -}; - -const cliOptions = getCliOptions(OPTIONS); -go(cliOptions).catch(error => { - console.error('Something went wrong!', Errors.toLogFormat(error)); -}); - -async function go(options: OptionsType) { - const { private: privateKeyPath, version } = options; - - let updatePaths: Array; - if (options.update) { - updatePaths = [options.update]; - } else { - updatePaths = await findUpdatePaths(); - } - - await Promise.all( - updatePaths.map(async updatePath => { - console.log('Signing with...'); - console.log(` version: ${version}`); - console.log(` update file: ${updatePath}`); - console.log(` private key file: ${privateKeyPath}`); - - await writeSignature(updatePath, version, privateKeyPath); - }) - ); -} - -const IS_EXE = /\.exe$/; -const IS_ZIP = /\.zip$/; -async function findUpdatePaths(): Promise> { - const releaseDir = resolve('release'); - const files: Array = await readdir(releaseDir); - - const results = new Array(); - for (const file of files) { - const fullPath = join(releaseDir, file); - - if (IS_EXE.test(file) || IS_ZIP.test(file)) { - results.push(fullPath); - } - } - - if (results.length === 0) { - throw new Error("No suitable file found in 'release' folder!"); - } - - return results; -} diff --git a/ts/updater/got.node.ts b/ts/updater/got.main.ts similarity index 94% rename from ts/updater/got.node.ts rename to ts/updater/got.main.ts index 68263fbd1e..0fcdc45518 100644 --- a/ts/updater/got.node.ts +++ b/ts/updater/got.main.ts @@ -5,7 +5,7 @@ import type { StrictOptions as GotOptions } from 'got'; import config from 'config'; import { Agent as HTTPAgent } from 'node:http'; -import { version } from '../util/packageJson.node.js'; +import { packageJson } from '../util/packageJson.main.js'; import { getUserAgent } from '../util/getUserAgent.node.js'; import * as durations from '../util/durations/index.std.js'; import { createHTTPSAgent } from '../util/createHTTPSAgent.node.js'; @@ -46,7 +46,7 @@ export async function getGotOptions(): Promise { }, headers: { 'Cache-Control': 'no-cache', - 'User-Agent': getUserAgent(version), + 'User-Agent': getUserAgent(packageJson.version), }, timeout: { connect: GOT_CONNECT_TIMEOUT, diff --git a/ts/updater/linux.main.ts b/ts/updater/linux.main.ts index 26fa6999db..aa656a0bd4 100644 --- a/ts/updater/linux.main.ts +++ b/ts/updater/linux.main.ts @@ -14,6 +14,7 @@ import { DialogType } from '../types/Dialogs.std.js'; import * as Errors from '../types/errors.std.js'; import type { UpdaterOptionsType } from './common.main.js'; import { appRelaunch } from '../util/relaunch.main.js'; +import { getAppRootDir } from '../util/appRootDir.main.js'; const MIN_UBUNTU_VERSION = '22.04'; @@ -69,7 +70,7 @@ export function initLinux({ logger, getMainWindow }: UpdaterOptionsType): void { // See our patch for app-builder-lib. // // /opt/Signal/resources/app.asar - const asarPath = join(__dirname, '..', '..'); + const asarPath = getAppRootDir(); if (!asarPath.endsWith('.asar')) { throw new Error('updater/linux: not running from ASAR'); } diff --git a/ts/updater/windows.main.ts b/ts/updater/windows.main.ts index 3498970dca..d5d6e38da4 100644 --- a/ts/updater/windows.main.ts +++ b/ts/updater/windows.main.ts @@ -9,6 +9,7 @@ import { readdir as readdirCallback, unlink as unlinkCallback } from 'node:fs'; import { app } from 'electron'; import pify from 'pify'; +import { getAppRootDir } from '../util/appRootDir.main.js'; import { Updater } from './common.main.js'; const readdir = pify(readdirCallback); @@ -112,7 +113,7 @@ export class WindowsUpdater extends Updater { // Helpers function getElevatePath() { - const installPath = app.getAppPath(); + const installPath = getAppRootDir(); return join(installPath, 'resources', 'elevate.exe'); } diff --git a/ts/util/appRootDir.main.ts b/ts/util/appRootDir.main.ts new file mode 100644 index 0000000000..f669880cc3 --- /dev/null +++ b/ts/util/appRootDir.main.ts @@ -0,0 +1,14 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { app } from 'electron'; +import { join } from 'node:path'; + +export function getAppRootDir(): string { + // We have this logic because test-node runs under `electron-mocha` that has + // `app.getAppPath()` pointing within `electron-mocha`'s folder. + if (app.isPackaged || process.env.IS_BUNDLED) { + return app.getAppPath(); + } + return join(__dirname, '..', '..'); +} diff --git a/ts/util/getAppErrorIcon.node.ts b/ts/util/getAppErrorIcon.node.ts index 6d35cd86fc..355370828b 100644 --- a/ts/util/getAppErrorIcon.node.ts +++ b/ts/util/getAppErrorIcon.node.ts @@ -5,13 +5,7 @@ import { nativeImage } from 'electron'; import type { NativeImage } from 'electron'; import { join } from 'node:path'; -export function getAppErrorIcon(): NativeImage { - const iconPath = join( - __dirname, - '..', - '..', - 'images', - 'app-icon-with-error.png' - ); +export function getAppErrorIcon(rootDir: string): NativeImage { + const iconPath = join(rootDir, 'images', 'app-icon-with-error.png'); return nativeImage.createFromPath(iconPath); } diff --git a/ts/util/packageJson.main.ts b/ts/util/packageJson.main.ts new file mode 100644 index 0000000000..722d400623 --- /dev/null +++ b/ts/util/packageJson.main.ts @@ -0,0 +1,13 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { PackageJsonType } from '../types/packageJson.d.ts'; +import { getAppRootDir } from './appRootDir.main.js'; + +const PACKAGE_JSON_PATH = join(getAppRootDir(), 'package.json'); + +export const packageJson: PackageJsonType = JSON.parse( + readFileSync(PACKAGE_JSON_PATH, 'utf8') +); diff --git a/ts/util/packageJson.node.ts b/ts/util/packageJson.node.ts index 0a47dd700d..17c989d3ae 100644 --- a/ts/util/packageJson.node.ts +++ b/ts/util/packageJson.node.ts @@ -3,32 +3,10 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +import type { PackageJsonType } from '../types/packageJson.d.ts'; const PACKAGE_JSON_PATH = join(__dirname, '..', '..', 'package.json'); -const json: { - name: string; - version: string; - productName: string; - build: { - appId: string; - mac: { - releaseInfo: { - vendor: { - minOSVersion: string; - }; - }; - }; - deb: { - depends: Array; - }; - files: Array>; - }; -} = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8')); - -export default json; - -export const { name } = json; -export const { version } = json; -export const { productName } = json; -export const { build } = json; +export const packageJson: PackageJsonType = JSON.parse( + readFileSync(PACKAGE_JSON_PATH, 'utf8') +); diff --git a/ts/util/privacy.node.ts b/ts/util/privacy.node.ts index 308446028c..571c8a59c9 100644 --- a/ts/util/privacy.node.ts +++ b/ts/util/privacy.node.ts @@ -11,7 +11,6 @@ import lodash from 'lodash'; import type { ExtendedStorageID } from '../types/StorageService.d.ts'; import type { ConversationModel } from '../models/conversations.preload.js'; -export const APP_ROOT_PATH = path.join(__dirname, '..', '..'); const { escapeRegExp, isString, isRegExp } = lodash; const { compose } = lodashFp; @@ -232,8 +231,6 @@ export const addSensitivePath = (filePath: string): void => { redactSensitivePaths = createRedactSensitivePaths(sensitivePaths); }; -addSensitivePath(APP_ROOT_PATH); - export const redactAll: RedactFunction = text => { let result = text; diff --git a/ts/util/smartling.node.ts b/ts/util/smartling.node.ts index 4d4b2ffcc5..df08a5e3a3 100644 --- a/ts/util/smartling.node.ts +++ b/ts/util/smartling.node.ts @@ -1,7 +1,7 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { version } from './packageJson.node.js'; +import { packageJson } from './packageJson.node.js'; import { getUserAgent } from './getUserAgent.node.js'; type AuthenticateOptionsType = Readonly<{ @@ -36,6 +36,6 @@ export async function authenticate({ return new Headers({ authorization: `${auth.tokenType} ${auth.accessToken}`, - 'user-agent': getUserAgent(version), + 'user-agent': getUserAgent(packageJson.version), }); } diff --git a/ts/workers/heicConverterMain.main.ts b/ts/workers/heicConverterMain.main.ts index 2b6a26c32f..be177ac72e 100644 --- a/ts/workers/heicConverterMain.main.ts +++ b/ts/workers/heicConverterMain.main.ts @@ -3,7 +3,7 @@ import { join } from 'node:path'; import { Worker } from 'node:worker_threads'; -import { app } from 'electron'; +import { getAppRootDir } from '../util/appRootDir.main.js'; export type WrappedWorkerRequest = { readonly uuid: string; @@ -21,7 +21,7 @@ export function getHeicConverter(): ( data: Uint8Array ) => Promise { const scriptDir = join( - app.getAppPath(), + getAppRootDir(), 'ts', 'workers', 'heicConverterWorker.node.js'