@involvex/msix-packager-cli 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -0
- package/package.json +57 -0
- package/src/certificates.js +320 -0
- package/src/cli.js +383 -0
- package/src/constants.js +140 -0
- package/src/index.js +414 -0
- package/src/manifest.js +389 -0
- package/src/package.js +909 -0
- package/src/sea-handler-new.js +301 -0
- package/src/sea-handler.js +1124 -0
- package/src/utils.js +292 -0
- package/src/validation.js +228 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const { CONSTANTS, ToolNotFoundError, MSIXError } = require("./constants");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Executes a command safely with error handling
|
|
9
|
+
* @param {string} command - Command to execute
|
|
10
|
+
* @param {Object} options - Execution options
|
|
11
|
+
* @returns {string} Command output
|
|
12
|
+
* @throws {MSIXError} When command execution fails
|
|
13
|
+
*/
|
|
14
|
+
function executeCommand(command, options = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const result = execSync(command, {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
stdio: options.silent ? "pipe" : "inherit",
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
...options,
|
|
21
|
+
});
|
|
22
|
+
return result;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const errorMessage = `Command failed: ${command}\n${error.message}`;
|
|
25
|
+
throw new MSIXError(errorMessage);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Finds Windows SDK tools
|
|
31
|
+
* @returns {Object} Object containing paths to makeappx and signtool
|
|
32
|
+
* @throws {ToolNotFoundError} When tools are not found
|
|
33
|
+
*/
|
|
34
|
+
function findWindowsSDKTools() {
|
|
35
|
+
const possiblePaths = CONSTANTS.WINDOWS_SDK_PATHS;
|
|
36
|
+
|
|
37
|
+
for (const basePath of possiblePaths) {
|
|
38
|
+
try {
|
|
39
|
+
const makeappxPath = path.join(basePath, "makeappx.exe");
|
|
40
|
+
const signtoolPath = path.join(basePath, "signtool.exe");
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(makeappxPath) && fs.existsSync(signtoolPath)) {
|
|
43
|
+
return {
|
|
44
|
+
makeappx: makeappxPath,
|
|
45
|
+
signtool: signtoolPath,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Continue searching
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new ToolNotFoundError(
|
|
54
|
+
"Windows SDK tools (makeappx.exe, signtool.exe) not found. Please install Windows SDK.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates a unique temporary directory name
|
|
60
|
+
* @param {string} prefix - Prefix for the temporary directory
|
|
61
|
+
* @returns {string} Temporary directory path
|
|
62
|
+
*/
|
|
63
|
+
function getTempDir(prefix = "msix-temp") {
|
|
64
|
+
const tempBase =
|
|
65
|
+
process.env.TEMP || process.env.TMP || path.join(process.cwd(), "temp");
|
|
66
|
+
const timestamp = Date.now();
|
|
67
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
68
|
+
return path.join(tempBase, `${prefix}-${timestamp}-${random}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Cleans up temporary directories and files
|
|
73
|
+
* @param {string[]} paths - Array of paths to clean up
|
|
74
|
+
*/
|
|
75
|
+
async function cleanup(paths) {
|
|
76
|
+
for (const cleanupPath of paths) {
|
|
77
|
+
try {
|
|
78
|
+
if (await fs.pathExists(cleanupPath)) {
|
|
79
|
+
await fs.remove(cleanupPath);
|
|
80
|
+
console.log(`Cleaned up: ${cleanupPath}`);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(
|
|
84
|
+
`Warning: Could not clean up ${cleanupPath}: ${error.message}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a directory structure ensuring all parent directories exist
|
|
92
|
+
* @param {string} dirPath - Directory path to create
|
|
93
|
+
*/
|
|
94
|
+
async function ensureDir(dirPath) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.ensureDir(dirPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new MSIXError(
|
|
99
|
+
`Failed to create directory ${dirPath}: ${error.message}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Copies files with progress indication
|
|
106
|
+
* @param {string} src - Source path
|
|
107
|
+
* @param {string} dest - Destination path
|
|
108
|
+
* @param {boolean} showProgress - Whether to show progress
|
|
109
|
+
*/
|
|
110
|
+
async function copyFiles(src, dest, showProgress = true) {
|
|
111
|
+
try {
|
|
112
|
+
if (showProgress) {
|
|
113
|
+
console.log(`Copying files from ${src} to ${dest}...`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await fs.copy(src, dest, {
|
|
117
|
+
overwrite: true,
|
|
118
|
+
errorOnExist: false,
|
|
119
|
+
filter: (src) => {
|
|
120
|
+
// Skip node_modules and common temporary directories
|
|
121
|
+
const relativePath = path.relative(src, src);
|
|
122
|
+
return !CONSTANTS.COPY_EXCLUDE_PATTERNS.some((pattern) =>
|
|
123
|
+
relativePath.includes(pattern),
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (showProgress) {
|
|
129
|
+
console.log("Files copied successfully");
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new MSIXError(`Failed to copy files: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads and parses package.json from a directory
|
|
138
|
+
* @param {string} directoryPath - Path to directory containing package.json
|
|
139
|
+
* @returns {Object} Parsed package.json content
|
|
140
|
+
* @throws {MSIXError} When package.json cannot be read or parsed
|
|
141
|
+
*/
|
|
142
|
+
async function readPackageJson(directoryPath) {
|
|
143
|
+
const packageJsonPath = path.join(directoryPath, "package.json");
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const content = await fs.readFile(packageJsonPath, "utf8");
|
|
147
|
+
return JSON.parse(content);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new MSIXError(
|
|
150
|
+
`Failed to read package.json from ${directoryPath}: ${error.message}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Formats file size for human reading
|
|
157
|
+
* @param {number} bytes - Size in bytes
|
|
158
|
+
* @returns {string} Formatted size string
|
|
159
|
+
*/
|
|
160
|
+
function formatFileSize(bytes) {
|
|
161
|
+
if (bytes === 0) return "0 Bytes";
|
|
162
|
+
|
|
163
|
+
const k = 1024;
|
|
164
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
165
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
166
|
+
|
|
167
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validates that all required tools are available
|
|
172
|
+
* @throws {ToolNotFoundError} When required tools are missing
|
|
173
|
+
*/
|
|
174
|
+
function validateRequiredTools() {
|
|
175
|
+
try {
|
|
176
|
+
// Check for Node.js
|
|
177
|
+
execSync("node --version", { stdio: "pipe" });
|
|
178
|
+
} catch {
|
|
179
|
+
throw new ToolNotFoundError("Node.js is required but not found in PATH");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Check for bun
|
|
184
|
+
execSync("bun --version", { stdio: "pipe" });
|
|
185
|
+
} catch {
|
|
186
|
+
throw new ToolNotFoundError("bun is required but not found in PATH");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for Windows SDK tools
|
|
190
|
+
findWindowsSDKTools(); // This will throw if not found
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets system information for debugging
|
|
195
|
+
* @returns {Object} System information
|
|
196
|
+
*/
|
|
197
|
+
function getSystemInfo() {
|
|
198
|
+
try {
|
|
199
|
+
const nodeVersion = execSync("node --version", { encoding: "utf8" }).trim();
|
|
200
|
+
const bunVersion = execSync("bun --version", { encoding: "utf8" }).trim();
|
|
201
|
+
const osVersion = require("os").release();
|
|
202
|
+
const platform = process.platform;
|
|
203
|
+
const arch = process.arch;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
node: nodeVersion,
|
|
207
|
+
bun: bunVersion,
|
|
208
|
+
os: osVersion,
|
|
209
|
+
platform,
|
|
210
|
+
architecture: arch,
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return {
|
|
215
|
+
error: "Could not gather system information",
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Gets the latest Node.js LTS version dynamically
|
|
223
|
+
* @returns {Promise<string>} Latest LTS version (e.g., 'v22.8.0')
|
|
224
|
+
*/
|
|
225
|
+
async function getLatestNodeVersion() {
|
|
226
|
+
try {
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const options = {
|
|
229
|
+
hostname: "nodejs.org",
|
|
230
|
+
path: "/dist/index.json",
|
|
231
|
+
method: "GET",
|
|
232
|
+
timeout: 10000,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const req = https.request(options, (res) => {
|
|
236
|
+
let data = "";
|
|
237
|
+
res.on("data", (chunk) => (data += chunk));
|
|
238
|
+
res.on("end", () => {
|
|
239
|
+
try {
|
|
240
|
+
const releases = JSON.parse(data);
|
|
241
|
+
// Find latest LTS version
|
|
242
|
+
const latestLTS = releases.find((release) => release.lts);
|
|
243
|
+
if (latestLTS) {
|
|
244
|
+
resolve(latestLTS.version);
|
|
245
|
+
} else {
|
|
246
|
+
// Fallback to latest stable
|
|
247
|
+
resolve(releases[0].version);
|
|
248
|
+
}
|
|
249
|
+
} catch (parseError) {
|
|
250
|
+
console.warn(
|
|
251
|
+
"Warning: Could not parse Node.js releases, using fallback version",
|
|
252
|
+
);
|
|
253
|
+
resolve("v22.8.0"); // Fallback version
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
req.on("error", (error) => {
|
|
259
|
+
console.warn(
|
|
260
|
+
"Warning: Could not fetch latest Node.js version, using fallback",
|
|
261
|
+
);
|
|
262
|
+
resolve("v22.8.0"); // Fallback version
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
req.on("timeout", () => {
|
|
266
|
+
console.warn(
|
|
267
|
+
"Warning: Timeout fetching Node.js version, using fallback",
|
|
268
|
+
);
|
|
269
|
+
resolve("v22.8.0"); // Fallback version
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
req.end();
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.warn("Warning: Error getting Node.js version, using fallback");
|
|
276
|
+
return "v22.8.0"; // Fallback version
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
executeCommand,
|
|
282
|
+
findWindowsSDKTools,
|
|
283
|
+
getTempDir,
|
|
284
|
+
cleanup,
|
|
285
|
+
ensureDir,
|
|
286
|
+
copyFiles,
|
|
287
|
+
readPackageJson,
|
|
288
|
+
formatFileSize,
|
|
289
|
+
validateRequiredTools,
|
|
290
|
+
getSystemInfo,
|
|
291
|
+
getLatestNodeVersion,
|
|
292
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const { CONSTANTS, ValidationError } = require("./constants");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates configuration object for MSIX package creation
|
|
7
|
+
* @param {Object} config - Configuration object to validate
|
|
8
|
+
* @throws {ValidationError} When validation fails
|
|
9
|
+
*/
|
|
10
|
+
function validateConfig(config) {
|
|
11
|
+
const errors = [];
|
|
12
|
+
|
|
13
|
+
// Required fields
|
|
14
|
+
const requiredFields = [
|
|
15
|
+
{ field: "inputPath", type: "string" },
|
|
16
|
+
{ field: "outputPath", type: "string" },
|
|
17
|
+
{ field: "appName", type: "string" },
|
|
18
|
+
{ field: "publisher", type: "string" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const { field, type } of requiredFields) {
|
|
22
|
+
if (!config[field]) {
|
|
23
|
+
errors.push(`Missing required field: ${field}`);
|
|
24
|
+
} else if (typeof config[field] !== type) {
|
|
25
|
+
errors.push(
|
|
26
|
+
`Field ${field} must be of type ${type}, got ${typeof config[field]}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate publisher format
|
|
32
|
+
if (config.publisher && !CONSTANTS.PUBLISHER_PATTERN.test(config.publisher)) {
|
|
33
|
+
errors.push('Publisher must start with "CN=" (e.g., "CN=MyCompany")');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate version format
|
|
37
|
+
if (config.version) {
|
|
38
|
+
if (!CONSTANTS.VERSION_PATTERN.test(config.version)) {
|
|
39
|
+
errors.push('Version must be in format x.x.x.x (e.g., "1.0.0.0")');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate architecture
|
|
44
|
+
if (
|
|
45
|
+
config.architecture &&
|
|
46
|
+
!CONSTANTS.SUPPORTED_ARCHITECTURES.includes(config.architecture)
|
|
47
|
+
) {
|
|
48
|
+
errors.push(
|
|
49
|
+
`Architecture must be one of: ${CONSTANTS.SUPPORTED_ARCHITECTURES.join(", ")}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate package name length
|
|
54
|
+
if (
|
|
55
|
+
config.packageName &&
|
|
56
|
+
config.packageName.length > CONSTANTS.MAX_PACKAGE_NAME_LENGTH
|
|
57
|
+
) {
|
|
58
|
+
errors.push(
|
|
59
|
+
`Package name must be ${CONSTANTS.MAX_PACKAGE_NAME_LENGTH} characters or less`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate capabilities
|
|
64
|
+
if (config.capabilities && !Array.isArray(config.capabilities)) {
|
|
65
|
+
errors.push("Capabilities must be an array");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (errors.length > 0) {
|
|
69
|
+
throw new ValidationError(
|
|
70
|
+
"config",
|
|
71
|
+
"valid configuration object",
|
|
72
|
+
errors.join("; "),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates that required paths exist
|
|
79
|
+
* @param {Object} config - Configuration object
|
|
80
|
+
* @throws {ValidationError} When paths don't exist
|
|
81
|
+
*/
|
|
82
|
+
async function validatePaths(config) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
|
|
85
|
+
// Check input path exists
|
|
86
|
+
if (!(await fs.pathExists(config.inputPath))) {
|
|
87
|
+
errors.push(`Input path does not exist: ${config.inputPath}`);
|
|
88
|
+
} else {
|
|
89
|
+
// Check if it's a directory
|
|
90
|
+
const stats = await fs.stat(config.inputPath);
|
|
91
|
+
if (!stats.isDirectory()) {
|
|
92
|
+
errors.push(`Input path must be a directory: ${config.inputPath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for package.json in input directory
|
|
96
|
+
const packageJsonPath = path.join(config.inputPath, "package.json");
|
|
97
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
98
|
+
errors.push(
|
|
99
|
+
`No package.json found in input directory: ${config.inputPath}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate icon path if provided
|
|
105
|
+
if (config.icon && !(await fs.pathExists(config.icon))) {
|
|
106
|
+
errors.push(`Icon file does not exist: ${config.icon}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
throw new ValidationError("paths", "existing paths", errors.join("; "));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sanitizes configuration values
|
|
116
|
+
* @param {Object} config - Configuration object to sanitize
|
|
117
|
+
* @returns {Object} Sanitized configuration
|
|
118
|
+
*/
|
|
119
|
+
function sanitizeConfig(config) {
|
|
120
|
+
const sanitized = { ...config };
|
|
121
|
+
|
|
122
|
+
// Set defaults
|
|
123
|
+
sanitized.version = sanitized.version || CONSTANTS.DEFAULT_VERSION;
|
|
124
|
+
sanitized.architecture =
|
|
125
|
+
sanitized.architecture || CONSTANTS.DEFAULT_ARCHITECTURE;
|
|
126
|
+
sanitized.executable = sanitized.executable || CONSTANTS.DEFAULT_EXECUTABLE;
|
|
127
|
+
sanitized.timestampUrl =
|
|
128
|
+
sanitized.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL;
|
|
129
|
+
sanitized.capabilities =
|
|
130
|
+
sanitized.capabilities || CONSTANTS.DEFAULT_CAPABILITIES;
|
|
131
|
+
|
|
132
|
+
// Sanitize strings
|
|
133
|
+
sanitized.appName = sanitized.appName.trim();
|
|
134
|
+
sanitized.publisher = sanitized.publisher.trim();
|
|
135
|
+
|
|
136
|
+
// Generate package name if not provided
|
|
137
|
+
if (!sanitized.packageName) {
|
|
138
|
+
const publisherName = sanitized.publisher
|
|
139
|
+
.replace(/^CN=/, "")
|
|
140
|
+
.split(",")[0]
|
|
141
|
+
.trim();
|
|
142
|
+
const appNameSafe = sanitized.appName.replace(/[^a-zA-Z0-9]/g, "");
|
|
143
|
+
sanitized.packageName = `${publisherName}.${appNameSafe}`.substring(
|
|
144
|
+
0,
|
|
145
|
+
CONSTANTS.MAX_PACKAGE_NAME_LENGTH,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Generate display name if not provided
|
|
150
|
+
sanitized.displayName = sanitized.displayName || sanitized.appName;
|
|
151
|
+
|
|
152
|
+
// Set description if not provided
|
|
153
|
+
sanitized.description =
|
|
154
|
+
sanitized.description ||
|
|
155
|
+
`${sanitized.appName} - Node.js application packaged as MSIX`;
|
|
156
|
+
|
|
157
|
+
// Set build options
|
|
158
|
+
sanitized.skipBuild = sanitized.skipBuild || false;
|
|
159
|
+
sanitized.installDevDeps = sanitized.installDevDeps !== false; // Default to true
|
|
160
|
+
|
|
161
|
+
// Normalize paths
|
|
162
|
+
sanitized.inputPath = path.resolve(sanitized.inputPath);
|
|
163
|
+
sanitized.outputPath = path.resolve(sanitized.outputPath);
|
|
164
|
+
|
|
165
|
+
if (sanitized.icon) {
|
|
166
|
+
sanitized.icon = path.resolve(sanitized.icon);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return sanitized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validates certificate configuration
|
|
174
|
+
* @param {Object} signingConfig - Signing configuration
|
|
175
|
+
* @throws {ValidationError} When signing configuration is invalid
|
|
176
|
+
*/
|
|
177
|
+
function validateSigningConfig(signingConfig) {
|
|
178
|
+
if (!signingConfig.sign) {
|
|
179
|
+
return; // No validation needed if signing is disabled
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const errors = [];
|
|
183
|
+
|
|
184
|
+
// At least one certificate identification method should be provided
|
|
185
|
+
const hasCertId =
|
|
186
|
+
signingConfig.certificateThumbprint ||
|
|
187
|
+
signingConfig.certificateSubject ||
|
|
188
|
+
signingConfig.certificatePath;
|
|
189
|
+
|
|
190
|
+
if (!hasCertId) {
|
|
191
|
+
// This is okay - we'll auto-discover certificates
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If certificatePath is provided, check if password is needed
|
|
196
|
+
if (signingConfig.certificatePath && !signingConfig.certificatePassword) {
|
|
197
|
+
// Warning but not error - some PFX files don't need passwords
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate thumbprint format if provided
|
|
201
|
+
if (signingConfig.certificateThumbprint) {
|
|
202
|
+
const thumbprintPattern = /^[A-Fa-f0-9]{40}$/;
|
|
203
|
+
if (
|
|
204
|
+
!thumbprintPattern.test(
|
|
205
|
+
signingConfig.certificateThumbprint.replace(/\s/g, ""),
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
errors.push(
|
|
209
|
+
"Certificate thumbprint must be a 40-character hexadecimal string",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (errors.length > 0) {
|
|
215
|
+
throw new ValidationError(
|
|
216
|
+
"signingConfig",
|
|
217
|
+
"valid signing configuration",
|
|
218
|
+
errors.join("; "),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
validateConfig,
|
|
225
|
+
validatePaths,
|
|
226
|
+
sanitizeConfig,
|
|
227
|
+
validateSigningConfig,
|
|
228
|
+
};
|