Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
@@ -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>;
|
||||
+66
@@ -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;
|
||||
}
|
||||
+4
@@ -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 };
|
||||
+7
@@ -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; } });
|
||||
+61
@@ -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>;
|
||||
+220
@@ -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;
|
||||
}
|
||||
+8
@@ -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>;
|
||||
+45
@@ -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;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { InternalSignOptions } from './types';
|
||||
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;
|
||||
+118
@@ -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;
|
||||
+13
@@ -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>;
|
||||
+45
@@ -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;
|
||||
+16
@@ -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>;
|
||||
+34
@@ -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;
|
||||
+143
@@ -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>;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function enableDebugging(): void;
|
||||
export declare const log: import("debug").Debugger;
|
||||
+9
@@ -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');
|
||||
+11
@@ -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;
|
||||
+24
@@ -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;
|
||||
+16
@@ -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>;
|
||||
+59
@@ -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;
|
||||
}
|
||||
+4
@@ -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 };
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { sign } from './sign';
|
||||
import { createSeaSignTool } from './sea';
|
||||
export { sign, createSeaSignTool };
|
||||
+61
@@ -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>;
|
||||
+213
@@ -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;
|
||||
}
|
||||
+8
@@ -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>;
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { InternalSignOptions } from './types';
|
||||
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;
|
||||
+111
@@ -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 */ });
|
||||
}
|
||||
}
|
||||
+13
@@ -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>;
|
||||
+41
@@ -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);
|
||||
}
|
||||
+16
@@ -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>;
|
||||
+30
@@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
+143
@@ -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>;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function enableDebugging(): void;
|
||||
export declare const log: import("debug").Debugger;
|
||||
+5
@@ -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');
|
||||
+11
@@ -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;
|
||||
+20
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user