Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

171 lines
4.4 KiB
JavaScript
Raw Permalink 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.

/**
* services/zhuyuan-bridge/lib/github-auth.js
*
* GitHub App 鉴权模块
* - 用 App Private Key 生成 JWT
* - 获取 Installation Access Token
* - 查找仓库对应的 Installation ID
*
* 环境变量:
* GHAPP_APP_ID — GitHub App ID
* GHAPP_PRIVATE_KEY — GitHub App Private Key (PEM)
*/
'use strict';
const https = require('https');
// 模块级检查jsonwebtoken 是否可用
let jwtLib = null;
try { jwtLib = require('jsonwebtoken'); } catch (_) { /* fallback to crypto */ }
/**
* 用 App Private Key 生成 JWT
* 优先使用 jsonwebtoken 库,不可用时降级到 Node.js crypto 实现
*/
function generateJWT() {
const privateKey = process.env.GHAPP_PRIVATE_KEY;
const appId = process.env.GHAPP_APP_ID;
if (!privateKey || !appId) {
throw new Error('GHAPP_APP_ID 和 GHAPP_PRIVATE_KEY 环境变量必须设置');
}
if (jwtLib) {
const payload = {
iat: Math.floor(Date.now() / 1000) - 60,
exp: Math.floor(Date.now() / 1000) + (10 * 60),
iss: appId
};
return jwtLib.sign(payload, privateKey, { algorithm: 'RS256' });
}
// jsonwebtoken 不可用时,使用 Node.js crypto 实现
const crypto = require('crypto');
function base64url(buf) {
return buf.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
const header = base64url(Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })));
const now = Math.floor(Date.now() / 1000);
const payload = base64url(Buffer.from(JSON.stringify({
iat: now - 60,
exp: now + 600,
iss: appId
})));
const sigInput = header + '.' + payload;
const sign = crypto.createSign('RSA-SHA256');
sign.update(sigInput);
sign.end();
const signature = base64url(sign.sign(privateKey));
return sigInput + '.' + signature;
}
/**
* GitHub API 请求封装
*/
function githubRequest(method, apiPath, token, body) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: apiPath,
method: method,
headers: {
'User-Agent': 'zhuyuan-bridge/1.0',
'Accept': 'application/vnd.github+json',
'Authorization': token.startsWith('Bearer ')
? token
: `token ${token}`
}
};
if (body) {
options.headers['Content-Type'] = 'application/json';
}
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(data) });
} catch (_) {
resolve({ status: res.statusCode, data: data });
}
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
/**
* 获取某个 Installation 的操作令牌
* @param {number|string} installationId
* @returns {Promise<string>} Installation access token
*/
async function getInstallationToken(installationId) {
const jwtToken = generateJWT();
const result = await githubRequest(
'POST',
`/app/installations/${installationId}/access_tokens`,
`Bearer ${jwtToken}`
);
if (result.status !== 201 || !result.data.token) {
throw new Error(`获取 installation token 失败: ${result.status} ${JSON.stringify(result.data)}`);
}
return result.data.token;
}
/**
* 查找某个仓库的 Installation ID
* @param {string} owner
* @param {string} repo
* @returns {Promise<number|null>}
*/
async function findInstallation(owner, repo) {
const jwtToken = generateJWT();
const result = await githubRequest(
'GET',
'/app/installations',
`Bearer ${jwtToken}`
);
if (result.status !== 200 || !Array.isArray(result.data)) {
console.warn('⚠️ 获取 installations 列表失败:', result.status);
return null;
}
for (const inst of result.data) {
try {
const token = await getInstallationToken(inst.id);
const reposResult = await githubRequest(
'GET',
'/installation/repositories?per_page=100',
token
);
if (reposResult.data.repositories) {
const found = reposResult.data.repositories.some(
r => r.full_name === `${owner}/${repo}`
);
if (found) return inst.id;
}
} catch (e) {
console.warn(`⚠️ 检查 installation ${inst.id} 失败:`, e.message);
}
}
return null;
}
module.exports = { generateJWT, getInstallationToken, findInstallation, githubRequest };