@lightharu/krouter 1.8.0

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.
Files changed (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. package/src/renderer/src/assets/krouter-mark.svg +9 -0
@@ -0,0 +1,9 @@
1
+ <svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="8" y="8" width="80" height="80" rx="22" fill="#0B1220"/>
3
+ <path d="M28 68V28h10v16.8L54.2 28H67L49.8 45.5 68.5 68H55.4L42.9 52.7 38 57.7V68H28Z" fill="#FFFFFF"/>
4
+ <path d="M63 28h5v5h-5v-5Z" fill="#22C55E"/>
5
+ <path d="M69 34h5v5h-5v-5Z" fill="#38BDF8"/>
6
+ <path d="M63 40h5v5h-5v-5Z" fill="#F59E0B"/>
7
+ <path d="M62.5 32.5 49.5 45.5M68.5 36.5 52 52M62.5 42.5 55.5 56" stroke="#94A3B8" stroke-width="2" stroke-linecap="round"/>
8
+ <circle cx="48" cy="48" r="5" fill="#38BDF8"/>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="8" y="8" width="80" height="80" rx="22" fill="#0B1220"/>
3
+ <path d="M28 68V28h10v16.8L54.2 28H67L49.8 45.5 68.5 68H55.4L42.9 52.7 38 57.7V68H28Z" fill="#FFFFFF"/>
4
+ <path d="M63 28h5v5h-5v-5Z" fill="#22C55E"/>
5
+ <path d="M69 34h5v5h-5v-5Z" fill="#38BDF8"/>
6
+ <path d="M63 40h5v5h-5v-5Z" fill="#F59E0B"/>
7
+ <path d="M62.5 32.5 49.5 45.5M68.5 36.5 52 52M62.5 42.5 55.5 56" stroke="#94A3B8" stroke-width="2" stroke-linecap="round"/>
8
+ <circle cx="48" cy="48" r="5" fill="#38BDF8"/>
9
+ </svg>
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Krouter</title>
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
8
+ <meta
9
+ http-equiv="Content-Security-Policy"
10
+ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
11
+ />
12
+ <script type="module" crossorigin src="/assets/index-DCslvfUR.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-CM4-0adf.css">
14
+ </head>
15
+
16
+ <body>
17
+ <div id="root"></div>
18
+ </body>
19
+ </html>
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ // Kiro IDE Auth Token 同步层
3
+ //
4
+ // Kiro IDE 桌面端把 token 持久化在 ~/.aws/sso/cache/kiro-auth-token.json,
5
+ // 并对该文件做 fs.watchFile 监听 + 内部 refresh loop。
6
+ //
7
+ // 反代和 IDE 必须以这个文件作为 single source of truth,否则会出现:
8
+ // 反代 store 里 refreshToken_v2,磁盘里 refreshToken_v1(已被服务端轮换作废)
9
+ // → IDE 一小时后用 v1 调 OIDC → 401 → logoutAndForget()
10
+ //
11
+ // 本模块提供:
12
+ // writeKiroAuthTokenFile — 以 Kiro IDE 兼容格式写入 token 文件(+ IdC 客户端注册)
13
+ // readKiroAuthTokenFile — 读取当前磁盘 token
14
+ // parseAccessTokenClaims — 解 accessToken 的 JWT 拿到 sub/email,用于反向匹配账号
15
+ // watchKiroAuthTokenFile — 监听文件变化(IDE 自己 refresh 时用于反向同步到反代 store)
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.KIRO_SOCIAL_PROFILE_ARN = exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN = exports.KIRO_AUTH_TOKEN_PATH = exports.KIRO_SSO_CACHE_DIR = void 0;
51
+ exports.isPlaceholderProfileArn = isPlaceholderProfileArn;
52
+ exports.resolveProfileArnForWrite = resolveProfileArnForWrite;
53
+ exports.writeKiroAuthTokenFile = writeKiroAuthTokenFile;
54
+ exports.readKiroAuthTokenFile = readKiroAuthTokenFile;
55
+ exports.parseAccessTokenClaims = parseAccessTokenClaims;
56
+ exports.watchKiroAuthTokenFile = watchKiroAuthTokenFile;
57
+ const fs = __importStar(require("fs/promises"));
58
+ const fsSync = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ const os = __importStar(require("os"));
61
+ const crypto = __importStar(require("crypto"));
62
+ exports.KIRO_SSO_CACHE_DIR = path.join(os.homedir(), '.aws', 'sso', 'cache');
63
+ exports.KIRO_AUTH_TOKEN_PATH = path.join(exports.KIRO_SSO_CACHE_DIR, 'kiro-auth-token.json');
64
+ const KIRO_DEFAULT_START_URL = 'https://view.awsapps.com/start';
65
+ const KIRO_OIDC_SCOPES = [
66
+ 'codewhisperer:completions',
67
+ 'codewhisperer:analysis',
68
+ 'codewhisperer:conversations',
69
+ 'codewhisperer:transformations',
70
+ 'codewhisperer:taskassist'
71
+ ];
72
+ // =============== profileArn 决策中心 ===============
73
+ //
74
+ // 占位符 ARN:Kiro IDE 源码 FixedProfileArns 里给 BuilderId 硬编码的值。
75
+ // Kiro IDE 内部逻辑依赖该字段存在,移除会导致 IDE 功能异常。
76
+ exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN = 'arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX';
77
+ // Social 登录(Github/Google)共用的 Kiro 后端固定 profileArn
78
+ exports.KIRO_SOCIAL_PROFILE_ARN = 'arn:aws:codewhisperer:us-east-1:699475941385:profile/EHGA3GRVQMUK';
79
+ const PLACEHOLDER_PROFILE_ARNS = new Set([exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN]);
80
+ /** 检查给定 ARN 是不是已知占位符(旧版反代 / Kiro IDE 自身可能写入的脏数据) */
81
+ function isPlaceholderProfileArn(arn) {
82
+ if (!arn)
83
+ return false;
84
+ return PLACEHOLDER_PROFILE_ARNS.has(arn);
85
+ }
86
+ /**
87
+ * 写入 token 文件前对 profileArn 的"应该写啥"做统一决策。
88
+ *
89
+ * 规则(优先级):
90
+ * 1. 调用方显式给出 profileArn 且非已知占位符 → 直接用
91
+ * 2. social/Github/Google → 用固定 Kiro Social profileArn
92
+ * 3. BuilderId / 其它 → 使用 Kiro IDE 官方占位符 ARN(IDE 内部逻辑依赖此字段存在)
93
+ */
94
+ function resolveProfileArnForWrite(input) {
95
+ if (input.profileArn && !isPlaceholderProfileArn(input.profileArn)) {
96
+ return input.profileArn;
97
+ }
98
+ if (input.authMethod === 'social' || input.provider === 'Github' || input.provider === 'Google') {
99
+ return exports.KIRO_SOCIAL_PROFILE_ARN;
100
+ }
101
+ return exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN;
102
+ }
103
+ function computeClientIdHash(startUrl) {
104
+ return crypto
105
+ .createHash('sha1')
106
+ .update(JSON.stringify({ startUrl: startUrl || KIRO_DEFAULT_START_URL }))
107
+ .digest('hex');
108
+ }
109
+ /**
110
+ * 以与 Kiro IDE 完全兼容的格式写入 ~/.aws/sso/cache/kiro-auth-token.json
111
+ * - mode 0o600:与 IDE 保持一致(writeTokenToDisk 用 0o600 即 384)
112
+ * - social 与 IdC 字段顺序对齐 Kiro IDE 序列化结果,便于人工 diff
113
+ * - IdC 同时写客户端注册文件 {clientIdHash}.json
114
+ */
115
+ async function writeKiroAuthTokenFile(input) {
116
+ await fs.mkdir(exports.KIRO_SSO_CACHE_DIR, { recursive: true });
117
+ const clientIdHash = computeClientIdHash(input.startUrl);
118
+ const tokenData = input.authMethod === 'social'
119
+ ? {
120
+ accessToken: input.accessToken,
121
+ refreshToken: input.refreshToken,
122
+ profileArn: input.profileArn,
123
+ expiresAt: input.expiresAtIso,
124
+ authMethod: input.authMethod,
125
+ provider: input.provider
126
+ }
127
+ : {
128
+ accessToken: input.accessToken,
129
+ refreshToken: input.refreshToken,
130
+ expiresAt: input.expiresAtIso,
131
+ clientIdHash,
132
+ authMethod: input.authMethod,
133
+ provider: input.provider,
134
+ region: input.region || 'us-east-1',
135
+ profileArn: input.profileArn
136
+ };
137
+ await fs.writeFile(exports.KIRO_AUTH_TOKEN_PATH, JSON.stringify(tokenData, null, 2), {
138
+ mode: 0o600
139
+ });
140
+ // Windows 上 chmod 对 0o600 是 no-op 但不抛错;Linux/macOS 上保证权限正确
141
+ try {
142
+ await fs.chmod(exports.KIRO_AUTH_TOKEN_PATH, 0o600);
143
+ }
144
+ catch {
145
+ /* ignore */
146
+ }
147
+ let clientRegPath;
148
+ if (input.authMethod !== 'social' && input.clientId && input.clientSecret) {
149
+ clientRegPath = path.join(exports.KIRO_SSO_CACHE_DIR, `${clientIdHash}.json`);
150
+ // IDE 客户端注册有效期 90 天(Kiro IDE 原版做法),格式为去掉 Z 的 ISO 字符串
151
+ const clientExpiresAt = new Date(Date.now() + 90 * 24 * 3600 * 1000).toISOString().replace('Z', '');
152
+ const clientData = {
153
+ clientId: input.clientId,
154
+ clientSecret: input.clientSecret,
155
+ expiresAt: clientExpiresAt,
156
+ scopes: KIRO_OIDC_SCOPES
157
+ };
158
+ await fs.writeFile(clientRegPath, JSON.stringify(clientData, null, 2), { mode: 0o600 });
159
+ try {
160
+ await fs.chmod(clientRegPath, 0o600);
161
+ }
162
+ catch {
163
+ /* ignore */
164
+ }
165
+ }
166
+ return { tokenPath: exports.KIRO_AUTH_TOKEN_PATH, clientRegPath };
167
+ }
168
+ async function readKiroAuthTokenFile() {
169
+ try {
170
+ const content = await fs.readFile(exports.KIRO_AUTH_TOKEN_PATH, 'utf-8');
171
+ const parsed = JSON.parse(content);
172
+ if (!parsed.accessToken || !parsed.refreshToken)
173
+ return null;
174
+ return parsed;
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ function parseAccessTokenClaims(accessToken) {
181
+ if (!accessToken)
182
+ return null;
183
+ const parts = accessToken.split('.');
184
+ if (parts.length < 2)
185
+ return null;
186
+ try {
187
+ let b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
188
+ while (b64.length % 4)
189
+ b64 += '=';
190
+ const json = Buffer.from(b64, 'base64').toString('utf-8');
191
+ const claims = JSON.parse(json);
192
+ const audRaw = claims.aud;
193
+ const aud = typeof audRaw === 'string' ? audRaw : Array.isArray(audRaw) && typeof audRaw[0] === 'string' ? audRaw[0] : undefined;
194
+ return {
195
+ sub: typeof claims.sub === 'string' ? claims.sub : undefined,
196
+ email: typeof claims.email === 'string' ? claims.email : undefined,
197
+ aud,
198
+ preferredUsername: typeof claims['preferred_username'] === 'string'
199
+ ? claims['preferred_username']
200
+ : undefined
201
+ };
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ function watchKiroAuthTokenFile(onChange, intervalMs = 2000) {
208
+ let debounceTimer = null;
209
+ let lastSeenSig = '';
210
+ let disposed = false;
211
+ const tick = async () => {
212
+ if (disposed)
213
+ return;
214
+ try {
215
+ const token = await readKiroAuthTokenFile();
216
+ if (!token)
217
+ return;
218
+ const sig = `${token.accessToken}|${token.refreshToken}`;
219
+ if (sig === lastSeenSig)
220
+ return;
221
+ lastSeenSig = sig;
222
+ await onChange(token);
223
+ }
224
+ catch (e) {
225
+ // 静默:watcher 不应该 throw 影响主进程
226
+ console.warn('[kiroAuthSync] watcher tick failed:', e);
227
+ }
228
+ };
229
+ const listener = () => {
230
+ if (debounceTimer)
231
+ clearTimeout(debounceTimer);
232
+ debounceTimer = setTimeout(() => {
233
+ debounceTimer = null;
234
+ void tick();
235
+ }, 600);
236
+ };
237
+ // 先做一次基线读,避免启动后第一次"虚假变更"
238
+ void readKiroAuthTokenFile().then((t) => {
239
+ if (t)
240
+ lastSeenSig = `${t.accessToken}|${t.refreshToken}`;
241
+ });
242
+ fsSync.watchFile(exports.KIRO_AUTH_TOKEN_PATH, { interval: intervalMs }, listener);
243
+ return () => {
244
+ disposed = true;
245
+ if (debounceTimer)
246
+ clearTimeout(debounceTimer);
247
+ fsSync.unwatchFile(exports.KIRO_AUTH_TOKEN_PATH, listener);
248
+ };
249
+ }
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CertManager = void 0;
37
+ exports.createCertManager = createCertManager;
38
+ // K-Proxy CA 证书管理
39
+ const forge = __importStar(require("node-forge"));
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const crypto = __importStar(require("crypto"));
43
+ const CA_CERT_FILENAME = 'kproxy-ca.crt';
44
+ const CA_KEY_FILENAME = 'kproxy-ca.key';
45
+ const CERT_CACHE_DIR = 'kproxy-certs';
46
+ // 证书缓存(避免重复生成)
47
+ const certCache = new Map();
48
+ /**
49
+ * CA 证书管理器
50
+ */
51
+ class CertManager {
52
+ dataPath;
53
+ caCert = null;
54
+ caKey = null;
55
+ caInfo = null;
56
+ constructor(dataPath) {
57
+ this.dataPath = dataPath;
58
+ }
59
+ /**
60
+ * 初始化 CA 证书(加载或生成)
61
+ */
62
+ async initialize() {
63
+ const certPath = path.join(this.dataPath, CA_CERT_FILENAME);
64
+ const keyPath = path.join(this.dataPath, CA_KEY_FILENAME);
65
+ // 确保证书缓存目录存在
66
+ const cachePath = path.join(this.dataPath, CERT_CACHE_DIR);
67
+ if (!fs.existsSync(cachePath)) {
68
+ fs.mkdirSync(cachePath, { recursive: true });
69
+ }
70
+ // 尝试加载现有证书
71
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
72
+ try {
73
+ const certPem = fs.readFileSync(certPath, 'utf8');
74
+ const keyPem = fs.readFileSync(keyPath, 'utf8');
75
+ this.caCert = forge.pki.certificateFromPem(certPem);
76
+ this.caKey = forge.pki.privateKeyFromPem(keyPem);
77
+ // 检查证书是否过期
78
+ const now = new Date();
79
+ if (this.caCert.validity.notAfter > now) {
80
+ this.caInfo = this.extractCertInfo(certPath, keyPath, certPem, keyPem);
81
+ console.log('[CertManager] Loaded existing CA certificate');
82
+ return this.caInfo;
83
+ }
84
+ console.log('[CertManager] CA certificate expired, regenerating...');
85
+ }
86
+ catch (error) {
87
+ console.error('[CertManager] Failed to load CA certificate:', error);
88
+ }
89
+ }
90
+ // 生成新的 CA 证书
91
+ return this.generateCACert(certPath, keyPath);
92
+ }
93
+ /**
94
+ * 生成 CA 证书
95
+ */
96
+ generateCACert(certPath, keyPath) {
97
+ console.log('[CertManager] Generating new CA certificate...');
98
+ // 生成 RSA 密钥对
99
+ const keys = forge.pki.rsa.generateKeyPair(2048);
100
+ // 创建证书
101
+ const cert = forge.pki.createCertificate();
102
+ cert.publicKey = keys.publicKey;
103
+ cert.serialNumber = this.generateSerialNumber();
104
+ // 设置有效期(10年)
105
+ cert.validity.notBefore = new Date();
106
+ cert.validity.notAfter = new Date();
107
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
108
+ // 设置证书属性
109
+ const attrs = [
110
+ { name: 'commonName', value: 'K-Proxy CA' },
111
+ { name: 'organizationName', value: 'Krouter' },
112
+ { name: 'countryName', value: 'CN' }
113
+ ];
114
+ cert.setSubject(attrs);
115
+ cert.setIssuer(attrs);
116
+ // 设置 CA 扩展
117
+ cert.setExtensions([
118
+ {
119
+ name: 'basicConstraints',
120
+ cA: true,
121
+ critical: true
122
+ },
123
+ {
124
+ name: 'keyUsage',
125
+ keyCertSign: true,
126
+ cRLSign: true,
127
+ critical: true
128
+ },
129
+ {
130
+ name: 'subjectKeyIdentifier'
131
+ }
132
+ ]);
133
+ // 自签名
134
+ cert.sign(keys.privateKey, forge.md.sha256.create());
135
+ // 转换为 PEM 格式
136
+ const certPem = forge.pki.certificateToPem(cert);
137
+ const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
138
+ // 保存到文件
139
+ fs.writeFileSync(certPath, certPem);
140
+ fs.writeFileSync(keyPath, keyPem);
141
+ this.caCert = cert;
142
+ this.caKey = keys.privateKey;
143
+ this.caInfo = this.extractCertInfo(certPath, keyPath, certPem, keyPem);
144
+ console.log('[CertManager] CA certificate generated successfully');
145
+ return this.caInfo;
146
+ }
147
+ /**
148
+ * 为指定域名生成证书
149
+ */
150
+ generateCertForHost(hostname) {
151
+ // 检查缓存
152
+ const cached = certCache.get(hostname);
153
+ if (cached) {
154
+ return cached;
155
+ }
156
+ if (!this.caCert || !this.caKey) {
157
+ throw new Error('CA certificate not initialized');
158
+ }
159
+ // 生成密钥对
160
+ const keys = forge.pki.rsa.generateKeyPair(2048);
161
+ // 创建证书
162
+ const cert = forge.pki.createCertificate();
163
+ cert.publicKey = keys.publicKey;
164
+ cert.serialNumber = this.generateSerialNumber();
165
+ // 设置有效期(1年)
166
+ cert.validity.notBefore = new Date();
167
+ cert.validity.notAfter = new Date();
168
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
169
+ // 设置证书属性
170
+ const attrs = [
171
+ { name: 'commonName', value: hostname },
172
+ { name: 'organizationName', value: 'K-Proxy' }
173
+ ];
174
+ cert.setSubject(attrs);
175
+ cert.setIssuer(this.caCert.subject.attributes);
176
+ // 设置扩展
177
+ cert.setExtensions([
178
+ {
179
+ name: 'basicConstraints',
180
+ cA: false
181
+ },
182
+ {
183
+ name: 'keyUsage',
184
+ digitalSignature: true,
185
+ keyEncipherment: true
186
+ },
187
+ {
188
+ name: 'extKeyUsage',
189
+ serverAuth: true
190
+ },
191
+ {
192
+ name: 'subjectAltName',
193
+ altNames: [
194
+ { type: 2, value: hostname }, // DNS
195
+ { type: 2, value: '*.' + hostname } // 通配符
196
+ ]
197
+ }
198
+ ]);
199
+ // 使用 CA 私钥签名
200
+ cert.sign(this.caKey, forge.md.sha256.create());
201
+ const result = {
202
+ cert: forge.pki.certificateToPem(cert),
203
+ key: forge.pki.privateKeyToPem(keys.privateKey)
204
+ };
205
+ // 缓存证书
206
+ certCache.set(hostname, result);
207
+ return result;
208
+ }
209
+ /**
210
+ * 获取 CA 证书信息
211
+ */
212
+ getCACertInfo() {
213
+ return this.caInfo;
214
+ }
215
+ /**
216
+ * 获取 CA 证书 PEM
217
+ */
218
+ getCACertPem() {
219
+ return this.caInfo?.certPem || null;
220
+ }
221
+ /**
222
+ * 清除证书缓存
223
+ */
224
+ clearCache() {
225
+ certCache.clear();
226
+ }
227
+ /**
228
+ * 生成序列号
229
+ */
230
+ generateSerialNumber() {
231
+ return crypto.randomBytes(16).toString('hex');
232
+ }
233
+ /**
234
+ * 提取证书信息
235
+ */
236
+ extractCertInfo(certPath, keyPath, certPem, keyPem) {
237
+ const cert = forge.pki.certificateFromPem(certPem);
238
+ const fingerprint = forge.md.sha256.create()
239
+ .update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes())
240
+ .digest()
241
+ .toHex()
242
+ .match(/.{2}/g)
243
+ .join(':')
244
+ .toUpperCase();
245
+ return {
246
+ certPath,
247
+ keyPath,
248
+ certPem,
249
+ keyPem,
250
+ fingerprint,
251
+ validFrom: cert.validity.notBefore,
252
+ validTo: cert.validity.notAfter
253
+ };
254
+ }
255
+ }
256
+ exports.CertManager = CertManager;
257
+ /**
258
+ * 创建证书管理器实例
259
+ */
260
+ function createCertManager(dataPath) {
261
+ return new CertManager(dataPath);
262
+ }