Files
koring-launcher/scripts/signpath-sign.js
T
dream_pep 3653e26a09 feat: 发布 v1.2.0 版本,新增多项核心功能
- 引入 electron-updater 实现自动更新功能
- 新增 Java 环境扫描与校验的 IPC 处理逻辑
- 实现离线账号登录功能
- 新增配置变更跨进程广播机制
- 重构游戏启动逻辑,使用主进程内存配置作为唯一权威来源
- 新增界面显示与语言设置的配置页面
- 添加 Windows 平台自动发布 CI 流水线
- 迁移旧版配置/认证文件到用户数据目录
- 修复崩溃日志路径、表单控件等多项 bug
- 重构设置页组件系统统一界面样式
2026-08-28 02:08:12 +08:00

158 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SignPath 远程代码签名模块(electron-builder 自定义签名)
*
* 挂载方式:electron-builder.yml -> win.sign: scripts/signpath-sign.js
*
* electron-builder 在构建 Windows 产物时会对每个待签名文件调用本模块,
* 本模块把文件提交到 SignPath 签名后,下载签名产物并原位覆盖。
*
* 实现与 SignPath 官方 PowerShell 模块(Submit-SigningRequest)对齐:
* POST {base}/v1/{orgId}/SigningRequests multipart/form-data,响应 Location 头 = 请求 URL
* GET {base}/v1/{orgId}/SigningRequests/{id} 轮询 status / isFinalStatus,完成后返回 signedArtifactLink
* GET signedArtifactLink 下载签名产物
* 认证:Authorization: Bearer <API token>
*
* 环境变量(CI 中由 GitHub Secrets 注入;本地构建未设置时自动跳过签名):
* SIGNPATH_API_TOKEN 必填(API Token
* SIGNPATH_ORG_ID 必填(SignPath 组织 ID
* SIGNPATH_PROJECT_SLUG 可选,默认 Koring_Launcher
* SIGNPATH_SIGNING_POLICY_SLUG 签名策略 slug(必填,如 Koring_Launcher_Dev_builder
* SIGNPATH_ARTIFACT_CONFIG_SLUG 产物配置 slug;项目只有一个配置时可省略
* SIGNPATH_API_BASE 可选,默认 https://app.signpath.io/api
*
* 说明:
* - 无 SIGNPATH_API_TOKEN / SIGNPATH_ORG_ID 时直接返回(不签名),
* 因此本地 `pnpm dist:dev/beta/run` 行为与之前完全一致。
* - 签名发生在 electron-builder 生成 blockmap 与 latest.yml 之前,
* 所以清单中的 sha512 自动对应签名后的安装包。
* - 参考文档:https://docs.signpath.io/build-system-integration
*/
'use strict';
const fs = require('fs');
const path = require('path');
const API_BASE = process.env.SIGNPATH_API_BASE || 'https://app.signpath.io/api';
const PROJECT_SLUG = process.env.SIGNPATH_PROJECT_SLUG || 'Koring_Launcher';
const POLL_INTERVAL_MS = 5000;
const POLL_TIMEOUT_MS = 15 * 60 * 1000; // 单文件签名最长等待 15 分钟
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function failWithDetail(res, ctx) {
let detail = '';
try {
detail = await res.text();
} catch {
/* ignore */
}
throw new Error(`[signpath-sign] ${ctx} failed (${res.status}): ${detail}`);
}
async function signWithSignPath(filePath) {
const token = process.env.SIGNPATH_API_TOKEN;
const orgId = process.env.SIGNPATH_ORG_ID;
const policySlug = process.env.SIGNPATH_SIGNING_POLICY_SLUG;
const artifactSlug = process.env.SIGNPATH_ARTIFACT_CONFIG_SLUG;
if (!policySlug) {
throw new Error('[signpath-sign] SIGNPATH_SIGNING_POLICY_SLUG is required when signing is enabled');
}
const headers = { Authorization: `Bearer ${token}` };
// 1. 提交签名请求(multipart/form-data,与官方 PowerShell 模块一致)
const fileBuf = fs.readFileSync(filePath);
const form = new FormData();
form.append('ProjectSlug', PROJECT_SLUG);
form.append('SigningPolicySlug', policySlug);
if (artifactSlug) {
form.append('ArtifactConfigurationSlug', artifactSlug);
}
form.append('Description', `electron-builder auto-sign: ${path.basename(filePath)}`);
form.append('Artifact', new Blob([fileBuf]), path.basename(filePath));
console.log(
`[signpath-sign] submitting signing request: ${path.basename(filePath)} (${(fileBuf.length / 1024 / 1024).toFixed(1)} MB)`
);
const submitRes = await fetch(`${API_BASE}/v1/${orgId}/SigningRequests`, {
method: 'POST',
headers,
body: form,
});
if (!submitRes.ok) {
await failWithDetail(submitRes, 'submit');
}
const requestUrl = submitRes.headers.get('location');
if (!requestUrl) {
throw new Error('[signpath-sign] submit response has no Location header');
}
console.log(`[signpath-sign] request created: ${requestUrl}`);
// 2. 轮询直到最终状态(isFinalStatus),要求 Completed
const deadline = Date.now() + POLL_TIMEOUT_MS;
let data = null;
for (;;) {
await sleep(POLL_INTERVAL_MS);
const pollRes = await fetch(requestUrl, { headers });
if (!pollRes.ok) {
await failWithDetail(pollRes, 'status query');
}
data = await pollRes.json();
console.log(`[signpath-sign] status: ${data.status}${data.workflowStatus ? ` / ${data.workflowStatus}` : ''}`);
if (data.isFinalStatus) break;
if (Date.now() > deadline) {
throw new Error('[signpath-sign] signing timed out');
}
}
if (data.status !== 'Completed') {
throw new Error(`[signpath-sign] request ${data.status}${data.workflowStatus ? ` / ${data.workflowStatus}` : ''}`);
}
// 3. 通过 signedArtifactLink 下载签名产物并原位覆盖(网络错误重试)
const downloadUrl = data.signedArtifactLink;
if (!downloadUrl) {
throw new Error('[signpath-sign] completed request has no signedArtifactLink');
}
const signedBuf = await downloadWithRetry(downloadUrl, headers);
const tmpPath = `${filePath}.signing.tmp`;
fs.writeFileSync(tmpPath, signedBuf);
fs.renameSync(tmpPath, filePath);
console.log(`[signpath-sign] signed OK (${(signedBuf.length / 1024 / 1024).toFixed(1)} MB)`);
}
const DOWNLOAD_MAX_ATTEMPTS = 3;
async function downloadWithRetry(downloadUrl, headers) {
let lastError = null;
for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
try {
const dlRes = await fetch(downloadUrl, { headers });
if (!dlRes.ok) {
await failWithDetail(dlRes, 'download signed artifact');
}
return Buffer.from(await dlRes.arrayBuffer());
} catch (err) {
lastError = err;
console.warn(`[signpath-sign] download attempt ${attempt}/${DOWNLOAD_MAX_ATTEMPTS} failed: ${err.message}`);
if (attempt < DOWNLOAD_MAX_ATTEMPTS) {
await sleep(3000 * attempt);
}
}
}
throw lastError;
}
/**
* electron-builder 自定义签名入口。
* configuration.path 为待签名文件绝对路径;无 token 时跳过(本地开发构建)。
*/
module.exports = async function signPathSign(configuration) {
if (!process.env.SIGNPATH_API_TOKEN || !process.env.SIGNPATH_ORG_ID) {
console.log(`[signpath-sign] no SignPath credentials, skip signing: ${configuration.name}`);
return;
}
await signWithSignPath(configuration.path);
};