/** Build identities and release requirements shared by packaging and tests. */ export const APP_ID = 'com.thetempusproject.capsule' /** Validate a public feed without embedding credentials in installed copies. */ export function validateFeedUrl(value) { let url try { url = new URL(value) } catch { throw new Error('CAPSULE_UPDATE_URL must be an absolute HTTPS directory URL.') } if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !url.pathname.endsWith('/') || /^(localhost|127\.|\[?::1\]?)/i.test(url.hostname) || /(^|\.)example\.(com|org|net)$|\.invalid$/i.test(url.hostname)) { throw new Error('Use a permanent public HTTPS feed ending in /, without credentials, query, or fragment.') } return url.href } /** Return a Windows build config; public releases fail closed without signing. */ export function createBuildConfig({ release = false, testUpdates = false, bootstrap = false, env = process.env } = {}) { if (release && testUpdates) throw new Error('Test updates cannot be combined with a public release.') if (bootstrap && !testUpdates) throw new Error('Bootstrap requires test updates.') const url = testUpdates ? validateFeedUrl(env.CAPSULE_TEST_UPDATE_URL) : release ? validateFeedUrl(env.CAPSULE_UPDATE_URL) : null if (testUpdates && !new URL(url).pathname.endsWith('/capsule/testfeed/')) { throw new Error('Test updates require a separate /capsule/testfeed/ URL.') } const publisher = env.CAPSULE_PUBLISHER_NAME?.trim() if (release && !publisher) { throw new Error('CAPSULE_PUBLISHER_NAME must match the signing certificate subject CN.') } if (release && !env.CSC_LINK && !env.CSC_NAME) { throw new Error('Set CSC_LINK (certificate) or CSC_NAME (Windows certificate store identity).') } return { appId: release ? APP_ID : `${APP_ID}.local`, productName: release ? 'Capsule' : 'Capsule Local', executableName: release ? 'Capsule' : 'Capsule Local', directories: { output: release ? 'dist/release' : testUpdates ? bootstrap ? 'dist/test-bootstrap' : 'dist/test' : 'dist/local' }, files: ['out/**/*', 'package.json'], asar: true, npmRebuild: false, forceCodeSigning: release, extraMetadata: { // Keep the release userData path compatible with the original dev app. name: release ? 'capsule' : 'capsule-local', ...(bootstrap ? { version: '0.1.0' } : {}), capsuleUpdates: { enabled: release || testUpdates, testing: testUpdates } }, artifactName: release || testUpdates ? 'Capsule-${version}-${arch}-Setup.${ext}' : 'Capsule-Local-${version}-${arch}-Setup.${ext}', win: { icon: 'build/icon.ico', target: [{ target: 'nsis', arch: ['x64'] }], verifyUpdateCodeSignature: !testUpdates, signExecutable: release, ...(release ? { signtoolOptions: { publisherName: publisher, signingHashAlgorithms: ['sha256'], ...(env.CSC_NAME ? { certificateSubjectName: env.CSC_NAME } : {}) } } : {}) }, nsis: { oneClick: true, perMachine: false, allowElevation: false, deleteAppDataOnUninstall: false, runAfterFinish: false, shortcutName: release ? 'Capsule' : 'Capsule Local' }, publish: release || testUpdates ? [{ provider: 'generic', url, channel: 'latest', useMultipleRangeRequest: false }] : null } }