update electron to v43

This commit is contained in:
olcxja 2026-07-09 22:38:33 +02:00
commit fb6c8b6ee9
5385 changed files with 513060 additions and 123058 deletions

23
electron/node_modules/@electron/windows-sign/LICENSE generated vendored Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) Felix Rieseberg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

262
electron/node_modules/@electron/windows-sign/README.md generated vendored Normal file
View file

@ -0,0 +1,262 @@
# @electron/windows-sign [![npm][npm_img]][npm_url]
Codesign your app for Windows. Made for [Electron][electron] but really supports any folder with binary files. `electron-windows-sign` scans a folder for [signable files](#file-types) and codesigns them with both SHA-1 and SHA-256. It can be highly customized and used either programmatically or on the command line.
This tool is particularly useful if you want to code sign Electron or other Windows binaries with an EV certificate or an HSM setup (like DigiCert KeyLocker, AWS CloudHSM, Azure Key Vault HSM, Google Cloud Key Management Service HSM, and other similar services). For more details on how you'd exactly do that, please see [Use with Cloud HSM Providers](#use-with-cloud-hsm-providers) below.
# Requirements
By default, this module spawns `signtool.exe` and needs to run on Windows. If you're building an Electron app and care enough to codesign them, I would heavily recommend that you build and test your apps on the platforms you're building for.
# Usage
Most developers of Electron apps will likely not use this module directly - and instead use it indirectly
instead. If you are one of those developers who is using a module like `@electron/forge` or `@electron/packager`, you can configure this module with global environment variables. If that describes
you, you can skip ahead to your use case:
- [With a certificate file and password](#with-a-certificate-file-and-password)
- [With a custom binary or custom parameters](#with-a-custom-signtoolexe-or-custom-parameters)
- [With a completely custom hook](#with-a-custom-hook-function)
## Direct Usage
`@electron/windows-codesign` is built to both esm and cjs and can be used both as a module as well as directly from the command line.
```ts
import { sign } from "@electron/windows-sign"
// or const { sign } = require("@electron/windows-sign")
await sign(signOptions)
```
```ps1
electron-windows-sign $PATH_TO_APP_DIRECTORY [options ...]
```
## With a certificate file and password
This is the "traditional" way to codesign Electron apps on Windows. You pass in a certificate file
(like a .pfx) and a password, which will then be passed to a built-in version of `signtool.exe` taken
directly from the Windows SDK.
```ts
await sign({
appDirectory: "C:\\Path\\To\\App",
// or process.env.WINDOWS_CERTIFICATE_FILE
certificateFile: "C:\\Cert.pfx",
// or process.env.WINDOWS_CERTIFICATE_PASSWORD
certificatePassword: "hunter99"
})
```
```ps1
electron-windows-sign $PATH_TO_APP_DIRECTORY --certificate-file=$PATH_TO_CERT --certificate-password=$CERT-PASSWORD
```
### Full configuration
```ts
// Path to a timestamp server. Defaults to http://timestamp.digicert.com
// Can also be passed as process.env.WINDOWS_TIMESTAMP_SERVER
timestampServer = "http://timestamp.digicert.com"
// Description of the signed content. Will be passed to signtool.exe as /d
// Can also be passed as process.env.WINDOWS_SIGN_DESCRIPTION
description = "My content"
// URL of the signed content. Will be passed to signtool.exe as /du
// Can also be passed as process.env.WINDOWS_SIGN_WEBSITE
website = "https://mywebsite.com"
// If enabled, attempt to sign .js JavaScript files. Disabled by default
signJavaScript = true
// If unspecified, both sha1 and sha256 signatures will be appended. Will be passed to signtool.exe as the /fd option.
hashes = ["sha256"]
```
## With a custom signtool.exe or custom parameters
Sometimes, you need to specify specific signing parameters or use a different version
of `signtool.exe`. In this mode, `@electron/windows-sign` will call the provided binary
with the provided parameters for each file to sign.
If you only provide `signToolPath`, the default parameters will be used.
If you only provide `signWithParams`, the default `signtool.exe` will be used.
All the [additional configuration](#additional-configuration) mentioned above is also
available here, but only used if you do not provide your own parameters.
```ts
await sign({
appDirectory: "C:\\Path\\To\\App",
// or process.env.WINDOWS_CERTIFICATE_FILE
certificateFile: "C:\\Cert.pfx",
// or process.env.WINDOWS_CERTIFICATE_PASSWORD
certificatePassword: "hunter99",
// or process.env.WINDOWS_SIGNTOOL_PATH
signToolPath: "C:\\Path\\To\\my-custom-tool.exe",
// or process.env.WINDOWS_SIGN_WITH_PARAMS
signWithParams: "--my=custom --parameters"
})
```
```ps1
electron-windows-sign $PATH_TO_APP_DIRECTORY --sign-tool-path=$PATH_TO_TOOL --sign-with-params="--my=custom --parameters"
```
## With a custom hook function
Sometimes, you just want all modules depending on `@electron/windows-sign` to call
your completely custom logic. You can either specify a `hookFunction` (if you're calling
this module yourself) or a `hookModulePath`, which this module will attempt to require.
Using the `hookModulePath` has the benefit that you can override how any other users
of this module (like `@electron/packager`) codesign your app.
```ts
await sign({
// A function with the following signature:
// (fileToSign: string) => void | Promise<void>
//
// This function will be called sequentially for each file that
// @electron/windows-sign wants to sign.
hookFunction: myHookFunction
// Path to a hook module.
hookModulePath: "C:\\Path\\To\\my-hook-module.js",
})
```
Your hook module should either directly export a function or
export a `default` function.
```js
// Good:
module.exports = function (filePath) {
console.log(`Path to file to sign: ${filePath}`)
}
// Good:
module.exports = async function (filePath) {
console.log(`Path to file to sign: ${filePath}`)
}
// Good:
export default async function (filePath) {
console.log(`Path to file to sign: ${filePath}`)
}
// Bad:
module.exports = {
myCustomHookName: function (filePath) {
console.log(`Path to file to sign: ${filePath}`)
}
}
// Bad:
export async function myCustomHookName(filePath) {
console.log(`Path to file to sign: ${filePath}`)
}
```
```
SYNOPSIS
electron-windows-sign app [options ...]
DESCRIPTION
app
Path to the application to sign. It must be a directory.
certificate-file
Path to the certificate file (.pfx) to use for signing. Uses
environment variable WINDOWS_CERTIFICATE_FILE if not provided.
certificate-password
Password to use for the certificate. Uses environment variable
WINDOWS_CERTIFICATE_PASSWORD if not provided.
sign-tool-path
Path to the signtool.exe binary. If not specified, the tool will attempt
use a built-in version.
timestamp-server
URL of the timestamp server to use. If not specified, the tool will
attempt to use a built-in server (http://timestamp.digicert.com)
description
Description to use for the signed files. Passed as /d to signtool.exe.
website
URL of the website to use for the signed files. Passed as /du to
signtool.exe.
sign-with-params
Additional parameters to pass to signtool.exe. This can be used to
specify additional certificates to use for cross-signing.
automatically-select-certificate
Automatically select the best certificate to use for signing. On by default.
help
Print this usage information.
debug
Print additional debug information.
```
# File Types
This tool will aggressively attempt to sign all files that _can_
be signed, excluding scripts.
- [Portable executable files][pe] (.exe, .dll, .sys, .efi, .scr, .node)
- Microsoft installers (.msi)
- APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
- Catalog files (.cat)
- Cabinet files (.cab)
- Silverlight applications (.xap)
- Scripts (.vbs, .wsf, .ps1)
If you do want to sign JavaScript, please enable it with the `signJavaScript`
parameter. As far as we are aware, there are no benefits to signing
JavaScript files, so we do not by default.
# Use with Cloud HSM Providers
Since 2023, Microsoft requires that Windows software be signed with an extended validation (EV) certificate in order to avoid a Windows Defender popup. This presents a challenge for developers who are used to building and signing in the cloud or some continuous integration setup like GitHub Actions, CircleCI, Travis CI, or AppVeyor because EV certificates require a Hardware Security Module (HSM). Or, in simpler words: If you want to code sign your Windows software so that your customers don't see a scary Windows Defender dialog, you need to code sign on a computer with a USB device plugged in.
The industry has an answer to this problem: Services like DigiCert KeyLocker, AWS CloudHSM, Azure Key Vault HSM, or Google Cloud Key Management Service HSM allow code signing binaries with an Extended Validation (EV) certificate from the cloud. `@electron/windows-sign` is compatible with all of these services.
## Custom signtool parameters
Most services allow signing your code with Microsoft's `signtool.exe`. Let's take DigiCert KeyLocker as an example. [DigiCert's documentation](https://docs.digicert.com/en/digicert-keylocker/signing-tools/sign-authenticode-files-with-signtool-on-windows.html) explains the steps necessary to setup your signing machine. Once done, you can sign with the following call:
```
signtool.exe sign /csp "DigiCert Signing Manager KSP" /kc <keypair_alias> /f <certificate_file> /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 <file_to_be_signed>
```
To sign with `@electron/windows-sign` using those instructions, you would take the parameters and add them to `signWithParams`:
```js
await sign({
signWithParams: "/csp \"DigiCert Signing Manager KSP\" /kc <keypair_alias> /f <certificate_file> /tr http://timestamp.digicert.com /td SHA256 /fd SHA256"
})
```
Both Google's and Amazon's solutions similarly allow you to sign with Microsoft's SignTool. Documentation for [Google can be found here](https://cloud.google.com/kms/docs/reference/cng-signtool), [Amazon's lives here](https://docs.aws.amazon.com/cloudhsm/latest/userguide/signtool.html).
## Custom signtool.exe
Some providers provide drop-in replacements for `signtool.exe`. If that's the case, simply pass the path to that replacement:
```ts
await sign({
// or process.env.WINDOWS_SIGNTOOL_PATH
signToolPath: "C:\\Path\\To\\my-custom-tool.exe",
})
```
## Fully custom process
If your HSM provider has a more complex setup, you might have to call a custom file with custom parameters. If that's the case, use a hook function or hook module, which allows you to completely customize what to do with each file that `@electron/windows-sign` wants to sign. More documentation about this option can be found in the ["Signing with a custom hook function" section above](#with-a-custom-hook-function).
# License
BSD 2-Clause "Simplified". Please see LICENSE for details.
[electron]: https://github.com/electron/electron
[npm_img]: https://img.shields.io/npm/v/@electron/windows-sign.svg
[npm_url]: https://npmjs.org/package/@electron/windows-sign
[pe]: https://en.wikipedia.org/wiki/Portable_Executable

View file

@ -0,0 +1,43 @@
NAME
electron-windows-sign -- code signing for Electron apps on Windows.
SYNOPSIS
electron-windows-sign app [options ...]
DESCRIPTION
app
Path to the application to sign. It must be a directory.
certificate-file
Path to the certificate file (.pfx) to use for signing.
certificate-password
Password to use for the certificate.
sign-tool-path
Path to the signtool.exe binary. If not specified, the tool will attempt
use a built-in version.
timestamp-server
URL of the timestamp server to use. If not specified, the tool will
attempt to use a built-in server (http://timestamp.digicert.com)
description
Description to use for the signed files. Passed as /d to signtool.exe.
website
URL of the website to use for the signed files. Passed as /du to
signtool.exe.
sign-with-params
Additional parameters to pass to signtool.exe. This can be used to
specify additional certificates to use for cross-signing.
automatically-select-certificate
Automatically select the best certificate to use for signing. On by default.
help
Print this usage information.
debug
Print additional debug information.

View file

@ -0,0 +1,47 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const args = require('minimist')(process.argv.slice(2), {
string: [
'certificate-file',
'certificate-password',
'sign-tool-path',
'timestamp-server',
'description',
'website',
'sign-with-params'
],
boolean: [
'help',
'debug'
],
default: {
'automatically-select-certificate': true
}
});
const usage = fs.readFileSync(path.join(__dirname, 'electron-windows-sign-usage.txt')).toString();
const sign = require('../').sign;
args.app = args._.shift();
if (!args.app || args.help) {
console.log(usage);
process.exit(0);
}
// Remove excess arguments
delete args._;
delete args.help;
sign(args, function done(err) {
if (err) {
console.error('Sign failed:');
if (err.message) console.error(err.message);
else if (err.stack) console.error(err.stack);
else console.log(err);
process.exit(1);
}
console.log('Application signed:', args.app);
process.exit(0);
});

View file

@ -0,0 +1,16 @@
import { SignOptions } from './types';
/**
* Recursively goes through an entire directory and returns an array
* of full paths for files ot sign.
*
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
* - Microsoft installers (.msi)
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
* - Catalog files (.cat)
* - Cabinet files (.cab)
* - Silverlight applications (.xap)
* - Scripts (.vbs, .wsf, .ps1)
* If configured:
* - JavaScript files (.js)
*/
export declare function getFilesToSign(options: SignOptions, dir?: string): Array<string>;

View file

@ -0,0 +1,66 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFilesToSign = void 0;
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const IS_PE_REGEX = /\.(exe|dll|sys|efi|scr|node)$/i;
const IS_MSI_REGEX = /\.msi$/i;
const IS_PACKAGE_REGEX = /\.(appx|appxbundle|msix|msixbundle)$/i;
const IS_CATCAB_REGEX = /\.(cat|cab)$/i;
const IS_SILVERLIGHT_REGEX = /\.xap$/i;
const IS_SCRIPT_REGEX = /\.(vbs|wsf|ps1)$/i;
const IS_JS_REGEX = /\.js$/i;
/**
* Recursively goes through an entire directory and returns an array
* of full paths for files ot sign.
*
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
* - Microsoft installers (.msi)
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
* - Catalog files (.cat)
* - Cabinet files (.cab)
* - Silverlight applications (.xap)
* - Scripts (.vbs, .wsf, .ps1)
* If configured:
* - JavaScript files (.js)
*/
function getFilesToSign(options, dir) {
if (isSignOptionsForFiles(options)) {
return options.files;
}
dir = dir || options.appDirectory;
// Array of file paths to sign
const result = [];
// Iterate over the app directory, looking for files to sign
const files = fs_extra_1.default.readdirSync(dir);
const regexes = [
IS_PE_REGEX,
IS_MSI_REGEX,
IS_PACKAGE_REGEX,
IS_CATCAB_REGEX,
IS_SILVERLIGHT_REGEX,
IS_SCRIPT_REGEX
];
if (options.signJavaScript) {
regexes.push(IS_JS_REGEX);
}
for (const file of files) {
const fullPath = path_1.default.resolve(dir, file);
if (fs_extra_1.default.statSync(fullPath).isDirectory()) {
// If it's a directory, recurse
result.push(...getFilesToSign(options, fullPath));
}
else if (regexes.some((regex) => regex.test(file))) {
// If it's a match, add it to the list
result.push(fullPath);
}
}
return result;
}
exports.getFilesToSign = getFilesToSign;
function isSignOptionsForFiles(input) {
return !!input.files;
}

View file

@ -0,0 +1,4 @@
import { sign } from './sign';
import { createSeaSignTool, SeaOptions, InternalSeaOptions } from './sea';
import { HookFunction, OptionalHookOptions, OptionalSignToolOptions, SignOptions, SignToolOptions, SignOptionsForDirectory, SignOptionsForFiles } from './types';
export { sign, SignOptions, SignToolOptions, HookFunction, OptionalSignToolOptions, OptionalHookOptions, createSeaSignTool, SeaOptions, InternalSeaOptions, SignOptionsForDirectory, SignOptionsForFiles };

View file

@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSeaSignTool = exports.sign = void 0;
const sign_1 = require("./sign");
Object.defineProperty(exports, "sign", { enumerable: true, get: function () { return sign_1.sign; } });
const sea_1 = require("./sea");
Object.defineProperty(exports, "createSeaSignTool", { enumerable: true, get: function () { return sea_1.createSeaSignTool; } });

View file

@ -0,0 +1,61 @@
import { SignToolOptions } from './types';
/**
* Options for signing with a Node.js single executable application.
*
* @category Single executable applications
*/
export interface SeaOptions {
/**
* Full path to the Node.js single executable application. Needs to end with `.exe`.
*/
path: string;
/**
* A binary to use. Will use the current executable (process.execPath) by default.
*
* @defaultValue The Node.js {@link https://nodejs.org/api/process.html#processexecpath | `process.execPath`}
*/
bin?: string;
/**
* Options to pass to SignTool.
*/
windowsSign: SignToolOptions;
}
/**
* This interface represents {@link SeaOptions} with all optional properties
* inferred by `@electron/windows-sign` if not passed in by the user.
*
* @category Single executable applications
*/
export interface InternalSeaOptions extends Required<SeaOptions> {
/**
* Directory of the Node.js single executable application.
*/
dir: string;
/**
* File name of the Node.js single executable application.
*/
filename: string;
}
/**
* cross-dir uses new Error() stacks
* to figure out our directory in a way
* that's somewhat cross-compatible.
*
* We can't just use __dirname because it's
* undefined in ESM - and we can't use import.meta.url
* because TypeScript won't allow usage unless you're
* _only_ compiling for ESM.
*/
export declare const DIRNAME: string;
/**
* Uses Node's "Single Executable App" functionality
* to create a Node-driven signtool.exe that calls this
* module.
*
* This is useful with other tooling that _always_ calls
* a signtool.exe to sign. Some of those tools cannot be
* easily configured, but we _can_ override their signtool.exe.
*
* @category Single executable applications
*/
export declare function createSeaSignTool(options?: Partial<SeaOptions>): Promise<InternalSeaOptions>;

View file

@ -0,0 +1,220 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSeaSignTool = exports.DIRNAME = void 0;
const cross_dirname_1 = require("cross-dirname");
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const postject_1 = __importDefault(require("postject"));
const spawn_1 = require("./spawn");
const log_1 = require("./utils/log");
/**
* cross-dir uses new Error() stacks
* to figure out our directory in a way
* that's somewhat cross-compatible.
*
* We can't just use __dirname because it's
* undefined in ESM - and we can't use import.meta.url
* because TypeScript won't allow usage unless you're
* _only_ compiling for ESM.
*/
exports.DIRNAME = (0, cross_dirname_1.getDirname)();
const FILENAMES = {
SEA_CONFIG: 'sea-config.json',
SEA_MAIN: 'sea.js',
SEA_BLOB: 'sea.blob',
SEA_RECEIVER: 'receiver.mjs'
};
const SEA_MAIN_SCRIPT = `
const bin = "%PATH_TO_BIN%";
const script = "%PATH_TO_SCRIPT%";
const options = %WINDOWS_SIGN_OPTIONS%
const { spawnSync } = require('child_process');
function main() {
console.log("@electron/windows-sign sea");
console.log({ bin, script });
try {
const spawn = spawnSync(
bin,
[ script, JSON.stringify(options), JSON.stringify(process.argv.slice(1)) ],
{ stdio: ['inherit', 'inherit', 'pipe'] }
);
if (spawn.status !== 0) {
throw new Error(\`Spawn failed with code: \${spawn.status}. Stderr: \${spawn.stderr}\`);
}
} catch (error) {
// Do not rethrow the error or write it to stdout/stderr. Then the process won't terminate.
// See: https://github.com/electron/windows-sign/pull/48
process.exit(1);
}
}
main();
`;
const SEA_RECEIVER_SCRIPT = `
import { sign } from '@electron/windows-sign';
import fs from 'fs-extra';
import path from 'path';
const logPath = path.join('electron-windows-sign.log');
const options = JSON.parse(process.argv[2]);
const signArgv = JSON.parse(process.argv[3]);
const files = signArgv.slice(-1);
try {
fs.appendFileSync(logPath, \`\\nCalled with: \${JSON.stringify(process.argv, null, 2)}\`);
sign({ ...options, files })
.then((result) => {
fs.appendFileSync(logPath, \`\\nSuccessfully signed with result: \${result}\`);
})
.catch((error) => {
fs.appendFileSync(logPath, \`\\nError from sign: \${error}\`);
throw new Error(error);
});
} catch (error) {
fs.appendFileSync(logPath, \`\\Error invoking sign: \${error}\`);
throw new Error(error);
}
`;
/**
* Uses Node's "Single Executable App" functionality
* to create a Node-driven signtool.exe that calls this
* module.
*
* This is useful with other tooling that _always_ calls
* a signtool.exe to sign. Some of those tools cannot be
* easily configured, but we _can_ override their signtool.exe.
*
* @category Single executable applications
*/
async function createSeaSignTool(options = {}) {
checkCompatibility();
const requiredOptions = await getOptions(options);
await createFiles(requiredOptions);
await createBlob(requiredOptions);
await createBinary(requiredOptions);
await createSeaReceiver(requiredOptions);
await cleanup(requiredOptions);
return requiredOptions;
}
exports.createSeaSignTool = createSeaSignTool;
async function createSeaReceiver(options) {
const receiverPath = path_1.default.join(options.dir, FILENAMES.SEA_RECEIVER);
await fs_extra_1.default.ensureFile(receiverPath);
await fs_extra_1.default.writeFile(receiverPath, SEA_RECEIVER_SCRIPT);
}
async function createFiles(options) {
const { dir, bin } = options;
const receiverPath = path_1.default.join(options.dir, FILENAMES.SEA_RECEIVER);
// sea-config.json
await fs_extra_1.default.outputJSON(path_1.default.join(dir, FILENAMES.SEA_CONFIG), {
main: FILENAMES.SEA_MAIN,
output: FILENAMES.SEA_BLOB,
disableExperimentalSEAWarning: true
}, {
spaces: 2
});
// signtool.js
const binPath = bin || process.execPath;
const script = SEA_MAIN_SCRIPT
.replace('%PATH_TO_BIN%', escapeMaybe(binPath))
.replace('%PATH_TO_SCRIPT%', escapeMaybe(receiverPath))
.replace('%WINDOWS_SIGN_OPTIONS%', JSON.stringify(options.windowsSign));
await fs_extra_1.default.outputFile(path_1.default.join(dir, FILENAMES.SEA_MAIN), script);
}
async function createBlob(options) {
const args = ['--experimental-sea-config', 'sea-config.json'];
const bin = process.execPath;
const cwd = options.dir;
(0, log_1.log)(`Calling ${bin} with options:`, args);
const { stderr, stdout } = await (0, spawn_1.spawnPromise)(bin, args, {
cwd
});
(0, log_1.log)('stdout:', stdout);
(0, log_1.log)('stderr:', stderr);
}
async function createBinary(options) {
const { dir, filename } = options;
(0, log_1.log)(`Creating ${filename} in ${dir}`);
// Copy Node over
const seaPath = path_1.default.join(dir, filename);
await fs_extra_1.default.copyFile(process.execPath, seaPath);
// Remove the Node signature
const signtool = path_1.default.join(exports.DIRNAME, '../../vendor/signtool.exe');
await (0, spawn_1.spawnPromise)(signtool, [
'remove',
'/s',
seaPath
]);
// Inject the blob
const blob = await fs_extra_1.default.readFile(path_1.default.join(dir, FILENAMES.SEA_BLOB));
await postject_1.default.inject(seaPath, 'NODE_SEA_BLOB', blob, {
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
});
}
async function cleanup(options) {
const { dir } = options;
const toRemove = [
FILENAMES.SEA_BLOB,
FILENAMES.SEA_MAIN,
FILENAMES.SEA_CONFIG
];
for (const file of toRemove) {
try {
await fs_extra_1.default.remove(path_1.default.join(dir, file));
}
catch (error) {
console.warn(`Tried and failed to remove ${file}. Continuing.`, error);
}
}
}
async function getOptions(options) {
const cloned = { ...options };
if (!cloned.path) {
cloned.path = path_1.default.join(os_1.default.homedir(), '.electron', 'windows-sign', 'sea.exe');
await fs_extra_1.default.ensureFile(cloned.path);
}
if (!cloned.bin) {
cloned.bin = process.execPath;
}
if (!cloned.windowsSign) {
throw new Error('Did not find windowsSign options, which are required');
}
return {
path: cloned.path,
dir: path_1.default.dirname(cloned.path),
filename: path_1.default.basename(cloned.path),
bin: cloned.bin,
windowsSign: cloned.windowsSign
};
}
/**
* Ensures that the current Node.js version supports SEA app generation and errors if not.
*/
function checkCompatibility() {
const version = process.versions.node;
const split = version.split('.');
const major = parseInt(split[0], 10);
if (major >= 20) {
return true;
}
throw new Error(`Your Node.js version (${process.version}) does not support Single Executable Applications. Please upgrade your version of Node.js.`);
}
/** Make sure that the input string has escaped backwards slashes
* - but never double-escaped backwards slashes.
*/
function escapeMaybe(input) {
const result = input.split(path_1.default.sep).join('\\\\');
if (result.includes('\\\\\\\\')) {
throw new Error(`Your passed input ${input} contains escaped slashes. Please do not escape them`);
}
return result;
}

View file

@ -0,0 +1,8 @@
import { InternalSignOptions } from './types';
/**
* Sign with a hook function, basically letting everyone
* write completely custom sign logic
*
* @param {InternalSignOptions} options
*/
export declare function signWithHook(options: InternalSignOptions): Promise<void>;

View file

@ -0,0 +1,45 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.signWithHook = void 0;
const path_1 = __importDefault(require("path"));
const log_1 = require("./utils/log");
let hookFunction;
function getHookFunction(options) {
if (options.hookFunction) {
return options.hookFunction;
}
if (options.hookModulePath) {
const module = require(path_1.default.resolve(options.hookModulePath));
if (module.default) {
return module.default;
}
if (typeof module === 'function') {
return module;
}
}
if (!hookFunction) {
throw new Error('No hook function found. Signing will not be possible. Please see the documentation for how to pass a hook function to @electron/windows-sign');
}
return hookFunction;
}
/**
* Sign with a hook function, basically letting everyone
* write completely custom sign logic
*
* @param {InternalSignOptions} options
*/
async function signWithHook(options) {
hookFunction = getHookFunction(options);
for (const file of options.files) {
try {
await hookFunction(file);
}
catch (error) {
(0, log_1.log)(`Error signing ${file}`, error);
}
}
}
exports.signWithHook = signWithHook;

View file

@ -0,0 +1,2 @@
import { InternalSignOptions } from './types';
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;

View file

@ -0,0 +1,118 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.signWithSignTool = void 0;
const path_1 = __importDefault(require("path"));
const log_1 = require("./utils/log");
const spawn_1 = require("./spawn");
const cross_dirname_1 = require("cross-dirname");
const DIRNAME = (0, cross_dirname_1.getDirname)();
function getSigntoolArgs(options) {
// See the following url for docs
// https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
const { certificateFile, certificatePassword, hash, timestampServer } = options;
const args = ['sign'];
// Automatically select cert
if (options.automaticallySelectCertificate) {
args.push('/a');
}
// Dual-sign
if (options.appendSignature) {
args.push('/as');
}
// Timestamp
if (hash === "sha256" /* HASHES.sha256 */) {
args.push('/tr', timestampServer);
args.push('/td', hash);
}
else {
args.push('/t', timestampServer);
}
// Certificate file
if (certificateFile) {
args.push('/f', path_1.default.resolve(certificateFile));
}
// Certificate password
if (certificatePassword) {
args.push('/p', certificatePassword);
}
// Hash
args.push('/fd', hash);
// Description
if (options.description) {
args.push('/d', options.description);
}
// Website
if (options.website) {
args.push('/du', options.website);
}
// Debug
if (options.debug) {
args.push('/debug');
}
if (options.signWithParams) {
const extraArgs = [];
if (Array.isArray(options.signWithParams)) {
extraArgs.push(...options.signWithParams);
}
else {
// Split up at spaces and doublequotes
extraArgs.push(...options.signWithParams.match(/(?:[^\s"]+|"[^"]*")+/g));
}
(0, log_1.log)('Parsed signWithParams as:', extraArgs);
args.push(...extraArgs);
}
return args;
}
async function execute(options) {
const { signToolPath, files } = options;
const args = getSigntoolArgs(options);
(0, log_1.log)('Executing signtool with args', { args, files });
const { code, stderr, stdout } = await (0, spawn_1.spawnPromise)(signToolPath, [...args, ...files], {
env: process.env,
cwd: process.cwd()
});
if (code !== 0) {
throw new Error(`Signtool exited with code ${code}. Stderr: ${stderr}. Stdout: ${stdout}`);
}
}
async function signWithSignTool(options) {
const certificatePassword = options.certificatePassword || process.env.WINDOWS_CERTIFICATE_PASSWORD;
const certificateFile = options.certificateFile || process.env.WINDOWS_CERTIFICATE_FILE;
const signWithParams = options.signWithParams || process.env.WINDOWS_SIGN_WITH_PARAMS;
const timestampServer = options.timestampServer || process.env.WINDOWS_TIMESTAMP_SERVER || 'http://timestamp.digicert.com';
const signToolPath = options.signToolPath || process.env.WINDOWS_SIGNTOOL_PATH || path_1.default.join(DIRNAME, '../../vendor/signtool.exe');
const description = options.description || process.env.WINDOWS_SIGN_DESCRIPTION;
const website = options.website || process.env.WINDOWS_SIGN_WEBSITE;
if (!certificateFile && !(signWithParams || signToolPath)) {
throw new Error('You must provide a certificateFile and a signToolPath or signing parameters');
}
if (!signToolPath && !signWithParams && !certificatePassword) {
throw new Error('You must provide a certificatePassword or signing parameters');
}
const internalOptions = {
appendSignature: false,
...options,
certificateFile,
certificatePassword,
signWithParams,
signToolPath,
description,
timestampServer,
website
};
const hashes = options.hashes == null || options.hashes.length === 0
? ["sha1" /* HASHES.sha1 */, "sha256" /* HASHES.sha256 */]
: options.hashes;
if (hashes.includes("sha1" /* HASHES.sha1 */)) {
await execute({ ...internalOptions, hash: "sha1" /* HASHES.sha1 */ });
// If we signed with SHA1, we need to append the SHA256 signature:
internalOptions.appendSignature = true;
}
if (hashes.includes("sha256" /* HASHES.sha256 */)) {
await execute({ ...internalOptions, hash: "sha256" /* HASHES.sha256 */ });
}
}
exports.signWithSignTool = signWithSignTool;

View file

@ -0,0 +1,13 @@
import { SignOptions } from './types';
/**
* This is the main function exported from this module. It'll
* look at your options, determine the best way to sign a file,
* and then return one of our internal functions to do the actual
* signing.
*
* @param options
* @returns {Promise<void>}
*
* @category Sign
*/
export declare function sign(options: SignOptions): Promise<void>;

View file

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sign = void 0;
const files_1 = require("./files");
const sign_with_hook_1 = require("./sign-with-hook");
const sign_with_signtool_1 = require("./sign-with-signtool");
const log_1 = require("./utils/log");
const parse_env_1 = require("./utils/parse-env");
/**
* This is the main function exported from this module. It'll
* look at your options, determine the best way to sign a file,
* and then return one of our internal functions to do the actual
* signing.
*
* @param options
* @returns {Promise<void>}
*
* @category Sign
*/
async function sign(options) {
const signJavaScript = options.signJavaScript || (0, parse_env_1.booleanFromEnv)('WINDOWS_SIGN_JAVASCRIPT');
const hookModulePath = options.hookModulePath || process.env.WINDOWS_SIGN_HOOK_MODULE_PATH;
if (options.debug) {
(0, log_1.enableDebugging)();
}
(0, log_1.log)('Called with options', { options });
const files = (0, files_1.getFilesToSign)(options);
const internalOptions = {
...options,
signJavaScript,
hookModulePath,
files
};
// If a hook is provides, sign with the hook
if (internalOptions.hookFunction || internalOptions.hookModulePath) {
(0, log_1.log)('Signing with hook');
return (0, sign_with_hook_1.signWithHook)(internalOptions);
}
// If we're going with the defaults, we're signing
// with signtool. Custom signing tools are also
// handled here.
(0, log_1.log)('Signing with signtool');
return (0, sign_with_signtool_1.signWithSignTool)(internalOptions);
}
exports.sign = sign;

View file

@ -0,0 +1,16 @@
/// <reference types="node" />
import { SpawnOptions } from 'child_process';
export interface SpawnPromiseResult {
stdout: string;
stderr: string;
code: number;
}
/**
* Spawn a process as a promise
*
* @param {string} name
* @param {Array<string>} args
* @param {SpawnOptions} [options]
* @returns {Promise<SpawnPromiseResult>}
*/
export declare function spawnPromise(name: string, args: Array<string>, options?: SpawnOptions): Promise<SpawnPromiseResult>;

View file

@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawnPromise = void 0;
const log_1 = require("./utils/log");
/**
* Spawn a process as a promise
*
* @param {string} name
* @param {Array<string>} args
* @param {SpawnOptions} [options]
* @returns {Promise<SpawnPromiseResult>}
*/
function spawnPromise(name, args, options) {
return new Promise((resolve) => {
const { spawn } = require('child_process');
const fork = spawn(name, args, options);
(0, log_1.log)(`Spawning ${name} with ${args}`);
let stdout = '';
let stderr = '';
fork.stdout.on('data', (data) => {
(0, log_1.log)(`Spawn ${name} stdout: ${data}`);
stdout += data;
});
fork.stderr.on('data', (data) => {
(0, log_1.log)(`Spawn ${name} stderr: ${data}`);
stderr += data;
});
fork.on('close', (code) => {
(0, log_1.log)(`Spawn ${name}: Child process exited with code ${code}`);
resolve({ stdout, stderr, code });
});
});
}
exports.spawnPromise = spawnPromise;

View file

@ -0,0 +1,143 @@
/**
* SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
* https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
*/
export declare const enum HASHES {
sha1 = "sha1",
sha256 = "sha256"
}
/**
* Signing can be either by specifying a directory of files to sign.
*
* @category Sign
*/
export type SignOptions = SignOptionsForDirectory | SignOptionsForFiles;
/**
* Options for signing by passing a path to a directory to be codesigned.
*
* @category Sign
*/
export interface SignOptionsForDirectory extends SignToolOptions {
/**
* Path to the application directory. We will scan this
* directory for any `.dll`, `.exe`, `.msi`, or `.node` files and
* codesign them with `signtool.exe`.
*/
appDirectory: string;
}
/**
* Options for signing by passing an array of files to be codesigned.
*
* @category Sign
*/
export interface SignOptionsForFiles extends SignToolOptions {
/**
* Array of paths to files to be codesigned with `signtool.exe`.
*/
files: Array<string>;
}
/**
* @category Utility
*/
export interface SignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
}
export interface InternalSignOptions extends SignOptionsForFiles {
}
export interface InternalSignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
signToolPath: string;
timestampServer: string;
files: Array<string>;
hash: HASHES;
appendSignature?: boolean;
}
/**
* @category Utility
*/
export interface OptionalSignToolOptions {
/**
* Path to a `.pfx` code signing certificate.
* Will use `process.env.WINDOWS_CERTIFICATE_FILE` if this option is not provided.
*/
certificateFile?: string;
/**
* Password to {@link certificateFile}. If you don't provide this,
* you need to provide the {@link signWithParams} option.
* Will use `process.env.WINDOWS_CERTIFICATE_PASSWORD` if this option is not provided.
*/
certificatePassword?: string;
/**
* Path to a timestamp server.
* Will use `process.env.WINDOWS_TIMESTAMP_SERVER` if this option is not provided.
*
* @defaultValue http://timestamp.digicert.com
*/
timestampServer?: string;
/**
* Description of the signed content. Will be passed to `signtool.exe` as `/d`.
*/
description?: string;
/**
* URL for the expanded description of the signed content. Will be passed to `signtool.exe` as `/du`.
*/
website?: string;
/**
* Path to the `signtool.exe` used to sign. Will use `vendor/signtool.exe` if not provided.
*/
signToolPath?: string;
/**
* Additional parameters to pass to `signtool.exe`.
*
* @see Microsoft's {@link https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe SignTool.exe documentation}
*/
signWithParams?: string | Array<string>;
/**
* Enables debug logging.
*
* @defaultValue false
*/
debug?: boolean;
/**
* Automatically selects the best signing certificate according to SignTool. Will be passed to `signtool.exe` as `/a`.
*
* @defaultValue true
*/
automaticallySelectCertificate?: boolean;
/**
* Whether or not to sign JavaScript files.
*
* @defaultValue false
*/
signJavaScript?: boolean;
/**
* Hash algorithms to use for signing.
*/
hashes?: HASHES[];
}
/**
* Custom function that is called sequentially for each file that needs to be signed.
*
* @param fileToSign Absolute path to the file to sign
*
* @category Utility
*/
export type HookFunction = (fileToSign: string) => void | Promise<void>;
/**
* @category Utility
*/
export interface OptionalHookOptions {
/**
* A hook function called for each file that needs to be signed.
* Use this for full control over your app's signing logic.
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
*/
hookFunction?: HookFunction;
/**
* A path to a JavaScript file, exporting a single function that will be called for each file that needs to be signed.
* Use this for full control over your app's signing logic.
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
*/
hookModulePath?: string;
}
export interface InternalHookOptions extends OptionalHookOptions {
files: Array<string>;
}

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,2 @@
export declare function enableDebugging(): void;
export declare const log: import("debug").Debugger;

View file

@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.log = exports.enableDebugging = void 0;
const debug_1 = require("debug");
function enableDebugging() {
debug_1.debug.enable('electron-windows-sign');
}
exports.enableDebugging = enableDebugging;
exports.log = (0, debug_1.debug)('electron-windows-sign');

View file

@ -0,0 +1,11 @@
/**
* Tries to parse an process.env string to a boolean.
* Will understand undefined as the default value
* Will understand "false", "False", "fAlse", or "0" as `false`
* Will understand everything else as true
*
* @export
* @param {string} name
* @return {*} {boolean}
*/
export declare function booleanFromEnv(name: string): boolean | undefined;

View file

@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.booleanFromEnv = void 0;
/**
* Tries to parse an process.env string to a boolean.
* Will understand undefined as the default value
* Will understand "false", "False", "fAlse", or "0" as `false`
* Will understand everything else as true
*
* @export
* @param {string} name
* @return {*} {boolean}
*/
function booleanFromEnv(name) {
const value = process.env[name];
if (value === undefined) {
return undefined;
}
if (value.toLowerCase() === 'false' || value === '0') {
return false;
}
return !!value;
}
exports.booleanFromEnv = booleanFromEnv;

View file

@ -0,0 +1,16 @@
import { SignOptions } from './types';
/**
* Recursively goes through an entire directory and returns an array
* of full paths for files ot sign.
*
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
* - Microsoft installers (.msi)
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
* - Catalog files (.cat)
* - Cabinet files (.cab)
* - Silverlight applications (.xap)
* - Scripts (.vbs, .wsf, .ps1)
* If configured:
* - JavaScript files (.js)
*/
export declare function getFilesToSign(options: SignOptions, dir?: string): Array<string>;

View file

@ -0,0 +1,59 @@
import path from 'path';
import fs from 'fs-extra';
const IS_PE_REGEX = /\.(exe|dll|sys|efi|scr|node)$/i;
const IS_MSI_REGEX = /\.msi$/i;
const IS_PACKAGE_REGEX = /\.(appx|appxbundle|msix|msixbundle)$/i;
const IS_CATCAB_REGEX = /\.(cat|cab)$/i;
const IS_SILVERLIGHT_REGEX = /\.xap$/i;
const IS_SCRIPT_REGEX = /\.(vbs|wsf|ps1)$/i;
const IS_JS_REGEX = /\.js$/i;
/**
* Recursively goes through an entire directory and returns an array
* of full paths for files ot sign.
*
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
* - Microsoft installers (.msi)
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
* - Catalog files (.cat)
* - Cabinet files (.cab)
* - Silverlight applications (.xap)
* - Scripts (.vbs, .wsf, .ps1)
* If configured:
* - JavaScript files (.js)
*/
export function getFilesToSign(options, dir) {
if (isSignOptionsForFiles(options)) {
return options.files;
}
dir = dir || options.appDirectory;
// Array of file paths to sign
const result = [];
// Iterate over the app directory, looking for files to sign
const files = fs.readdirSync(dir);
const regexes = [
IS_PE_REGEX,
IS_MSI_REGEX,
IS_PACKAGE_REGEX,
IS_CATCAB_REGEX,
IS_SILVERLIGHT_REGEX,
IS_SCRIPT_REGEX
];
if (options.signJavaScript) {
regexes.push(IS_JS_REGEX);
}
for (const file of files) {
const fullPath = path.resolve(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
// If it's a directory, recurse
result.push(...getFilesToSign(options, fullPath));
}
else if (regexes.some((regex) => regex.test(file))) {
// If it's a match, add it to the list
result.push(fullPath);
}
}
return result;
}
function isSignOptionsForFiles(input) {
return !!input.files;
}

View file

@ -0,0 +1,4 @@
import { sign } from './sign';
import { createSeaSignTool, SeaOptions, InternalSeaOptions } from './sea';
import { HookFunction, OptionalHookOptions, OptionalSignToolOptions, SignOptions, SignToolOptions, SignOptionsForDirectory, SignOptionsForFiles } from './types';
export { sign, SignOptions, SignToolOptions, HookFunction, OptionalSignToolOptions, OptionalHookOptions, createSeaSignTool, SeaOptions, InternalSeaOptions, SignOptionsForDirectory, SignOptionsForFiles };

View file

@ -0,0 +1,3 @@
import { sign } from './sign';
import { createSeaSignTool } from './sea';
export { sign, createSeaSignTool };

View file

@ -0,0 +1,61 @@
import { SignToolOptions } from './types';
/**
* Options for signing with a Node.js single executable application.
*
* @category Single executable applications
*/
export interface SeaOptions {
/**
* Full path to the Node.js single executable application. Needs to end with `.exe`.
*/
path: string;
/**
* A binary to use. Will use the current executable (process.execPath) by default.
*
* @defaultValue The Node.js {@link https://nodejs.org/api/process.html#processexecpath | `process.execPath`}
*/
bin?: string;
/**
* Options to pass to SignTool.
*/
windowsSign: SignToolOptions;
}
/**
* This interface represents {@link SeaOptions} with all optional properties
* inferred by `@electron/windows-sign` if not passed in by the user.
*
* @category Single executable applications
*/
export interface InternalSeaOptions extends Required<SeaOptions> {
/**
* Directory of the Node.js single executable application.
*/
dir: string;
/**
* File name of the Node.js single executable application.
*/
filename: string;
}
/**
* cross-dir uses new Error() stacks
* to figure out our directory in a way
* that's somewhat cross-compatible.
*
* We can't just use __dirname because it's
* undefined in ESM - and we can't use import.meta.url
* because TypeScript won't allow usage unless you're
* _only_ compiling for ESM.
*/
export declare const DIRNAME: string;
/**
* Uses Node's "Single Executable App" functionality
* to create a Node-driven signtool.exe that calls this
* module.
*
* This is useful with other tooling that _always_ calls
* a signtool.exe to sign. Some of those tools cannot be
* easily configured, but we _can_ override their signtool.exe.
*
* @category Single executable applications
*/
export declare function createSeaSignTool(options?: Partial<SeaOptions>): Promise<InternalSeaOptions>;

View file

@ -0,0 +1,213 @@
import { getDirname } from 'cross-dirname';
import path from 'path';
import os from 'os';
import fs from 'fs-extra';
import postject from 'postject';
import { spawnPromise } from './spawn';
import { log } from './utils/log';
/**
* cross-dir uses new Error() stacks
* to figure out our directory in a way
* that's somewhat cross-compatible.
*
* We can't just use __dirname because it's
* undefined in ESM - and we can't use import.meta.url
* because TypeScript won't allow usage unless you're
* _only_ compiling for ESM.
*/
export const DIRNAME = getDirname();
const FILENAMES = {
SEA_CONFIG: 'sea-config.json',
SEA_MAIN: 'sea.js',
SEA_BLOB: 'sea.blob',
SEA_RECEIVER: 'receiver.mjs'
};
const SEA_MAIN_SCRIPT = `
const bin = "%PATH_TO_BIN%";
const script = "%PATH_TO_SCRIPT%";
const options = %WINDOWS_SIGN_OPTIONS%
const { spawnSync } = require('child_process');
function main() {
console.log("@electron/windows-sign sea");
console.log({ bin, script });
try {
const spawn = spawnSync(
bin,
[ script, JSON.stringify(options), JSON.stringify(process.argv.slice(1)) ],
{ stdio: ['inherit', 'inherit', 'pipe'] }
);
if (spawn.status !== 0) {
throw new Error(\`Spawn failed with code: \${spawn.status}. Stderr: \${spawn.stderr}\`);
}
} catch (error) {
// Do not rethrow the error or write it to stdout/stderr. Then the process won't terminate.
// See: https://github.com/electron/windows-sign/pull/48
process.exit(1);
}
}
main();
`;
const SEA_RECEIVER_SCRIPT = `
import { sign } from '@electron/windows-sign';
import fs from 'fs-extra';
import path from 'path';
const logPath = path.join('electron-windows-sign.log');
const options = JSON.parse(process.argv[2]);
const signArgv = JSON.parse(process.argv[3]);
const files = signArgv.slice(-1);
try {
fs.appendFileSync(logPath, \`\\nCalled with: \${JSON.stringify(process.argv, null, 2)}\`);
sign({ ...options, files })
.then((result) => {
fs.appendFileSync(logPath, \`\\nSuccessfully signed with result: \${result}\`);
})
.catch((error) => {
fs.appendFileSync(logPath, \`\\nError from sign: \${error}\`);
throw new Error(error);
});
} catch (error) {
fs.appendFileSync(logPath, \`\\Error invoking sign: \${error}\`);
throw new Error(error);
}
`;
/**
* Uses Node's "Single Executable App" functionality
* to create a Node-driven signtool.exe that calls this
* module.
*
* This is useful with other tooling that _always_ calls
* a signtool.exe to sign. Some of those tools cannot be
* easily configured, but we _can_ override their signtool.exe.
*
* @category Single executable applications
*/
export async function createSeaSignTool(options = {}) {
checkCompatibility();
const requiredOptions = await getOptions(options);
await createFiles(requiredOptions);
await createBlob(requiredOptions);
await createBinary(requiredOptions);
await createSeaReceiver(requiredOptions);
await cleanup(requiredOptions);
return requiredOptions;
}
async function createSeaReceiver(options) {
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER);
await fs.ensureFile(receiverPath);
await fs.writeFile(receiverPath, SEA_RECEIVER_SCRIPT);
}
async function createFiles(options) {
const { dir, bin } = options;
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER);
// sea-config.json
await fs.outputJSON(path.join(dir, FILENAMES.SEA_CONFIG), {
main: FILENAMES.SEA_MAIN,
output: FILENAMES.SEA_BLOB,
disableExperimentalSEAWarning: true
}, {
spaces: 2
});
// signtool.js
const binPath = bin || process.execPath;
const script = SEA_MAIN_SCRIPT
.replace('%PATH_TO_BIN%', escapeMaybe(binPath))
.replace('%PATH_TO_SCRIPT%', escapeMaybe(receiverPath))
.replace('%WINDOWS_SIGN_OPTIONS%', JSON.stringify(options.windowsSign));
await fs.outputFile(path.join(dir, FILENAMES.SEA_MAIN), script);
}
async function createBlob(options) {
const args = ['--experimental-sea-config', 'sea-config.json'];
const bin = process.execPath;
const cwd = options.dir;
log(`Calling ${bin} with options:`, args);
const { stderr, stdout } = await spawnPromise(bin, args, {
cwd
});
log('stdout:', stdout);
log('stderr:', stderr);
}
async function createBinary(options) {
const { dir, filename } = options;
log(`Creating ${filename} in ${dir}`);
// Copy Node over
const seaPath = path.join(dir, filename);
await fs.copyFile(process.execPath, seaPath);
// Remove the Node signature
const signtool = path.join(DIRNAME, '../../vendor/signtool.exe');
await spawnPromise(signtool, [
'remove',
'/s',
seaPath
]);
// Inject the blob
const blob = await fs.readFile(path.join(dir, FILENAMES.SEA_BLOB));
await postject.inject(seaPath, 'NODE_SEA_BLOB', blob, {
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
});
}
async function cleanup(options) {
const { dir } = options;
const toRemove = [
FILENAMES.SEA_BLOB,
FILENAMES.SEA_MAIN,
FILENAMES.SEA_CONFIG
];
for (const file of toRemove) {
try {
await fs.remove(path.join(dir, file));
}
catch (error) {
console.warn(`Tried and failed to remove ${file}. Continuing.`, error);
}
}
}
async function getOptions(options) {
const cloned = { ...options };
if (!cloned.path) {
cloned.path = path.join(os.homedir(), '.electron', 'windows-sign', 'sea.exe');
await fs.ensureFile(cloned.path);
}
if (!cloned.bin) {
cloned.bin = process.execPath;
}
if (!cloned.windowsSign) {
throw new Error('Did not find windowsSign options, which are required');
}
return {
path: cloned.path,
dir: path.dirname(cloned.path),
filename: path.basename(cloned.path),
bin: cloned.bin,
windowsSign: cloned.windowsSign
};
}
/**
* Ensures that the current Node.js version supports SEA app generation and errors if not.
*/
function checkCompatibility() {
const version = process.versions.node;
const split = version.split('.');
const major = parseInt(split[0], 10);
if (major >= 20) {
return true;
}
throw new Error(`Your Node.js version (${process.version}) does not support Single Executable Applications. Please upgrade your version of Node.js.`);
}
/** Make sure that the input string has escaped backwards slashes
* - but never double-escaped backwards slashes.
*/
function escapeMaybe(input) {
const result = input.split(path.sep).join('\\\\');
if (result.includes('\\\\\\\\')) {
throw new Error(`Your passed input ${input} contains escaped slashes. Please do not escape them`);
}
return result;
}

View file

@ -0,0 +1,8 @@
import { InternalSignOptions } from './types';
/**
* Sign with a hook function, basically letting everyone
* write completely custom sign logic
*
* @param {InternalSignOptions} options
*/
export declare function signWithHook(options: InternalSignOptions): Promise<void>;

View file

@ -0,0 +1,38 @@
import path from 'path';
import { log } from './utils/log';
let hookFunction;
function getHookFunction(options) {
if (options.hookFunction) {
return options.hookFunction;
}
if (options.hookModulePath) {
const module = require(path.resolve(options.hookModulePath));
if (module.default) {
return module.default;
}
if (typeof module === 'function') {
return module;
}
}
if (!hookFunction) {
throw new Error('No hook function found. Signing will not be possible. Please see the documentation for how to pass a hook function to @electron/windows-sign');
}
return hookFunction;
}
/**
* Sign with a hook function, basically letting everyone
* write completely custom sign logic
*
* @param {InternalSignOptions} options
*/
export async function signWithHook(options) {
hookFunction = getHookFunction(options);
for (const file of options.files) {
try {
await hookFunction(file);
}
catch (error) {
log(`Error signing ${file}`, error);
}
}
}

View file

@ -0,0 +1,2 @@
import { InternalSignOptions } from './types';
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;

View file

@ -0,0 +1,111 @@
import path from 'path';
import { log } from './utils/log';
import { spawnPromise } from './spawn';
import { getDirname } from 'cross-dirname';
const DIRNAME = getDirname();
function getSigntoolArgs(options) {
// See the following url for docs
// https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
const { certificateFile, certificatePassword, hash, timestampServer } = options;
const args = ['sign'];
// Automatically select cert
if (options.automaticallySelectCertificate) {
args.push('/a');
}
// Dual-sign
if (options.appendSignature) {
args.push('/as');
}
// Timestamp
if (hash === "sha256" /* HASHES.sha256 */) {
args.push('/tr', timestampServer);
args.push('/td', hash);
}
else {
args.push('/t', timestampServer);
}
// Certificate file
if (certificateFile) {
args.push('/f', path.resolve(certificateFile));
}
// Certificate password
if (certificatePassword) {
args.push('/p', certificatePassword);
}
// Hash
args.push('/fd', hash);
// Description
if (options.description) {
args.push('/d', options.description);
}
// Website
if (options.website) {
args.push('/du', options.website);
}
// Debug
if (options.debug) {
args.push('/debug');
}
if (options.signWithParams) {
const extraArgs = [];
if (Array.isArray(options.signWithParams)) {
extraArgs.push(...options.signWithParams);
}
else {
// Split up at spaces and doublequotes
extraArgs.push(...options.signWithParams.match(/(?:[^\s"]+|"[^"]*")+/g));
}
log('Parsed signWithParams as:', extraArgs);
args.push(...extraArgs);
}
return args;
}
async function execute(options) {
const { signToolPath, files } = options;
const args = getSigntoolArgs(options);
log('Executing signtool with args', { args, files });
const { code, stderr, stdout } = await spawnPromise(signToolPath, [...args, ...files], {
env: process.env,
cwd: process.cwd()
});
if (code !== 0) {
throw new Error(`Signtool exited with code ${code}. Stderr: ${stderr}. Stdout: ${stdout}`);
}
}
export async function signWithSignTool(options) {
const certificatePassword = options.certificatePassword || process.env.WINDOWS_CERTIFICATE_PASSWORD;
const certificateFile = options.certificateFile || process.env.WINDOWS_CERTIFICATE_FILE;
const signWithParams = options.signWithParams || process.env.WINDOWS_SIGN_WITH_PARAMS;
const timestampServer = options.timestampServer || process.env.WINDOWS_TIMESTAMP_SERVER || 'http://timestamp.digicert.com';
const signToolPath = options.signToolPath || process.env.WINDOWS_SIGNTOOL_PATH || path.join(DIRNAME, '../../vendor/signtool.exe');
const description = options.description || process.env.WINDOWS_SIGN_DESCRIPTION;
const website = options.website || process.env.WINDOWS_SIGN_WEBSITE;
if (!certificateFile && !(signWithParams || signToolPath)) {
throw new Error('You must provide a certificateFile and a signToolPath or signing parameters');
}
if (!signToolPath && !signWithParams && !certificatePassword) {
throw new Error('You must provide a certificatePassword or signing parameters');
}
const internalOptions = {
appendSignature: false,
...options,
certificateFile,
certificatePassword,
signWithParams,
signToolPath,
description,
timestampServer,
website
};
const hashes = options.hashes == null || options.hashes.length === 0
? ["sha1" /* HASHES.sha1 */, "sha256" /* HASHES.sha256 */]
: options.hashes;
if (hashes.includes("sha1" /* HASHES.sha1 */)) {
await execute({ ...internalOptions, hash: "sha1" /* HASHES.sha1 */ });
// If we signed with SHA1, we need to append the SHA256 signature:
internalOptions.appendSignature = true;
}
if (hashes.includes("sha256" /* HASHES.sha256 */)) {
await execute({ ...internalOptions, hash: "sha256" /* HASHES.sha256 */ });
}
}

View file

@ -0,0 +1,13 @@
import { SignOptions } from './types';
/**
* This is the main function exported from this module. It'll
* look at your options, determine the best way to sign a file,
* and then return one of our internal functions to do the actual
* signing.
*
* @param options
* @returns {Promise<void>}
*
* @category Sign
*/
export declare function sign(options: SignOptions): Promise<void>;

View file

@ -0,0 +1,41 @@
import { getFilesToSign } from './files';
import { signWithHook } from './sign-with-hook';
import { signWithSignTool } from './sign-with-signtool';
import { enableDebugging, log } from './utils/log';
import { booleanFromEnv } from './utils/parse-env';
/**
* This is the main function exported from this module. It'll
* look at your options, determine the best way to sign a file,
* and then return one of our internal functions to do the actual
* signing.
*
* @param options
* @returns {Promise<void>}
*
* @category Sign
*/
export async function sign(options) {
const signJavaScript = options.signJavaScript || booleanFromEnv('WINDOWS_SIGN_JAVASCRIPT');
const hookModulePath = options.hookModulePath || process.env.WINDOWS_SIGN_HOOK_MODULE_PATH;
if (options.debug) {
enableDebugging();
}
log('Called with options', { options });
const files = getFilesToSign(options);
const internalOptions = {
...options,
signJavaScript,
hookModulePath,
files
};
// If a hook is provides, sign with the hook
if (internalOptions.hookFunction || internalOptions.hookModulePath) {
log('Signing with hook');
return signWithHook(internalOptions);
}
// If we're going with the defaults, we're signing
// with signtool. Custom signing tools are also
// handled here.
log('Signing with signtool');
return signWithSignTool(internalOptions);
}

View file

@ -0,0 +1,16 @@
/// <reference types="node" />
import { SpawnOptions } from 'child_process';
export interface SpawnPromiseResult {
stdout: string;
stderr: string;
code: number;
}
/**
* Spawn a process as a promise
*
* @param {string} name
* @param {Array<string>} args
* @param {SpawnOptions} [options]
* @returns {Promise<SpawnPromiseResult>}
*/
export declare function spawnPromise(name: string, args: Array<string>, options?: SpawnOptions): Promise<SpawnPromiseResult>;

View file

@ -0,0 +1,30 @@
import { log } from './utils/log';
/**
* Spawn a process as a promise
*
* @param {string} name
* @param {Array<string>} args
* @param {SpawnOptions} [options]
* @returns {Promise<SpawnPromiseResult>}
*/
export function spawnPromise(name, args, options) {
return new Promise((resolve) => {
const { spawn } = require('child_process');
const fork = spawn(name, args, options);
log(`Spawning ${name} with ${args}`);
let stdout = '';
let stderr = '';
fork.stdout.on('data', (data) => {
log(`Spawn ${name} stdout: ${data}`);
stdout += data;
});
fork.stderr.on('data', (data) => {
log(`Spawn ${name} stderr: ${data}`);
stderr += data;
});
fork.on('close', (code) => {
log(`Spawn ${name}: Child process exited with code ${code}`);
resolve({ stdout, stderr, code });
});
});
}

View file

@ -0,0 +1,143 @@
/**
* SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
* https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
*/
export declare const enum HASHES {
sha1 = "sha1",
sha256 = "sha256"
}
/**
* Signing can be either by specifying a directory of files to sign.
*
* @category Sign
*/
export type SignOptions = SignOptionsForDirectory | SignOptionsForFiles;
/**
* Options for signing by passing a path to a directory to be codesigned.
*
* @category Sign
*/
export interface SignOptionsForDirectory extends SignToolOptions {
/**
* Path to the application directory. We will scan this
* directory for any `.dll`, `.exe`, `.msi`, or `.node` files and
* codesign them with `signtool.exe`.
*/
appDirectory: string;
}
/**
* Options for signing by passing an array of files to be codesigned.
*
* @category Sign
*/
export interface SignOptionsForFiles extends SignToolOptions {
/**
* Array of paths to files to be codesigned with `signtool.exe`.
*/
files: Array<string>;
}
/**
* @category Utility
*/
export interface SignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
}
export interface InternalSignOptions extends SignOptionsForFiles {
}
export interface InternalSignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
signToolPath: string;
timestampServer: string;
files: Array<string>;
hash: HASHES;
appendSignature?: boolean;
}
/**
* @category Utility
*/
export interface OptionalSignToolOptions {
/**
* Path to a `.pfx` code signing certificate.
* Will use `process.env.WINDOWS_CERTIFICATE_FILE` if this option is not provided.
*/
certificateFile?: string;
/**
* Password to {@link certificateFile}. If you don't provide this,
* you need to provide the {@link signWithParams} option.
* Will use `process.env.WINDOWS_CERTIFICATE_PASSWORD` if this option is not provided.
*/
certificatePassword?: string;
/**
* Path to a timestamp server.
* Will use `process.env.WINDOWS_TIMESTAMP_SERVER` if this option is not provided.
*
* @defaultValue http://timestamp.digicert.com
*/
timestampServer?: string;
/**
* Description of the signed content. Will be passed to `signtool.exe` as `/d`.
*/
description?: string;
/**
* URL for the expanded description of the signed content. Will be passed to `signtool.exe` as `/du`.
*/
website?: string;
/**
* Path to the `signtool.exe` used to sign. Will use `vendor/signtool.exe` if not provided.
*/
signToolPath?: string;
/**
* Additional parameters to pass to `signtool.exe`.
*
* @see Microsoft's {@link https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe SignTool.exe documentation}
*/
signWithParams?: string | Array<string>;
/**
* Enables debug logging.
*
* @defaultValue false
*/
debug?: boolean;
/**
* Automatically selects the best signing certificate according to SignTool. Will be passed to `signtool.exe` as `/a`.
*
* @defaultValue true
*/
automaticallySelectCertificate?: boolean;
/**
* Whether or not to sign JavaScript files.
*
* @defaultValue false
*/
signJavaScript?: boolean;
/**
* Hash algorithms to use for signing.
*/
hashes?: HASHES[];
}
/**
* Custom function that is called sequentially for each file that needs to be signed.
*
* @param fileToSign Absolute path to the file to sign
*
* @category Utility
*/
export type HookFunction = (fileToSign: string) => void | Promise<void>;
/**
* @category Utility
*/
export interface OptionalHookOptions {
/**
* A hook function called for each file that needs to be signed.
* Use this for full control over your app's signing logic.
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
*/
hookFunction?: HookFunction;
/**
* A path to a JavaScript file, exporting a single function that will be called for each file that needs to be signed.
* Use this for full control over your app's signing logic.
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
*/
hookModulePath?: string;
}
export interface InternalHookOptions extends OptionalHookOptions {
files: Array<string>;
}

View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1,2 @@
export declare function enableDebugging(): void;
export declare const log: import("debug").Debugger;

View file

@ -0,0 +1,5 @@
import { debug as debugModule } from 'debug';
export function enableDebugging() {
debugModule.enable('electron-windows-sign');
}
export const log = debugModule('electron-windows-sign');

View file

@ -0,0 +1,11 @@
/**
* Tries to parse an process.env string to a boolean.
* Will understand undefined as the default value
* Will understand "false", "False", "fAlse", or "0" as `false`
* Will understand everything else as true
*
* @export
* @param {string} name
* @return {*} {boolean}
*/
export declare function booleanFromEnv(name: string): boolean | undefined;

View file

@ -0,0 +1,20 @@
/**
* Tries to parse an process.env string to a boolean.
* Will understand undefined as the default value
* Will understand "false", "False", "fAlse", or "0" as `false`
* Will understand everything else as true
*
* @export
* @param {string} name
* @return {*} {boolean}
*/
export function booleanFromEnv(name) {
const value = process.env[name];
if (value === undefined) {
return undefined;
}
if (value.toLowerCase() === 'false' || value === '0') {
return false;
}
return !!value;
}

View file

@ -0,0 +1,70 @@
{
"name": "@electron/windows-sign",
"version": "1.2.2",
"description": "Codesign Electron Windows apps",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
"dist",
"entitlements",
"README.md",
"LICENSE",
"bin",
"vendor"
],
"bin": {
"electron-windows-sign": "bin/electron-windows-sign.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/electron/windows-sign.git"
},
"author": {
"name": "Felix Rieseberg",
"email": "felix@felixrieseberg.com"
},
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/electron/windows-sign/issues"
},
"homepage": "https://github.com/electron/windows-sign",
"publishConfig": {
"provenance": true
},
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
"fs-extra": "^11.1.1",
"minimist": "^1.2.8",
"postject": "^1.0.0-alpha.6"
},
"devDependencies": {
"@types/debug": "^4.1.10",
"@types/fs-extra": "^11.0.3",
"@types/node": "^18.18.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.52.0",
"eslint-config-eslint": "^9.0.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"globstar": "^1.0.0",
"standard": "^17.1.0",
"tsx": "^3.14.0",
"typedoc": "~0.25.13",
"typescript": "^5.2.2"
},
"scripts": {
"build": "tsc && tsc -p tsconfig.esm.json",
"docs": "npx typedoc",
"lint": "eslint --ext .ts,.js src bin test",
"test:loader": "globstar -- node --loader tsx --test \"test/**/*.spec.ts\"",
"test": "globstar -- node --import tsx --test \"test/**/*.spec.ts\"",
"prepublishOnly": "yarn build"
},
"engines": {
"node": ">=14.14"
}
}

Binary file not shown.